Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4354227a05 |
No files matched your search
@@ -19,7 +19,7 @@ on:
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
FLUTTER_VERSION: "3.27.4"
|
||||
RUST_TOOLCHAIN: "1.85.0"
|
||||
RUST_TOOLCHAIN: "1.84.0"
|
||||
CARGO_MAKE_VERSION: "0.37.18"
|
||||
CLOUD_VERSION: 0.6.54-amd64
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ on:
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
FLUTTER_VERSION: "3.27.4"
|
||||
RUST_TOOLCHAIN: "1.85.0"
|
||||
RUST_TOOLCHAIN: "1.84.0"
|
||||
CARGO_MAKE_VERSION: "0.37.18"
|
||||
CLOUD_VERSION: 0.9.49-amd64
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ on:
|
||||
|
||||
env:
|
||||
FLUTTER_VERSION: "3.27.4"
|
||||
RUST_TOOLCHAIN: "1.85.0"
|
||||
RUST_TOOLCHAIN: "1.84.0"
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
|
||||
@@ -7,7 +7,7 @@ on:
|
||||
|
||||
env:
|
||||
FLUTTER_VERSION: "3.27.4"
|
||||
RUST_TOOLCHAIN: "1.85.0"
|
||||
RUST_TOOLCHAIN: "1.84.0"
|
||||
|
||||
jobs:
|
||||
create-release:
|
||||
@@ -180,24 +180,17 @@ jobs:
|
||||
- name: Create macOS dmg
|
||||
run: |
|
||||
brew install create-dmg
|
||||
i=0
|
||||
until [[ -e "${{ env.MACOS_APP_RELEASE_PATH }}/${{ env.MACOS_DMG_NAME }}.dmg" ]]; do
|
||||
create-dmg \
|
||||
--volname ${{ env.MACOS_DMG_NAME }} \
|
||||
--hide-extension "AppFlowy.app" \
|
||||
--background frontend/scripts/dmg_assets/AppFlowyInstallerBackground.jpg \
|
||||
--window-size 600 450 \
|
||||
--icon-size 94 \
|
||||
--icon "AppFlowy.app" 141 249 \
|
||||
--app-drop-link 458 249 \
|
||||
"${{ env.MACOS_APP_RELEASE_PATH }}/${{ env.MACOS_DMG_NAME }}.dmg" \
|
||||
"${{ env.MACOS_APP_RELEASE_PATH }}/AppFlowy.app" || true
|
||||
if [[ $i -eq 10 ]]; then
|
||||
echo 'Error: create-dmg did not succeed even after 10 tries.'
|
||||
exit 1
|
||||
fi
|
||||
i=$((i+1))
|
||||
done
|
||||
create-dmg \
|
||||
--volname ${{ env.MACOS_DMG_NAME }} \
|
||||
--hide-extension "AppFlowy.app" \
|
||||
--background frontend/scripts/dmg_assets/AppFlowyInstallerBackground.jpg \
|
||||
--window-size 600 450 \
|
||||
--icon-size 94 \
|
||||
--icon "AppFlowy.app" 141 249 \
|
||||
--app-drop-link 458 249 \
|
||||
"${{ env.MACOS_APP_RELEASE_PATH }}/${{ env.MACOS_DMG_NAME }}.dmg" \
|
||||
"${{ env.MACOS_APP_RELEASE_PATH }}/AppFlowy.app"
|
||||
|
||||
- name: Notarize AppFlowy
|
||||
run: |
|
||||
xcrun notarytool submit ${{ env.MACOS_APP_RELEASE_PATH }}/${{ env.MACOS_DMG_NAME }}.dmg --apple-id ${{ secrets.MACOS_NOTARY_USER }} --team-id ${{ secrets.MACOS_TEAM_ID }} --password ${{ secrets.MACOS_NOTARY_PWD }} -v -f "json" --wait
|
||||
@@ -240,7 +233,7 @@ jobs:
|
||||
job:
|
||||
- {
|
||||
targets: "aarch64-apple-darwin,x86_64-apple-darwin",
|
||||
os: macos-14,
|
||||
os: macos-latest,
|
||||
extra-build-args: "",
|
||||
}
|
||||
steps:
|
||||
|
||||
@@ -19,7 +19,7 @@ on:
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
CLOUD_VERSION: 0.9.49-amd64
|
||||
RUST_TOOLCHAIN: "1.85.0"
|
||||
RUST_TOOLCHAIN: "1.84.0"
|
||||
|
||||
jobs:
|
||||
ubuntu-job:
|
||||
|
||||
@@ -11,7 +11,7 @@ on:
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
FLUTTER_VERSION: "3.27.4"
|
||||
RUST_TOOLCHAIN: "1.85.0"
|
||||
RUST_TOOLCHAIN: "1.84.0"
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
|
||||
@@ -1,38 +1,4 @@
|
||||
# Release Notes
|
||||
## Version 0.9.3 - 28/05/2025
|
||||
### Desktop
|
||||
#### New Features
|
||||
- Meet AppFlowy Workspace AI Search: Quickly find pages by searching titles, keywords, or asking natural-language questions
|
||||
- AI Overviews: Ask natural questions and receive instant AI-generated summaries with source links, inspired by Google's AI Overviews
|
||||
- Revamped Search Panel: A cleaner, smarter interface to help you search faster and more effectively
|
||||
- Custom Prompts: Load a database page as the source for your own custom AI prompts
|
||||
#### Bug Fixes
|
||||
- Fixed misalignment in database view after setting maxDocumentWidth
|
||||
- Centered embedded link when the site name is empty
|
||||
- Fixed issue where row observer was not clearing as expected
|
||||
- Fixed issue where workspace name reverted after being updated
|
||||
- Aligned checkbox icon with the first line of text
|
||||
### Mobile
|
||||
#### New Features
|
||||
- Meet AppFlowy Workspace AI Search: Quickly find pages by searching titles, keywords, or asking natural-language questions
|
||||
- AI Overviews: Ask natural questions and receive instant AI-generated summaries with source links, inspired by Google's AI Overviews
|
||||
- Revamped Search Tab: A redesigned interface that helps you find what you need more efficiently
|
||||
#### Bug Fixes
|
||||
- Fixed issue where font size reset after restarting the app
|
||||
|
||||
## Version 0.9.2 - 14/05/2025
|
||||
### Desktop
|
||||
#### New Features
|
||||
- Supported AI Overview in Search to answer user queries based on their entire workspace
|
||||
- Revamped the Search panel in the desktop app
|
||||
- Enabled loading custom prompts from an AppFlowy database page
|
||||
#### Bug Fixes
|
||||
- Improved inserting emojis using the colon (:)
|
||||
- Supported automatically filling the link name with the URL if the name is left empty
|
||||
### Mobile
|
||||
#### Bug Fixes
|
||||
- Supported automatically filling the link name with the URL if the name is left empty
|
||||
|
||||
## Version 0.9.1 - 01/05/2025
|
||||
### Desktop
|
||||
#### New Features
|
||||
|
||||
@@ -26,7 +26,7 @@ CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true
|
||||
CARGO_MAKE_CRATE_FS_NAME = "dart_ffi"
|
||||
CARGO_MAKE_CRATE_NAME = "dart-ffi"
|
||||
LIB_NAME = "dart_ffi"
|
||||
APPFLOWY_VERSION = "0.9.3"
|
||||
APPFLOWY_VERSION = "0.9.1"
|
||||
FLUTTER_DESKTOP_FEATURES = "dart"
|
||||
PRODUCT_NAME = "AppFlowy"
|
||||
MACOSX_DEPLOYMENT_TARGET = "11.0"
|
||||
@@ -190,9 +190,6 @@ FLUTTER_OUTPUT_DIR = "Debug"
|
||||
RUST_COMPILE_TARGET = "aarch64-apple-ios"
|
||||
BUILD_ARCHS = "arm64"
|
||||
CRATE_TYPE = "staticlib"
|
||||
IPHONEOS_DEPLOYMENT_TARGET = "10.0"
|
||||
CFLAGS = "-miphoneos-version-min=10.0"
|
||||
LDFLAGS = "-miphoneos-version-min=10.0"
|
||||
|
||||
[env.production-ios-arm64]
|
||||
BUILD_FLAG = "release"
|
||||
@@ -201,9 +198,6 @@ FLUTTER_OUTPUT_DIR = "Release"
|
||||
RUST_COMPILE_TARGET = "aarch64-apple-ios"
|
||||
BUILD_ARCHS = "arm64"
|
||||
CRATE_TYPE = "staticlib"
|
||||
IPHONEOS_DEPLOYMENT_TARGET = "10.0"
|
||||
CFLAGS = "-miphoneos-version-min=10.0"
|
||||
LDFLAGS = "-miphoneos-version-min=10.0"
|
||||
|
||||
[env.development-android]
|
||||
BUILD_FLAG = "debug"
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"{""data"":""Juan"",""last_modified"":1726063423,""created_at"":1726063423,""field_type"":0}","{""created_at"":1726122510,""reminder_id"":"""",""include_time"":false,""is_range"":true,""last_modified"":1726122515,""data"":""1725604115"",""end_timestamp"":""1725776915"",""field_type"":2}","{""field_type"":1,""created_at"":1726063677,""last_modified"":1726063677,""data"":""93100""}","{""field_type"":1,""data"":""4.86"",""created_at"":1726062597,""last_modified"":1726062597}",,"{""last_modified"":1726062377,""field_type"":5,""data"":""Yes"",""created_at"":1726062377}","{""last_modified"":1726064412,""field_type"":7,""data"":""{\""options\"":[{\""id\"":\""tTDq\"",\""name\"":\""complete onboarding\"",\""color\"":\""Purple\""},{\""id\"":\""E8Ds\"",\""name\"":\""contact support\"",\""color\"":\""Purple\""},{\""id\"":\""RoGN\"",\""name\"":\""get started\"",\""color\"":\""Purple\""}],\""selected_option_ids\"":[\""tTDq\"",\""E8Ds\""]}"",""created_at"":1726064396}",,"{""created_at"":1726065278,""field_type"":3,""data"":""qnja"",""last_modified"":1726065278}","{""data"":""R9I7,yORP,1i4f"",""field_type"":4,""created_at"":1726105126,""last_modified"":1726105127}","{""data"":""1726122515"",""field_type"":8}","{""data"":""1726060541"",""field_type"":9}"
|
||||
"{""data"":""Alex"",""created_at"":1726063432,""last_modified"":1726063432,""field_type"":0}","{""reminder_id"":"""",""data"":""1725292800"",""include_time"":true,""last_modified"":1726122448,""created_at"":1726122422,""is_range"":true,""end_timestamp"":""1725551940"",""field_type"":2}","{""field_type"":1,""last_modified"":1726063683,""created_at"":1726063683,""data"":""3560""}","{""created_at"":1726062561,""data"":""1.96"",""last_modified"":1726062561,""field_type"":1}","{""last_modified"":1726063952,""created_at"":1726063931,""data"":""al3x1343@protonmail.com"",""field_type"":6}","{""last_modified"":1726062375,""field_type"":5,""created_at"":1726062375,""data"":""Yes""}","{""data"":""{\""options\"":[{\""id\"":\""qNyr\"",\""name\"":\""finish reading book\"",\""color\"":\""Purple\""}],\""selected_option_ids\"":[]}"",""created_at"":1726064616,""last_modified"":1726064616,""field_type"":7}",,"{""data"":""qnja"",""created_at"":1726065272,""last_modified"":1726065272,""field_type"":3}","{""created_at"":1726105180,""last_modified"":1726105180,""field_type"":4,""data"":""R9I7,1i4f""}","{""field_type"":8,""data"":""1726122448""}","{""field_type"":9,""data"":""1726060541""}"
|
||||
"{""last_modified"":1726063478,""created_at"":1726063436,""field_type"":0,""data"":""Alexander""}",,"{""field_type"":1,""last_modified"":1726063691,""created_at"":1726063691,""data"":""2073""}","{""field_type"":1,""data"":""0.5"",""last_modified"":1726062577,""created_at"":1726062577}","{""last_modified"":1726063991,""field_type"":6,""created_at"":1726063991,""data"":""alexandernotthedra@gmail.com""}","{""field_type"":5,""last_modified"":1726062378,""created_at"":1726062377,""data"":""No""}",,,"{""created_at"":1726065291,""data"":""GSf_"",""last_modified"":1726065291,""field_type"":3}","{""last_modified"":1726105142,""created_at"":1726105133,""data"":""SEUo"",""field_type"":4}","{""field_type"":8,""data"":""1726105142""}","{""field_type"":9,""data"":""1726060542""}"
|
||||
"{""field_type"":0,""created_at"":1726063454,""last_modified"":1726063454,""data"":""George""}","{""created_at"":1726122467,""end_timestamp"":""1726468070"",""include_time"":false,""is_range"":true,""reminder_id"":"""",""field_type"":2,""data"":""1726295270"",""last_modified"":1726122470}",,,"{""field_type"":6,""data"":""george.aq@appflowy.io"",""last_modified"":1726064104,""created_at"":1726064016}","{""last_modified"":1726062376,""created_at"":1726062376,""field_type"":5,""data"":""Yes""}","{""data"":""{\""options\"":[{\""id\"":\""s_dQ\"",\""name\"":\""bug triage\"",\""color\"":\""Purple\""},{\""id\"":\""-Zfo\"",\""name\"":\""fix bugs\"",\""color\"":\""Purple\""},{\""id\"":\""wsDN\"",\""name\"":\""attend meetings\"",\""color\"":\""Purple\""}],\""selected_option_ids\"":[\""s_dQ\"",\""-Zfo\""]}"",""last_modified"":1726064468,""created_at"":1726064424,""field_type"":7}","{""data"":[""{\""id\"":\""8a77f84d-64e9-4e67-b902-fa23980459ec\"",\""name\"":\""BQdTmxpRI6f.png\"",\""url\"":\""https://samplelib.com/lib/preview/png/sample-blue-200x200.png\"",\""upload_type\"":\""NetworkMedia\"",\""file_type\"":\""Image\""}""],""field_type"":14,""created_at"":1726122956,""last_modified"":1726122956}","{""field_type"":3,""data"":""qnja"",""created_at"":1726065313,""last_modified"":1726065313}","{""data"":""R9I7,yORP"",""field_type"":4,""last_modified"":1726105198,""created_at"":1726105187}","{""data"":""1726122956"",""field_type"":8}","{""data"":""1726060543"",""field_type"":9}"
|
||||
"{""field_type"":0,""created_at"":1726063454,""last_modified"":1726063454,""data"":""George""}","{""created_at"":1726122467,""end_timestamp"":""1726468070"",""include_time"":false,""is_range"":true,""reminder_id"":"""",""field_type"":2,""data"":""1726295270"",""last_modified"":1726122470}",,,"{""field_type"":6,""data"":""george.aq@appflowy.io"",""last_modified"":1726064104,""created_at"":1726064016}","{""last_modified"":1726062376,""created_at"":1726062376,""field_type"":5,""data"":""Yes""}","{""data"":""{\""options\"":[{\""id\"":\""s_dQ\"",\""name\"":\""bug triage\"",\""color\"":\""Purple\""},{\""id\"":\""-Zfo\"",\""name\"":\""fix bugs\"",\""color\"":\""Purple\""},{\""id\"":\""wsDN\"",\""name\"":\""attend meetings\"",\""color\"":\""Purple\""}],\""selected_option_ids\"":[\""s_dQ\"",\""-Zfo\""]}"",""last_modified"":1726064468,""created_at"":1726064424,""field_type"":7}","{""data"":[""{\""id\"":\""8a77f84d-64e9-4e67-b902-fa23980459ec\"",\""name\"":\""BQdTmxpRI6f.png\"",\""url\"":\""https://file-examples.com/wp-content/storage/2017/10/file_example_PNG_500kB.png\"",\""upload_type\"":\""NetworkMedia\"",\""file_type\"":\""Image\""}""],""field_type"":14,""created_at"":1726122956,""last_modified"":1726122956}","{""field_type"":3,""data"":""qnja"",""created_at"":1726065313,""last_modified"":1726065313}","{""data"":""R9I7,yORP"",""field_type"":4,""last_modified"":1726105198,""created_at"":1726105187}","{""data"":""1726122956"",""field_type"":8}","{""data"":""1726060543"",""field_type"":9}"
|
||||
"{""field_type"":0,""last_modified"":1726063467,""data"":""Joanna"",""created_at"":1726063467}","{""include_time"":false,""end_timestamp"":""1727072893"",""is_range"":true,""last_modified"":1726122493,""created_at"":1726122483,""data"":""1726554493"",""field_type"":2,""reminder_id"":""""}","{""last_modified"":1726065463,""data"":""16470"",""field_type"":1,""created_at"":1726065463}","{""created_at"":1726062626,""field_type"":1,""last_modified"":1726062626,""data"":""-5.36""}","{""last_modified"":1726064069,""data"":""joannastrawberry29+hello@gmail.com"",""created_at"":1726064069,""field_type"":6}",,"{""field_type"":7,""created_at"":1726064444,""last_modified"":1726064460,""data"":""{\""options\"":[{\""id\"":\""ZxJz\"",\""name\"":\""post on Twitter\"",\""color\"":\""Purple\""},{\""id\"":\""upwi\"",\""name\"":\""watch Youtube videos\"",\""color\"":\""Purple\""}],\""selected_option_ids\"":[\""upwi\""]}""}",,"{""created_at"":1726065317,""last_modified"":1726065317,""field_type"":3,""data"":""qnja""}","{""field_type"":4,""last_modified"":1726105173,""data"":""uRAO,yORP"",""created_at"":1726105170}","{""data"":""1726122493"",""field_type"":8}","{""data"":""1726060545"",""field_type"":9}"
|
||||
"{""last_modified"":1726063457,""created_at"":1726063457,""data"":""George"",""field_type"":0}","{""include_time"":true,""reminder_id"":"""",""field_type"":2,""is_range"":true,""created_at"":1726122521,""end_timestamp"":""1725829200"",""data"":""1725822900"",""last_modified"":1726122535}","{""last_modified"":1726065493,""field_type"":1,""data"":""9500"",""created_at"":1726065493}","{""last_modified"":1726062680,""created_at"":1726062680,""field_type"":1,""data"":""1.7""}","{""data"":""plgeorgebball@gmail.com"",""field_type"":6,""last_modified"":1726064087,""created_at"":1726064036}",,"{""last_modified"":1726064513,""data"":""{\""options\"":[{\""id\"":\""zy0x\"",\""name\"":\""game vs celtics\"",\""color\"":\""Purple\""},{\""id\"":\""WJsv\"",\""name\"":\""training\"",\""color\"":\""Purple\""},{\""id\"":\""w-f8\"",\""name\"":\""game vs spurs\"",\""color\"":\""Purple\""},{\""id\"":\""p1VQ\"",\""name\"":\""game vs knicks\"",\""color\"":\""Purple\""},{\""id\"":\""VjUA\"",\""name\"":\""recovery\"",\""color\"":\""Purple\""},{\""id\"":\""sQ8X\"",\""name\"":\""don't get injured\"",\""color\"":\""Purple\""}],\""selected_option_ids\"":[]}"",""created_at"":1726064486,""field_type"":7}",,"{""field_type"":3,""last_modified"":1726065310,""data"":""qnja"",""created_at"":1726065310}","{""created_at"":1726105205,""field_type"":4,""last_modified"":1726105249,""data"":""R9I7,1i4f,yORP,SEUo""}","{""data"":""1726122535"",""field_type"":8}","{""field_type"":9,""data"":""1726060546""}"
|
||||
"{""data"":""Judy"",""created_at"":1726063475,""field_type"":0,""last_modified"":1726063487}","{""end_timestamp"":"""",""reminder_id"":"""",""data"":""1726640950"",""field_type"":2,""include_time"":false,""created_at"":1726122550,""last_modified"":1726122550,""is_range"":false}",,,"{""created_at"":1726063882,""field_type"":6,""last_modified"":1726064000,""data"":""judysmithjr@outlook.com""}","{""last_modified"":1726062712,""field_type"":5,""data"":""Yes"",""created_at"":1726062712}","{""created_at"":1726064549,""field_type"":7,""data"":""{\""options\"":[{\""id\"":\""j8cC\"",\""name\"":\""finish training\"",\""color\"":\""Purple\""},{\""id\"":\""SmSk\"",\""name\"":\""brainwash\"",\""color\"":\""Purple\""},{\""id\"":\""mnf5\"",\""name\"":\""welcome to ba sing se\"",\""color\"":\""Purple\""},{\""id\"":\""hcrj\"",\""name\"":\""don't mess up\"",\""color\"":\""Purple\""}],\""selected_option_ids\"":[\""j8cC\"",\""SmSk\"",\""mnf5\"",\""hcrj\""]}"",""last_modified"":1726064591}",,,"{""field_type"":4,""last_modified"":1726105152,""created_at"":1726105152,""data"":""R9I7""}","{""field_type"":8,""data"":""1726122550""}","{""field_type"":9,""data"":""1726060549""}"
|
||||
@@ -1,264 +0,0 @@
|
||||
import 'package:appflowy/plugins/ai_chat/presentation/chat_page/chat_animation_list_widget.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:flutter_chat_core/flutter_chat_core.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import '../../shared/ai_test_op.dart';
|
||||
import '../../shared/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('chat page:', () {
|
||||
testWidgets('send messages with default messages', (tester) async {
|
||||
skipAIChatWelcomePage = true;
|
||||
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
// create a chat page
|
||||
final pageName = 'Untitled';
|
||||
await tester.createNewPageWithNameUnderParent(
|
||||
name: pageName,
|
||||
layout: ViewLayoutPB.Chat,
|
||||
openAfterCreated: false,
|
||||
);
|
||||
|
||||
await tester.pumpAndSettle(const Duration(milliseconds: 300));
|
||||
|
||||
final userId = '457037009907617792';
|
||||
final user = User(id: userId, lastName: 'Lucas');
|
||||
final aiUserId = '0';
|
||||
final aiUser = User(id: aiUserId, lastName: 'AI');
|
||||
|
||||
await tester.loadDefaultMessages(
|
||||
[
|
||||
Message.text(
|
||||
id: '1746776401',
|
||||
text: 'How to use Kanban to manage tasks?',
|
||||
author: user,
|
||||
createdAt: DateTime.now().add(const Duration(seconds: 1)),
|
||||
),
|
||||
Message.text(
|
||||
id: '1746776401_ans',
|
||||
text:
|
||||
'I couldn’t find any relevant information in the sources you selected. Please try asking a different question',
|
||||
author: aiUser,
|
||||
createdAt: DateTime.now().add(const Duration(seconds: 2)),
|
||||
),
|
||||
Message.text(
|
||||
id: '1746776402',
|
||||
text: 'How to use Kanban to manage tasks?',
|
||||
author: user,
|
||||
createdAt: DateTime.now().add(const Duration(seconds: 3)),
|
||||
),
|
||||
Message.text(
|
||||
id: '1746776402_ans',
|
||||
text:
|
||||
'I couldn’t find any relevant information in the sources you selected. Please try asking a different question',
|
||||
author: aiUser,
|
||||
createdAt: DateTime.now().add(const Duration(seconds: 4)),
|
||||
),
|
||||
].reversed.toList(),
|
||||
);
|
||||
await tester.pumpAndSettle(Duration(seconds: 1));
|
||||
|
||||
// start chat
|
||||
final int messageId = 1;
|
||||
|
||||
// send a message
|
||||
await tester.sendUserMessage(
|
||||
Message.text(
|
||||
id: messageId.toString(),
|
||||
text: 'How to use AppFlowy?',
|
||||
author: user,
|
||||
createdAt: DateTime.now(),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle(Duration(seconds: 1));
|
||||
|
||||
// receive a message
|
||||
await tester.receiveAIMessage(
|
||||
Message.text(
|
||||
id: '${messageId}_ans',
|
||||
text: '''# How to Use AppFlowy
|
||||
- Download and install AppFlowy from the official website (appflowy.io) or through app stores for your operating system (Windows, macOS, Linux, or mobile)
|
||||
- Create an account or sign in when you first launch the app
|
||||
- The main interface shows your workspace with a sidebar for navigation and a content area''',
|
||||
author: aiUser,
|
||||
createdAt: DateTime.now(),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle(Duration(seconds: 1));
|
||||
|
||||
final chatBloc = tester.getCurrentChatBloc();
|
||||
expect(chatBloc.chatController.messages.length, equals(6));
|
||||
});
|
||||
|
||||
testWidgets('send messages without default messages', (tester) async {
|
||||
skipAIChatWelcomePage = true;
|
||||
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
// create a chat page
|
||||
final pageName = 'Untitled';
|
||||
await tester.createNewPageWithNameUnderParent(
|
||||
name: pageName,
|
||||
layout: ViewLayoutPB.Chat,
|
||||
openAfterCreated: false,
|
||||
);
|
||||
|
||||
await tester.pumpAndSettle(const Duration(milliseconds: 300));
|
||||
|
||||
final userId = '457037009907617792';
|
||||
final user = User(id: userId, lastName: 'Lucas');
|
||||
final aiUserId = '0';
|
||||
final aiUser = User(id: aiUserId, lastName: 'AI');
|
||||
|
||||
// start chat
|
||||
int messageId = 1;
|
||||
|
||||
// round 1
|
||||
{
|
||||
// send a message
|
||||
await tester.sendUserMessage(
|
||||
Message.text(
|
||||
id: messageId.toString(),
|
||||
text: 'How to use AppFlowy?',
|
||||
author: user,
|
||||
createdAt: DateTime.now(),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle(Duration(seconds: 1));
|
||||
|
||||
// receive a message
|
||||
await tester.receiveAIMessage(
|
||||
Message.text(
|
||||
id: '${messageId}_ans',
|
||||
text: '''# How to Use AppFlowy
|
||||
- Download and install AppFlowy from the official website (appflowy.io) or through app stores for your operating system (Windows, macOS, Linux, or mobile)
|
||||
- Create an account or sign in when you first launch the app
|
||||
- The main interface shows your workspace with a sidebar for navigation and a content area''',
|
||||
author: aiUser,
|
||||
createdAt: DateTime.now(),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle(Duration(seconds: 1));
|
||||
messageId++;
|
||||
}
|
||||
|
||||
// round 2
|
||||
{
|
||||
// send a message
|
||||
await tester.sendUserMessage(
|
||||
Message.text(
|
||||
id: messageId.toString(),
|
||||
text: 'How to use AppFlowy?',
|
||||
author: user,
|
||||
createdAt: DateTime.now(),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle(Duration(seconds: 1));
|
||||
|
||||
// receive a message
|
||||
await tester.receiveAIMessage(
|
||||
Message.text(
|
||||
id: '${messageId}_ans',
|
||||
text:
|
||||
'I couldn’t find any relevant information in the sources you selected. Please try asking a different question',
|
||||
author: aiUser,
|
||||
createdAt: DateTime.now(),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle(Duration(seconds: 1));
|
||||
messageId++;
|
||||
}
|
||||
|
||||
// round 3
|
||||
{
|
||||
// send a message
|
||||
await tester.sendUserMessage(
|
||||
Message.text(
|
||||
id: messageId.toString(),
|
||||
text: 'What document formatting options are available?',
|
||||
author: user,
|
||||
createdAt: DateTime.now(),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle(Duration(seconds: 1));
|
||||
|
||||
// receive a message
|
||||
await tester.receiveAIMessage(
|
||||
Message.text(
|
||||
id: '${messageId}_ans',
|
||||
text:
|
||||
'# AppFlowy Document Formatting\n- Basic formatting: Bold, italic, underline, strikethrough\n- Headings: 6 levels of headings for structuring content\n- Lists: Bullet points, numbered lists, and checklists\n- Code blocks: Format text as code with syntax highlighting\n- Tables: Create and format data tables\n- Embedded content: Add images, files, and other rich media',
|
||||
author: aiUser,
|
||||
createdAt: DateTime.now(),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle(Duration(seconds: 1));
|
||||
messageId++;
|
||||
}
|
||||
|
||||
// round 4
|
||||
{
|
||||
// send a message
|
||||
await tester.sendUserMessage(
|
||||
Message.text(
|
||||
id: messageId.toString(),
|
||||
text: 'How do I export my data from AppFlowy?',
|
||||
author: user,
|
||||
createdAt: DateTime.now(),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle(Duration(seconds: 1));
|
||||
|
||||
// receive a message
|
||||
await tester.receiveAIMessage(
|
||||
Message.text(
|
||||
id: '${messageId}_ans',
|
||||
text:
|
||||
'# Exporting from AppFlowy\n- Export documents in multiple formats: Markdown, HTML, PDF\n- Export databases as CSV or Excel files\n- Batch export entire workspaces for backup\n- Use the export menu (three dots → Export) on any page\n- Exported files maintain most formatting and structure',
|
||||
author: aiUser,
|
||||
createdAt: DateTime.now(),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle(Duration(seconds: 1));
|
||||
messageId++;
|
||||
}
|
||||
|
||||
// round 5
|
||||
{
|
||||
// send a message
|
||||
await tester.sendUserMessage(
|
||||
Message.text(
|
||||
id: messageId.toString(),
|
||||
text: 'Is there a mobile version of AppFlowy?',
|
||||
author: user,
|
||||
createdAt: DateTime.now(),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle(Duration(seconds: 1));
|
||||
|
||||
// receive a message
|
||||
await tester.receiveAIMessage(
|
||||
Message.text(
|
||||
id: '${messageId}_ans',
|
||||
text:
|
||||
'# AppFlowy on Mobile\n- Yes, AppFlowy is available for iOS and Android devices\n- Download from the App Store or Google Play Store\n- Mobile app includes core functionality: document editing, databases, and boards\n- Offline mode allows working without internet connection\n- Sync automatically when you reconnect\n- Responsive design adapts to different screen sizes',
|
||||
author: aiUser,
|
||||
createdAt: DateTime.now(),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle(Duration(seconds: 1));
|
||||
messageId++;
|
||||
}
|
||||
|
||||
final chatBloc = tester.getCurrentChatBloc();
|
||||
expect(chatBloc.chatController.messages.length, equals(10));
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import 'database/database_test_runner.dart' as database_test_runner;
|
||||
import 'document/document_test_runner.dart' as document_test_runner;
|
||||
import 'set_env.dart' as preset_af_cloud_env_test;
|
||||
import 'sidebar/sidebar_icon_test.dart' as sidebar_icon_test;
|
||||
import 'sidebar/sidebar_search_test.dart' as sidebar_search_test;
|
||||
import 'sidebar/sidebar_move_page_test.dart' as sidebar_move_page_test;
|
||||
import 'sidebar/sidebar_rename_untitled_test.dart'
|
||||
as sidebar_rename_untitled_test;
|
||||
@@ -32,7 +31,6 @@ Future<void> main() async {
|
||||
sidebar_move_page_test.main();
|
||||
sidebar_rename_untitled_test.main();
|
||||
sidebar_icon_test.main();
|
||||
sidebar_search_test.main();
|
||||
}
|
||||
|
||||
// database
|
||||
|
||||
+6
-6
@@ -1,10 +1,10 @@
|
||||
import 'package:appflowy/env/cloud_env.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialog_v2.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pbenum.dart';
|
||||
import 'package:appflowy_ui/appflowy_ui.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text_input.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
@@ -38,12 +38,12 @@ void main() {
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(AFTextFieldDialog), findsOneWidget);
|
||||
expect(find.byType(NavigatorTextFieldDialog), findsOneWidget);
|
||||
|
||||
final textField = tester.widget<AFTextField>(
|
||||
final textField = tester.widget<FlowyFormTextInput>(
|
||||
find.descendant(
|
||||
of: find.byType(AFTextFieldDialog),
|
||||
matching: find.byType(AFTextField),
|
||||
of: find.byType(NavigatorTextFieldDialog),
|
||||
matching: find.byType(FlowyFormTextInput),
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
-42
@@ -1,42 +0,0 @@
|
||||
import 'package:appflowy/ai/widgets/prompt_input/desktop_prompt_input.dart';
|
||||
import 'package:appflowy/env/cloud_env.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/workspace/presentation/command_palette/widgets/search_ask_ai_entrance.dart';
|
||||
import 'package:appflowy/workspace/presentation/command_palette/widgets/search_field.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import '../../../shared/util.dart';
|
||||
|
||||
void main() {
|
||||
setUpAll(() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
});
|
||||
|
||||
testWidgets('Test for searching', (tester) async {
|
||||
await tester.initializeAppFlowy(
|
||||
cloudType: AuthenticatorType.appflowyCloudSelfHost,
|
||||
);
|
||||
await tester.tapGoogleLoginInButton();
|
||||
await tester.expectToSeeHomePageWithGetStartedPage();
|
||||
|
||||
/// show searching page
|
||||
final searchingButton = find.text(LocaleKeys.search_label.tr());
|
||||
await tester.tapButton(searchingButton);
|
||||
final askAIButton = find.byType(SearchAskAiEntrance);
|
||||
expect(askAIButton, findsOneWidget);
|
||||
|
||||
/// searching for [gettingStarted]
|
||||
final searchField = find.byType(SearchField);
|
||||
final textFiled =
|
||||
find.descendant(of: searchField, matching: find.byType(TextField));
|
||||
await tester.enterText(textFiled, gettingStarted);
|
||||
await tester.pumpAndSettle(Duration(seconds: 1));
|
||||
|
||||
/// tap ask AI button
|
||||
await tester.tapButton(askAIButton);
|
||||
expect(find.byType(DesktopPromptInput), findsOneWidget);
|
||||
});
|
||||
}
|
||||
+5
-5
@@ -34,7 +34,7 @@ void main() {
|
||||
var workspaceIcon = tester.widget<WorkspaceIcon>(
|
||||
find.byType(WorkspaceIcon),
|
||||
);
|
||||
expect(workspaceIcon.workspaceIcon, '');
|
||||
expect(workspaceIcon.workspace.icon, '');
|
||||
|
||||
await tester.openWorkspaceMenu();
|
||||
await tester.changeWorkspaceIcon(icon);
|
||||
@@ -47,8 +47,8 @@ void main() {
|
||||
workspaceIcon = tester.widget<WorkspaceIcon>(
|
||||
find.byType(WorkspaceIcon),
|
||||
);
|
||||
expect(workspaceIcon.workspaceIcon, icon);
|
||||
expect(workspaceIcon.workspaceName, name);
|
||||
expect(workspaceIcon.workspace.icon, icon);
|
||||
expect(find.findTextInFlowyText(name), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('verify the result again after relaunching', (tester) async {
|
||||
@@ -69,7 +69,7 @@ void main() {
|
||||
final workspaceIcon = tester.widget<WorkspaceIcon>(
|
||||
find.byType(WorkspaceIcon),
|
||||
);
|
||||
expect(workspaceIcon.workspaceIcon, icon);
|
||||
expect(workspaceIcon.workspaceName, name);
|
||||
expect(workspaceIcon.workspace.icon, icon);
|
||||
expect(workspaceIcon.workspace.name, name);
|
||||
});
|
||||
}
|
||||
+12
-7
@@ -36,20 +36,23 @@ void main() {
|
||||
final loading = find.byType(Loading);
|
||||
await tester.pumpUntilNotFound(loading);
|
||||
|
||||
Finder success;
|
||||
|
||||
final Finder items = find.byType(WorkspaceMenuItem);
|
||||
|
||||
// delete the newly created workspace
|
||||
await tester.openCollaborativeWorkspaceMenu();
|
||||
await tester.pumpUntilFound(items);
|
||||
|
||||
final items = find.byType(WorkspaceMenuItem);
|
||||
expect(items, findsNWidgets(2));
|
||||
|
||||
final lastWorkspace = items.last;
|
||||
expect(
|
||||
tester.widget<WorkspaceMenuItem>(lastWorkspace).workspace.name,
|
||||
tester.widget<WorkspaceMenuItem>(items.last).workspace.name,
|
||||
name,
|
||||
);
|
||||
|
||||
final secondWorkspace = find.byType(WorkspaceMenuItem).last;
|
||||
await tester.hoverOnWidget(
|
||||
lastWorkspace,
|
||||
secondWorkspace,
|
||||
onHover: () async {
|
||||
// click the more button
|
||||
final moreButton = find.byType(WorkspaceMoreActionList);
|
||||
@@ -65,8 +68,10 @@ void main() {
|
||||
expect(confirm, findsOneWidget);
|
||||
await tester.tapButton(find.text(LocaleKeys.button_ok.tr()));
|
||||
// delete success
|
||||
final success = find.text(LocaleKeys.workspace_createSuccess.tr());
|
||||
success = find.text(LocaleKeys.workspace_createSuccess.tr());
|
||||
await tester.pumpUntilFound(success);
|
||||
expect(success, findsOneWidget);
|
||||
await tester.pumpUntilNotFound(success);
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -95,7 +100,7 @@ void main() {
|
||||
|
||||
// expect to see the member count
|
||||
final memberCount = find.text('1 member');
|
||||
expect(memberCount, findsAny);
|
||||
expect(memberCount, findsNWidgets(2));
|
||||
});
|
||||
|
||||
testWidgets('workspace menu popover behavior test', (tester) async {
|
||||
|
||||
+2
-3
@@ -5,13 +5,12 @@ import 'collaborative_workspace_test.dart' as collaborative_workspace_test;
|
||||
import 'share_menu_test.dart' as share_menu_test;
|
||||
import 'tabs_test.dart' as tabs_test;
|
||||
import 'workspace_icon_test.dart' as workspace_icon_test;
|
||||
// import 'workspace_settings_test.dart' as workspace_settings_test;
|
||||
import 'workspace_settings_test.dart' as workspace_settings_test;
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
// Enable it after implementing the share feature
|
||||
// workspace_settings_test.main();
|
||||
workspace_settings_test.main();
|
||||
share_menu_test.main();
|
||||
collaborative_workspace_test.main();
|
||||
change_name_and_icon_test.main();
|
||||
|
||||
+1
-77
@@ -1,17 +1,7 @@
|
||||
import 'package:appflowy/workspace/presentation/command_palette/command_palette.dart';
|
||||
import 'package:appflowy/workspace/presentation/command_palette/widgets/search_ask_ai_entrance.dart';
|
||||
import 'package:appflowy/workspace/presentation/command_palette/widgets/search_result_cell.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/workspace/presentation/command_palette/widgets/page_preview.dart';
|
||||
import 'package:appflowy/workspace/presentation/command_palette/widgets/recent_views_list.dart';
|
||||
import 'package:appflowy/workspace/presentation/command_palette/widgets/search_field.dart';
|
||||
import 'package:appflowy/workspace/presentation/command_palette/widgets/search_results_list.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../shared/util.dart';
|
||||
|
||||
void main() {
|
||||
@@ -29,70 +19,4 @@ void main() {
|
||||
expect(find.byType(CommandPaletteModal), findsNothing);
|
||||
});
|
||||
});
|
||||
|
||||
group('Search', () {
|
||||
testWidgets('Test for searching', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
await tester.createNewPageWithNameUnderParent(name: 'Switch To New Page');
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
/// tap getting started page
|
||||
await tester.tapButton(
|
||||
find.descendant(
|
||||
of: find.byType(HomeSideBar),
|
||||
matching: find.textContaining(gettingStarted),
|
||||
),
|
||||
);
|
||||
|
||||
/// show searching page
|
||||
final searchingButton = find.text(LocaleKeys.search_label.tr());
|
||||
await tester.tapButton(searchingButton);
|
||||
final askAIButton = find.byType(SearchAskAiEntrance);
|
||||
expect(askAIButton, findsNothing);
|
||||
final recentList = find.byType(RecentViewsList);
|
||||
expect(recentList, findsOneWidget);
|
||||
|
||||
/// there is [gettingStarted] in recent list
|
||||
final gettingStartedRecentCell = find.descendant(
|
||||
of: recentList,
|
||||
matching: find.textContaining(gettingStarted),
|
||||
);
|
||||
expect(gettingStartedRecentCell, findsAtLeast(1));
|
||||
|
||||
/// hover to show preview
|
||||
await tester.hoverOnWidget(gettingStartedRecentCell.first);
|
||||
final pagePreview = find.byType(PagePreview);
|
||||
expect(pagePreview, findsOneWidget);
|
||||
final gettingStartedPreviewTitle = find.descendant(
|
||||
of: pagePreview,
|
||||
matching: find.textContaining(gettingStarted),
|
||||
);
|
||||
expect(gettingStartedPreviewTitle, findsOneWidget);
|
||||
|
||||
/// searching for [gettingStarted]
|
||||
final searchField = find.byType(SearchField);
|
||||
final textFiled =
|
||||
find.descendant(of: searchField, matching: find.byType(TextField));
|
||||
await tester.enterText(textFiled, gettingStarted);
|
||||
await tester.pumpAndSettle(Duration(seconds: 1));
|
||||
|
||||
/// there is [gettingStarted] in result list
|
||||
final resultList = find.byType(SearchResultList);
|
||||
expect(resultList, findsOneWidget);
|
||||
final resultCells = find.byType(SearchResultCell);
|
||||
expect(resultCells, findsAtLeast(1));
|
||||
|
||||
/// hover to show preview
|
||||
await tester.hoverOnWidget(resultCells.first);
|
||||
expect(find.byType(PagePreview), findsOneWidget);
|
||||
|
||||
/// clear search content
|
||||
final clearButton = find.byFlowySvg(FlowySvgs.search_clear_m);
|
||||
await tester.tapButton(clearButton);
|
||||
expect(find.byType(SearchResultList), findsNothing);
|
||||
expect(find.byType(RecentViewsList), findsOneWidget);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -168,38 +168,6 @@ void main() {
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
testWidgets('clear cells under field', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);
|
||||
|
||||
await tester.scrollToRight(find.byType(GridPage));
|
||||
|
||||
// edit the data
|
||||
await tester.editCell(
|
||||
rowIndex: 0,
|
||||
fieldType: FieldType.RichText,
|
||||
input: 'Hello',
|
||||
);
|
||||
await tester.editCell(
|
||||
rowIndex: 1,
|
||||
fieldType: FieldType.RichText,
|
||||
input: 'World',
|
||||
);
|
||||
|
||||
expect(find.text('Hello'), findsOneWidget);
|
||||
expect(find.text('World'), findsOneWidget);
|
||||
|
||||
// clear the cells
|
||||
await tester.tapGridFieldWithName('Name');
|
||||
await tester.tapClearCellsButton();
|
||||
await tester.tapButtonWithName(LocaleKeys.button_confirm.tr());
|
||||
|
||||
expect(find.text('Hello'), findsNothing);
|
||||
expect(find.text('World'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('create list of fields', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ import '../../shared/util.dart';
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('create and delete the document:', () {
|
||||
group('create and delete the document', () {
|
||||
testWidgets('create a new document when launching app in first time',
|
||||
(tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
|
||||
+2
-10
@@ -53,7 +53,7 @@ void main() {
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Select result
|
||||
await tapMenuItemWithName(name, tester);
|
||||
await tester.editor.tapAtMenuItemWithName(name);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final mentionBlock = find.byType(MentionPageBlock);
|
||||
@@ -73,7 +73,7 @@ void main() {
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Select result
|
||||
await tapMenuItemWithName(name, tester);
|
||||
await tester.editor.tapAtMenuItemWithName(name);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final mentionBlock = find.byType(MentionPageBlock);
|
||||
@@ -134,11 +134,3 @@ Future<void> enterDocumentText(WidgetTester tester) async {
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
Future<void> tapMenuItemWithName(String name, WidgetTester tester) async {
|
||||
final menuItem = find.descendant(
|
||||
of: find.byType(InlineActionsHandler),
|
||||
matching: find.text(name, findRichText: true),
|
||||
);
|
||||
await tester.tapButton(menuItem);
|
||||
}
|
||||
+2
-3
@@ -7,7 +7,6 @@ import 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/view/view_action_type.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_ui/appflowy_ui.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
@@ -522,8 +521,8 @@ extension _SubPageTestHelper on WidgetTester {
|
||||
await hoverOnPageName(currentName, onHover: () async => pumpAndSettle());
|
||||
await rightClickOnPageName(currentName);
|
||||
await tapButtonWithName(ViewMoreActionType.rename.name);
|
||||
await enterText(find.byType(AFTextField), newName);
|
||||
await tapButton(find.text(LocaleKeys.button_confirm.tr()));
|
||||
await enterText(find.byType(TextFormField), newName);
|
||||
await tapOKButton();
|
||||
await pumpAndSettle();
|
||||
}
|
||||
}
|
||||
-84
@@ -366,89 +366,5 @@ void main() {
|
||||
expect(getLinkFromNode(node), link);
|
||||
expect(getNodeText(node), afterText);
|
||||
});
|
||||
|
||||
testWidgets('insert link and clear link name', (tester) async {
|
||||
const text = 'edit link', link = 'https://test.appflowy.cloud';
|
||||
await prepareForToolbar(tester, text);
|
||||
|
||||
/// tap link button to show CreateLinkMenu
|
||||
final linkButton = find.byFlowySvg(FlowySvgs.toolbar_link_m);
|
||||
await tester.tapButton(linkButton);
|
||||
|
||||
/// search for page and select it
|
||||
final textField = find.descendant(
|
||||
of: find.byType(LinkCreateMenu),
|
||||
matching: find.byType(TextFormField),
|
||||
);
|
||||
await tester.enterText(textField, link);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.simulateKeyEvent(LogicalKeyboardKey.enter);
|
||||
Node node = tester.editor.getNodeAtPath([0]);
|
||||
expect(getLinkFromNode(node), link);
|
||||
await tester.simulateKeyEvent(LogicalKeyboardKey.escape);
|
||||
|
||||
/// hover link
|
||||
await tester.hoverOnWidget(find.byType(LinkHoverTrigger));
|
||||
|
||||
/// click edit button to show LinkEditMenu
|
||||
final editButton = find.byFlowySvg(FlowySvgs.toolbar_link_edit_m);
|
||||
await tester.tapButton(editButton);
|
||||
final linkEditMenu = find.byType(LinkEditMenu);
|
||||
expect(linkEditMenu, findsOneWidget);
|
||||
|
||||
/// clear the link name
|
||||
final titleField = find.descendant(
|
||||
of: linkEditMenu,
|
||||
matching: find.byType(TextFormField),
|
||||
);
|
||||
await tester.enterText(titleField, '');
|
||||
await tester.pumpAndSettle();
|
||||
await tester.simulateKeyEvent(LogicalKeyboardKey.enter);
|
||||
node = tester.editor.getNodeAtPath([0]);
|
||||
expect(getNodeText(node), link);
|
||||
});
|
||||
|
||||
testWidgets('insert link and clear link name and remove link',
|
||||
(tester) async {
|
||||
const text = 'edit link', link = 'https://test.appflowy.cloud';
|
||||
await prepareForToolbar(tester, text);
|
||||
|
||||
/// tap link button to show CreateLinkMenu
|
||||
final linkButton = find.byFlowySvg(FlowySvgs.toolbar_link_m);
|
||||
await tester.tapButton(linkButton);
|
||||
|
||||
/// search for page and select it
|
||||
final textField = find.descendant(
|
||||
of: find.byType(LinkCreateMenu),
|
||||
matching: find.byType(TextFormField),
|
||||
);
|
||||
await tester.enterText(textField, link);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.simulateKeyEvent(LogicalKeyboardKey.enter);
|
||||
Node node = tester.editor.getNodeAtPath([0]);
|
||||
expect(getLinkFromNode(node), link);
|
||||
await tester.simulateKeyEvent(LogicalKeyboardKey.escape);
|
||||
|
||||
/// hover link
|
||||
await tester.hoverOnWidget(find.byType(LinkHoverTrigger));
|
||||
|
||||
/// click edit button to show LinkEditMenu
|
||||
final editButton = find.byFlowySvg(FlowySvgs.toolbar_link_edit_m);
|
||||
await tester.tapButton(editButton);
|
||||
final linkEditMenu = find.byType(LinkEditMenu);
|
||||
expect(linkEditMenu, findsOneWidget);
|
||||
|
||||
/// clear the link name
|
||||
final titleField = find.descendant(
|
||||
of: linkEditMenu,
|
||||
matching: find.byType(TextFormField),
|
||||
);
|
||||
await tester.enterText(titleField, '');
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tapButton(find.byFlowySvg(FlowySvgs.toolbar_link_unlink_m));
|
||||
node = tester.editor.getNodeAtPath([0]);
|
||||
expect(getNodeText(node), link);
|
||||
expect(getLinkFromNode(node), null);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -56,9 +56,6 @@ void main() {
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
currentScaleFactor += 0.1;
|
||||
currentScaleFactor = double.parse(
|
||||
currentScaleFactor.toStringAsFixed(2),
|
||||
);
|
||||
|
||||
final scaleFactor = await windowSizeManager.getScaleFactor();
|
||||
expect(currentScaleFactor, appflowyScaleFactor);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import 'desktop/chat/chat_page_test.dart' as chat_page_test;
|
||||
import 'desktop/database/database_icon_test.dart' as database_icon_test;
|
||||
import 'desktop/first_test/first_test.dart' as first_test;
|
||||
import 'desktop/uncategorized/code_block_language_selector_test.dart'
|
||||
@@ -18,5 +17,4 @@ Future<void> runIntegration9OnDesktop() async {
|
||||
tabs_test.main();
|
||||
code_language_selector.main();
|
||||
database_icon_test.main();
|
||||
chat_page_test.main();
|
||||
}
|
||||
@@ -1,9 +1,6 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/home/home.dart';
|
||||
import 'package:appflowy/mobile/presentation/notifications/mobile_notifications_screen.dart';
|
||||
import 'package:appflowy/mobile/presentation/search/mobile_search_ask_ai_entrance.dart';
|
||||
import 'package:appflowy/mobile/presentation/search/mobile_search_page.dart';
|
||||
import 'package:appflowy/mobile/presentation/search/mobile_search_result.dart';
|
||||
import 'package:appflowy/mobile/presentation/search/mobile_search_textfield.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
@@ -59,35 +56,4 @@ void main() {
|
||||
expect(cancelButton, findsNothing);
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets('tap to search page and back to home', (tester) async {
|
||||
await tester.launchInAnonymousMode();
|
||||
|
||||
/// go to search page
|
||||
final searchButton = find.byFlowySvg(FlowySvgs.m_home_search_icon_m);
|
||||
expect(find.byType(MobileSearchScreen), findsNothing);
|
||||
await tester.tapButton(searchButton);
|
||||
expect(find.byType(MobileSearchScreen), findsOneWidget);
|
||||
|
||||
/// back to home page
|
||||
final backButton = find.byFlowySvg(FlowySvgs.search_page_arrow_left_m);
|
||||
expect(backButton, findsOneWidget);
|
||||
expect(find.byType(MobileHomeScreen), findsNothing);
|
||||
await tester.tapButton(backButton);
|
||||
expect(find.byType(MobileHomeScreen), findsOneWidget);
|
||||
|
||||
/// go to notification page
|
||||
final notificationButton = find.byFlowySvg(FlowySvgs.m_home_notification_m);
|
||||
expect(find.byType(MobileNotificationsScreenV2), findsNothing);
|
||||
await tester.tapButton(notificationButton);
|
||||
expect(find.byType(MobileNotificationsScreenV2), findsOneWidget);
|
||||
|
||||
/// go to search page
|
||||
await tester.tapButton(searchButton);
|
||||
expect(find.byType(MobileNotificationsScreenV2), findsNothing);
|
||||
expect(find.byType(MobileSearchScreen), findsOneWidget);
|
||||
await tester.tapButton(backButton);
|
||||
expect(find.byType(MobileNotificationsScreenV2), findsOneWidget);
|
||||
expect(find.byType(MobileSearchScreen), findsNothing);
|
||||
});
|
||||
}
|
||||
@@ -1,14 +1,9 @@
|
||||
import 'package:appflowy/ai/ai.dart';
|
||||
import 'package:appflowy/plugins/ai_chat/application/chat_bloc.dart';
|
||||
import 'package:appflowy/plugins/ai_chat/presentation/chat_page/chat_content_page.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/ai/ai_writer_toolbar_item.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_entities.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_chat_core/flutter_chat_core.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import '../../test/util.dart';
|
||||
import 'util.dart';
|
||||
|
||||
extension AppFlowyAITest on WidgetTester {
|
||||
@@ -43,27 +38,4 @@ extension AppFlowyAITest on WidgetTester {
|
||||
testTextInput.enterText(text);
|
||||
await pumpAndSettle(const Duration(milliseconds: 300));
|
||||
}
|
||||
|
||||
ChatBloc getCurrentChatBloc() {
|
||||
return element(find.byType(ChatContentPage)).read<ChatBloc>();
|
||||
}
|
||||
|
||||
Future<void> loadDefaultMessages(List<Message> messages) async {
|
||||
final chatBloc = getCurrentChatBloc();
|
||||
chatBloc.add(ChatEvent.didLoadLatestMessages(messages));
|
||||
await blocResponseFuture();
|
||||
}
|
||||
|
||||
Future<void> sendUserMessage(Message message) async {
|
||||
final chatBloc = getCurrentChatBloc();
|
||||
// using received message to simulate the user message
|
||||
chatBloc.add(ChatEvent.receiveMessage(message));
|
||||
await blocResponseFuture();
|
||||
}
|
||||
|
||||
Future<void> receiveAIMessage(Message message) async {
|
||||
final chatBloc = getCurrentChatBloc();
|
||||
chatBloc.add(ChatEvent.receiveMessage(message));
|
||||
await blocResponseFuture();
|
||||
}
|
||||
}
|
||||
@@ -38,13 +38,11 @@ import 'package:appflowy/workspace/presentation/notifications/widgets/notificati
|
||||
import 'package:appflowy/workspace/presentation/notifications/widgets/notification_tab_bar.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_menu.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialog_v2.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/more_view_actions/more_view_actions.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/more_view_actions/widgets/common_view_action.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/view_title_bar.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:appflowy_ui/appflowy_ui.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/widget/buttons/primary_button.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
@@ -266,8 +264,8 @@ extension CommonOperations on WidgetTester {
|
||||
/// Rename the page.
|
||||
Future<void> renamePage(String name) async {
|
||||
await tapRenamePageButton();
|
||||
await enterText(find.byType(AFTextField), name);
|
||||
await tapButton(find.text(LocaleKeys.button_confirm.tr()));
|
||||
await enterText(find.byType(TextFormField), name);
|
||||
await tapOKButton();
|
||||
}
|
||||
|
||||
Future<void> tapTrashButton() async {
|
||||
@@ -361,7 +359,7 @@ extension CommonOperations on WidgetTester {
|
||||
);
|
||||
final showRenameDialog = settingsOrFailure ?? false;
|
||||
if (showRenameDialog) {
|
||||
await tapButton(find.text(LocaleKeys.button_confirm.tr()));
|
||||
await tapOKButton();
|
||||
}
|
||||
await pumpAndSettle();
|
||||
|
||||
@@ -738,7 +736,8 @@ extension CommonOperations on WidgetTester {
|
||||
final workspace = find.byType(SidebarWorkspace);
|
||||
expect(workspace, findsOneWidget);
|
||||
|
||||
await tapButton(workspace, milliseconds: 5000);
|
||||
await tapButton(workspace, pumpAndSettle: false);
|
||||
await pump(const Duration(seconds: 5));
|
||||
}
|
||||
|
||||
Future<void> createCollaborativeWorkspace(String name) async {
|
||||
@@ -753,20 +752,22 @@ extension CommonOperations on WidgetTester {
|
||||
// click the create button
|
||||
final createButton = find.byKey(createWorkspaceButtonKey);
|
||||
expect(createButton, findsOneWidget);
|
||||
await tapButton(createButton);
|
||||
await tapButton(createButton, pumpAndSettle: false);
|
||||
await pump(const Duration(seconds: 5));
|
||||
|
||||
// see the create workspace dialog
|
||||
final createWorkspaceDialog = find.byType(CreateWorkspaceDialog);
|
||||
expect(createWorkspaceDialog, findsOneWidget);
|
||||
|
||||
// input the workspace name
|
||||
final workspaceNameInput = find.descendant(
|
||||
of: find.byType(AFTextFieldDialog),
|
||||
of: createWorkspaceDialog,
|
||||
matching: find.byType(TextField),
|
||||
);
|
||||
await enterText(workspaceNameInput, name);
|
||||
await pumpAndSettle();
|
||||
|
||||
await tapButton(
|
||||
find.text(LocaleKeys.button_confirm.tr()),
|
||||
milliseconds: 2000,
|
||||
);
|
||||
await tapButtonWithName(LocaleKeys.button_ok.tr(), pumpAndSettle: false);
|
||||
await pump(const Duration(seconds: 5));
|
||||
}
|
||||
|
||||
// For mobile platform to launch the app in anonymous mode
|
||||
@@ -1050,8 +1051,6 @@ extension ViewLayoutPBTest on ViewLayoutPB {
|
||||
return LocaleKeys.document_menuName.tr();
|
||||
case ViewLayoutPB.Calendar:
|
||||
return LocaleKeys.calendar_menuName.tr();
|
||||
case ViewLayoutPB.Chat:
|
||||
return LocaleKeys.chat_newChat.tr();
|
||||
default:
|
||||
throw UnsupportedError('Unsupported layout: $this');
|
||||
}
|
||||
|
||||
@@ -76,16 +76,15 @@ import 'package:appflowy/util/field_type_extension.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/clear_date_button.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/date_type_option_button.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/reminder_selector.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialog_v2.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:appflowy_board/appflowy_board.dart';
|
||||
import 'package:appflowy_ui/appflowy_ui.dart';
|
||||
import 'package:calendar_view/calendar_view.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text_input.dart';
|
||||
import 'package:flowy_infra_ui/widget/buttons/primary_button.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -837,14 +836,6 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
await tapButton(button);
|
||||
}
|
||||
|
||||
Future<void> tapClearCellsButton() async {
|
||||
final field = find.byWidgetPredicate(
|
||||
(widget) =>
|
||||
widget is FieldActionCell && widget.action == FieldAction.clearData,
|
||||
);
|
||||
await tapButton(field);
|
||||
}
|
||||
|
||||
Future<void> tapRowDetailPageRowActionButton() async =>
|
||||
tapButton(find.byType(RowActionButton));
|
||||
|
||||
@@ -1647,13 +1638,18 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
|
||||
await enterText(
|
||||
find.descendant(
|
||||
of: find.byType(AFTextFieldDialog),
|
||||
matching: find.byType(AFTextField),
|
||||
of: find.byType(FlowyFormTextInput),
|
||||
matching: find.byType(TextFormField),
|
||||
),
|
||||
name,
|
||||
);
|
||||
|
||||
await tapButton(find.text(LocaleKeys.button_confirm.tr()));
|
||||
final field = find.byWidgetPredicate(
|
||||
(widget) =>
|
||||
widget is PrimaryTextButton &&
|
||||
widget.label == LocaleKeys.button_ok.tr(),
|
||||
);
|
||||
await tapButton(field);
|
||||
}
|
||||
|
||||
Future<void> deleteDatebaseView(Finder linkedView) async {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import 'dart:async';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:appflowy/features/mension_person/presentation/mention_menu.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/base/view_page/app_bar_buttons.dart';
|
||||
import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_quick_action_button.dart';
|
||||
@@ -16,6 +15,7 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/header/cov
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/document_cover_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/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/workspace/presentation/widgets/pop_up_action.dart';
|
||||
@@ -233,7 +233,7 @@ class EditorOperations {
|
||||
/// Must call [showAtMenu] first.
|
||||
Future<void> tapAtMenuItemWithName(String name) async {
|
||||
final atMenuItem = find.descendant(
|
||||
of: find.byType(MentionMenu),
|
||||
of: find.byType(InlineActionsHandler),
|
||||
matching: find.text(name, findRichText: true),
|
||||
);
|
||||
await tester.tapButton(atMenuItem);
|
||||
|
||||
@@ -98,8 +98,10 @@ extension Expectation on WidgetTester {
|
||||
|
||||
/// Expect to the markdown file export success dialog.
|
||||
void expectToExportSuccess() {
|
||||
final exportSuccess = find.text(
|
||||
LocaleKeys.settings_files_exportFileSuccess.tr(),
|
||||
final exportSuccess = find.byWidgetPredicate(
|
||||
(widget) =>
|
||||
widget is FlowyText &&
|
||||
widget.text == LocaleKeys.settings_files_exportFileSuccess.tr(),
|
||||
);
|
||||
expect(exportSuccess, findsOneWidget);
|
||||
}
|
||||
|
||||
@@ -84,7 +84,6 @@ class MockAIRepository extends Mock implements AppFlowyAIService {
|
||||
String? objectId,
|
||||
required String text,
|
||||
PredefinedFormat? format,
|
||||
String? promptId,
|
||||
List<String> sourceIds = const [],
|
||||
List<AiWriterRecord> history = const [],
|
||||
required CompletionTypePB completionType,
|
||||
|
||||
@@ -4,9 +4,8 @@ import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sid
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/sidebar_workspace.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialog_v2.dart';
|
||||
import 'package:appflowy_ui/appflowy_ui.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'util.dart';
|
||||
@@ -32,24 +31,26 @@ extension AppFlowyWorkspace on WidgetTester {
|
||||
}
|
||||
|
||||
Future<void> changeWorkspaceName(String name) async {
|
||||
final menuItem = find.byType(WorkspaceMenuItem);
|
||||
expect(menuItem, findsOneWidget);
|
||||
final moreButton = find.descendant(
|
||||
of: find.byType(WorkspaceMenuItem),
|
||||
matching: find.byType(WorkspaceMoreActionList),
|
||||
);
|
||||
expect(moreButton, findsOneWidget);
|
||||
await hoverOnWidget(
|
||||
menuItem,
|
||||
moreButton,
|
||||
onHover: () async {
|
||||
await tapButton(
|
||||
find.descendant(
|
||||
of: menuItem,
|
||||
matching: find.byType(WorkspaceMoreActionList),
|
||||
),
|
||||
);
|
||||
await tapButton(find.text(LocaleKeys.button_rename.tr()));
|
||||
final input = find.descendant(
|
||||
of: find.byType(AFTextFieldDialog),
|
||||
matching: find.byType(AFTextField),
|
||||
await tapButton(moreButton);
|
||||
// wait for the menu to open
|
||||
final renameButton = find.findTextInFlowyText(
|
||||
LocaleKeys.button_rename.tr(),
|
||||
);
|
||||
await pumpUntilFound(renameButton);
|
||||
expect(renameButton, findsOneWidget);
|
||||
await tapButton(renameButton);
|
||||
final input = find.byType(TextFormField);
|
||||
expect(input, findsOneWidget);
|
||||
await enterText(input, name);
|
||||
await tapButton(find.text(LocaleKeys.button_confirm.tr()));
|
||||
await tapButton(find.text(LocaleKeys.button_ok.tr()));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -71,6 +71,11 @@ PODS:
|
||||
- SDWebImage (5.21.0):
|
||||
- SDWebImage/Core (= 5.21.0)
|
||||
- SDWebImage/Core (5.21.0)
|
||||
- Sentry/HybridSDK (8.46.0)
|
||||
- sentry_flutter (8.14.2):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- Sentry/HybridSDK (= 8.46.0)
|
||||
- share_plus (0.0.1):
|
||||
- Flutter
|
||||
- shared_preferences_foundation (0.0.1):
|
||||
@@ -107,6 +112,7 @@ DEPENDENCIES:
|
||||
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
|
||||
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
|
||||
- saver_gallery (from `.symlinks/plugins/saver_gallery/ios`)
|
||||
- sentry_flutter (from `.symlinks/plugins/sentry_flutter/ios`)
|
||||
- share_plus (from `.symlinks/plugins/share_plus/ios`)
|
||||
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
|
||||
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
|
||||
@@ -120,6 +126,7 @@ SPEC REPOS:
|
||||
- DKPhotoGallery
|
||||
- ReachabilitySwift
|
||||
- SDWebImage
|
||||
- Sentry
|
||||
- SwiftyGif
|
||||
- Toast
|
||||
|
||||
@@ -158,6 +165,8 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/permission_handler_apple/ios"
|
||||
saver_gallery:
|
||||
:path: ".symlinks/plugins/saver_gallery/ios"
|
||||
sentry_flutter:
|
||||
:path: ".symlinks/plugins/sentry_flutter/ios"
|
||||
share_plus:
|
||||
:path: ".symlinks/plugins/share_plus/ios"
|
||||
shared_preferences_foundation:
|
||||
@@ -193,6 +202,8 @@ SPEC CHECKSUMS:
|
||||
ReachabilitySwift: 32793e867593cfc1177f5d16491e3a197d2fccda
|
||||
saver_gallery: af2d0c762dafda254e0ad025ef0dabd6506cd490
|
||||
SDWebImage: f84b0feeb08d2d11e6a9b843cb06d75ebf5b8868
|
||||
Sentry: da60d980b197a46db0b35ea12cb8f39af48d8854
|
||||
sentry_flutter: 27892878729f42701297c628eb90e7c6529f3684
|
||||
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
|
||||
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
|
||||
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
|
||||
|
||||
@@ -5,9 +5,7 @@ export 'service/appflowy_ai_service.dart';
|
||||
export 'service/error.dart';
|
||||
export 'service/ai_model_state_notifier.dart';
|
||||
export 'service/select_model_bloc.dart';
|
||||
export 'service/view_selector_cubit.dart';
|
||||
export 'widgets/loading_indicator.dart';
|
||||
export 'widgets/view_selector.dart';
|
||||
export 'widgets/prompt_input/action_buttons.dart';
|
||||
export 'widgets/prompt_input/desktop_prompt_input.dart';
|
||||
export 'widgets/prompt_input/file_attachment_list.dart';
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/shared/easy_localiation_service.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-ai/protobuf.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
@@ -19,12 +14,11 @@ class AIStreamEventPrefix {
|
||||
static const start = 'start:';
|
||||
static const finish = 'finish:';
|
||||
static const comment = 'comment:';
|
||||
static const aiResponseLimit = 'ai_response_limit:';
|
||||
static const aiImageResponseLimit = 'ai_image_response_limit:';
|
||||
static const aiMaxRequired = 'ai_max_required:';
|
||||
static const localAINotReady = 'local_ai_not_ready:';
|
||||
static const localAIDisabled = 'local_ai_disabled:';
|
||||
static const aiFollowUp = 'ai_follow_up:';
|
||||
static const aiResponseLimit = 'AI_RESPONSE_LIMIT';
|
||||
static const aiImageResponseLimit = 'AI_IMAGE_RESPONSE_LIMIT';
|
||||
static const aiMaxRequired = 'AI_MAX_REQUIRED:';
|
||||
static const localAINotReady = 'LOCAL_AI_NOT_READY';
|
||||
static const localAIDisabled = 'LOCAL_AI_DISABLED';
|
||||
}
|
||||
|
||||
enum AiType {
|
||||
@@ -116,55 +110,75 @@ enum TextFormat {
|
||||
}
|
||||
|
||||
enum AiPromptCategory {
|
||||
@JsonValue("other")
|
||||
other,
|
||||
@JsonValue("development")
|
||||
development,
|
||||
@JsonValue("writing")
|
||||
writing,
|
||||
@JsonValue("healthAndFitness")
|
||||
healthAndFitness,
|
||||
@JsonValue("business")
|
||||
business,
|
||||
@JsonValue("marketing")
|
||||
marketing,
|
||||
@JsonValue("travel")
|
||||
travel,
|
||||
@JsonValue("contentSeo")
|
||||
contentSeo,
|
||||
@JsonValue("emailMarketing")
|
||||
emailMarketing,
|
||||
@JsonValue("paidAds")
|
||||
paidAds,
|
||||
@JsonValue("prCommunication")
|
||||
prCommunication,
|
||||
@JsonValue("recruiting")
|
||||
recruiting,
|
||||
@JsonValue("sales")
|
||||
sales,
|
||||
@JsonValue("socialMedia")
|
||||
socialMedia,
|
||||
@JsonValue("strategy")
|
||||
strategy,
|
||||
@JsonValue("caseStudies")
|
||||
caseStudies,
|
||||
@JsonValue("salesCopy")
|
||||
salesCopy,
|
||||
@JsonValue("education")
|
||||
education,
|
||||
@JsonValue("work")
|
||||
work,
|
||||
@JsonValue("podcastProduction")
|
||||
podcastProduction,
|
||||
@JsonValue("copyWriting")
|
||||
copyWriting,
|
||||
@JsonValue("customerSuccess")
|
||||
customerSuccess;
|
||||
|
||||
String get i18n => token.tr();
|
||||
|
||||
String get token {
|
||||
String get i18n {
|
||||
return switch (this) {
|
||||
other => LocaleKeys.ai_customPrompt_others,
|
||||
development => LocaleKeys.ai_customPrompt_development,
|
||||
writing => LocaleKeys.ai_customPrompt_writing,
|
||||
healthAndFitness => LocaleKeys.ai_customPrompt_healthAndFitness,
|
||||
business => LocaleKeys.ai_customPrompt_business,
|
||||
marketing => LocaleKeys.ai_customPrompt_marketing,
|
||||
travel => LocaleKeys.ai_customPrompt_travel,
|
||||
contentSeo => LocaleKeys.ai_customPrompt_contentSeo,
|
||||
emailMarketing => LocaleKeys.ai_customPrompt_emailMarketing,
|
||||
paidAds => LocaleKeys.ai_customPrompt_paidAds,
|
||||
prCommunication => LocaleKeys.ai_customPrompt_prCommunication,
|
||||
recruiting => LocaleKeys.ai_customPrompt_recruiting,
|
||||
sales => LocaleKeys.ai_customPrompt_sales,
|
||||
socialMedia => LocaleKeys.ai_customPrompt_socialMedia,
|
||||
strategy => LocaleKeys.ai_customPrompt_strategy,
|
||||
caseStudies => LocaleKeys.ai_customPrompt_caseStudies,
|
||||
salesCopy => LocaleKeys.ai_customPrompt_salesCopy,
|
||||
education => LocaleKeys.ai_customPrompt_education,
|
||||
work => LocaleKeys.ai_customPrompt_work,
|
||||
podcastProduction => LocaleKeys.ai_customPrompt_podcastProduction,
|
||||
copyWriting => LocaleKeys.ai_customPrompt_copyWriting,
|
||||
customerSuccess => LocaleKeys.ai_customPrompt_customerSuccess,
|
||||
other => LocaleKeys.ai_customPrompt_others.tr(),
|
||||
development => LocaleKeys.ai_customPrompt_development.tr(),
|
||||
writing => LocaleKeys.ai_customPrompt_writing.tr(),
|
||||
healthAndFitness => LocaleKeys.ai_customPrompt_healthAndFitness.tr(),
|
||||
business => LocaleKeys.ai_customPrompt_business.tr(),
|
||||
marketing => LocaleKeys.ai_customPrompt_marketing.tr(),
|
||||
travel => LocaleKeys.ai_customPrompt_travel.tr(),
|
||||
contentSeo => LocaleKeys.ai_customPrompt_contentSeo.tr(),
|
||||
emailMarketing => LocaleKeys.ai_customPrompt_emailMarketing.tr(),
|
||||
paidAds => LocaleKeys.ai_customPrompt_paidAds.tr(),
|
||||
prCommunication => LocaleKeys.ai_customPrompt_prCommunication.tr(),
|
||||
recruiting => LocaleKeys.ai_customPrompt_recruiting.tr(),
|
||||
sales => LocaleKeys.ai_customPrompt_sales.tr(),
|
||||
socialMedia => LocaleKeys.ai_customPrompt_socialMedia.tr(),
|
||||
strategy => LocaleKeys.ai_customPrompt_strategy.tr(),
|
||||
caseStudies => LocaleKeys.ai_customPrompt_caseStudies.tr(),
|
||||
salesCopy => LocaleKeys.ai_customPrompt_salesCopy.tr(),
|
||||
education => LocaleKeys.ai_customPrompt_education.tr(),
|
||||
work => LocaleKeys.ai_customPrompt_work.tr(),
|
||||
podcastProduction => LocaleKeys.ai_customPrompt_podcastProduction.tr(),
|
||||
copyWriting => LocaleKeys.ai_customPrompt_copyWriting.tr(),
|
||||
customerSuccess => LocaleKeys.ai_customPrompt_customerSuccess.tr(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -178,38 +192,8 @@ class AiPrompt extends Equatable {
|
||||
required this.category,
|
||||
required this.example,
|
||||
required this.isFeatured,
|
||||
required this.isCustom,
|
||||
});
|
||||
|
||||
factory AiPrompt.fromPB(CustomPromptPB pb) {
|
||||
final map = _buildCategoryNameMap();
|
||||
final categories = pb.category
|
||||
.split(',')
|
||||
.map((categoryName) => categoryName.trim())
|
||||
.map(
|
||||
(categoryName) {
|
||||
final entry = map.entries.firstWhereOrNull(
|
||||
(entry) =>
|
||||
entry.value.$1 == categoryName ||
|
||||
entry.value.$2 == categoryName,
|
||||
);
|
||||
return entry?.key ?? AiPromptCategory.other;
|
||||
},
|
||||
)
|
||||
.toSet()
|
||||
.toList();
|
||||
|
||||
return AiPrompt(
|
||||
id: pb.id,
|
||||
name: pb.name,
|
||||
content: pb.content,
|
||||
category: categories,
|
||||
example: pb.example,
|
||||
isFeatured: false,
|
||||
isCustom: true,
|
||||
);
|
||||
}
|
||||
|
||||
factory AiPrompt.fromJson(Map<String, dynamic> json) =>
|
||||
_$AiPromptFromJson(json);
|
||||
|
||||
@@ -218,169 +202,16 @@ class AiPrompt extends Equatable {
|
||||
final String id;
|
||||
final String name;
|
||||
final String content;
|
||||
@JsonKey(fromJson: _categoryFromJson)
|
||||
final List<AiPromptCategory> category;
|
||||
@JsonKey(
|
||||
unknownEnumValue: AiPromptCategory.other,
|
||||
defaultValue: AiPromptCategory.other,
|
||||
)
|
||||
final AiPromptCategory category;
|
||||
@JsonKey(defaultValue: "")
|
||||
final String example;
|
||||
@JsonKey(defaultValue: false)
|
||||
final bool isFeatured;
|
||||
@JsonKey(defaultValue: false)
|
||||
final bool isCustom;
|
||||
|
||||
@override
|
||||
List<Object?> get props =>
|
||||
[id, name, content, category, example, isFeatured, isCustom];
|
||||
|
||||
static Map<AiPromptCategory, (String, String)> _buildCategoryNameMap() {
|
||||
final service = getIt<EasyLocalizationService>();
|
||||
return {
|
||||
for (final category in AiPromptCategory.values)
|
||||
category: (
|
||||
service.getFallbackTranslation(category.token),
|
||||
service.getFallbackTranslation(category.token),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
static List<AiPromptCategory> _categoryFromJson(dynamic json) {
|
||||
if (json is String) {
|
||||
return json
|
||||
.split(',')
|
||||
.map((categoryName) => categoryName.trim())
|
||||
.map(
|
||||
(categoryName) => $enumDecode(
|
||||
_aiPromptCategoryEnumMap,
|
||||
categoryName,
|
||||
unknownValue: AiPromptCategory.other,
|
||||
),
|
||||
)
|
||||
.toSet()
|
||||
.toList();
|
||||
}
|
||||
|
||||
return [AiPromptCategory.other];
|
||||
}
|
||||
}
|
||||
|
||||
const _aiPromptCategoryEnumMap = {
|
||||
AiPromptCategory.other: 'other',
|
||||
AiPromptCategory.development: 'development',
|
||||
AiPromptCategory.writing: 'writing',
|
||||
AiPromptCategory.healthAndFitness: 'healthAndFitness',
|
||||
AiPromptCategory.business: 'business',
|
||||
AiPromptCategory.marketing: 'marketing',
|
||||
AiPromptCategory.travel: 'travel',
|
||||
AiPromptCategory.contentSeo: 'contentSeo',
|
||||
AiPromptCategory.emailMarketing: 'emailMarketing',
|
||||
AiPromptCategory.paidAds: 'paidAds',
|
||||
AiPromptCategory.prCommunication: 'prCommunication',
|
||||
AiPromptCategory.recruiting: 'recruiting',
|
||||
AiPromptCategory.sales: 'sales',
|
||||
AiPromptCategory.socialMedia: 'socialMedia',
|
||||
AiPromptCategory.strategy: 'strategy',
|
||||
AiPromptCategory.caseStudies: 'caseStudies',
|
||||
AiPromptCategory.salesCopy: 'salesCopy',
|
||||
AiPromptCategory.education: 'education',
|
||||
AiPromptCategory.work: 'work',
|
||||
AiPromptCategory.podcastProduction: 'podcastProduction',
|
||||
AiPromptCategory.copyWriting: 'copyWriting',
|
||||
AiPromptCategory.customerSuccess: 'customerSuccess',
|
||||
};
|
||||
|
||||
class CustomPromptDatabaseConfig extends Equatable {
|
||||
const CustomPromptDatabaseConfig({
|
||||
required this.view,
|
||||
required this.titleFieldId,
|
||||
required this.contentFieldId,
|
||||
required this.exampleFieldId,
|
||||
required this.categoryFieldId,
|
||||
});
|
||||
|
||||
factory CustomPromptDatabaseConfig.fromAiPB(
|
||||
CustomPromptDatabaseConfigurationPB pb,
|
||||
ViewPB view,
|
||||
) {
|
||||
final config = CustomPromptDatabaseConfig(
|
||||
view: view,
|
||||
titleFieldId: pb.titleFieldId,
|
||||
contentFieldId: pb.contentFieldId,
|
||||
exampleFieldId: pb.hasExampleFieldId() ? pb.exampleFieldId : null,
|
||||
categoryFieldId: pb.hasCategoryFieldId() ? pb.categoryFieldId : null,
|
||||
);
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
factory CustomPromptDatabaseConfig.fromDbPB(
|
||||
CustomPromptDatabaseConfigPB pb,
|
||||
ViewPB view,
|
||||
) {
|
||||
final config = CustomPromptDatabaseConfig(
|
||||
view: view,
|
||||
titleFieldId: pb.titleFieldId,
|
||||
contentFieldId: pb.contentFieldId,
|
||||
exampleFieldId: pb.hasExampleFieldId() ? pb.exampleFieldId : null,
|
||||
categoryFieldId: pb.hasCategoryFieldId() ? pb.categoryFieldId : null,
|
||||
);
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
final ViewPB view;
|
||||
final String titleFieldId;
|
||||
final String contentFieldId;
|
||||
final String? exampleFieldId;
|
||||
final String? categoryFieldId;
|
||||
|
||||
@override
|
||||
List<Object?> get props =>
|
||||
[view.id, titleFieldId, contentFieldId, exampleFieldId, categoryFieldId];
|
||||
|
||||
CustomPromptDatabaseConfig copyWith({
|
||||
ViewPB? view,
|
||||
String? titleFieldId,
|
||||
String? contentFieldId,
|
||||
String? exampleFieldId,
|
||||
String? categoryFieldId,
|
||||
}) {
|
||||
return CustomPromptDatabaseConfig(
|
||||
view: view ?? this.view,
|
||||
titleFieldId: titleFieldId ?? this.titleFieldId,
|
||||
contentFieldId: contentFieldId ?? this.contentFieldId,
|
||||
exampleFieldId: exampleFieldId ?? this.exampleFieldId,
|
||||
categoryFieldId: categoryFieldId ?? this.categoryFieldId,
|
||||
);
|
||||
}
|
||||
|
||||
CustomPromptDatabaseConfigurationPB toAiPB() {
|
||||
final payload = CustomPromptDatabaseConfigurationPB.create()
|
||||
..viewId = view.id
|
||||
..titleFieldId = titleFieldId
|
||||
..contentFieldId = contentFieldId;
|
||||
|
||||
if (exampleFieldId != null) {
|
||||
payload.exampleFieldId = exampleFieldId!;
|
||||
}
|
||||
if (categoryFieldId != null) {
|
||||
payload.categoryFieldId = categoryFieldId!;
|
||||
}
|
||||
|
||||
return payload;
|
||||
}
|
||||
|
||||
CustomPromptDatabaseConfigPB toDbPB() {
|
||||
final payload = CustomPromptDatabaseConfigPB.create()
|
||||
..viewId = view.id
|
||||
..titleFieldId = titleFieldId
|
||||
..contentFieldId = contentFieldId;
|
||||
|
||||
if (exampleFieldId != null) {
|
||||
payload.exampleFieldId = exampleFieldId!;
|
||||
}
|
||||
if (categoryFieldId != null) {
|
||||
payload.categoryFieldId = categoryFieldId!;
|
||||
}
|
||||
|
||||
return payload;
|
||||
}
|
||||
List<Object?> get props => [id, name, content, category, example, isFeatured];
|
||||
}
|
||||
@@ -1,161 +0,0 @@
|
||||
import 'package:appflowy/ai/ai.dart';
|
||||
import 'package:appflowy/plugins/database/domain/field_service.dart';
|
||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'ai_prompt_database_selector_cubit.freezed.dart';
|
||||
|
||||
class AiPromptDatabaseSelectorCubit
|
||||
extends Cubit<AiPromptDatabaseSelectorState> {
|
||||
AiPromptDatabaseSelectorCubit({
|
||||
required CustomPromptDatabaseConfig? configuration,
|
||||
}) : super(AiPromptDatabaseSelectorState.loading()) {
|
||||
_init(configuration);
|
||||
}
|
||||
|
||||
void _init(CustomPromptDatabaseConfig? config) async {
|
||||
if (config == null) {
|
||||
emit(AiPromptDatabaseSelectorState.empty());
|
||||
return;
|
||||
}
|
||||
|
||||
final fields = await _getFields(config.view.id);
|
||||
|
||||
if (fields == null) {
|
||||
emit(AiPromptDatabaseSelectorState.empty());
|
||||
return;
|
||||
}
|
||||
|
||||
emit(
|
||||
AiPromptDatabaseSelectorState.selected(
|
||||
config: config,
|
||||
fields: fields,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void selectDatabaseView(String viewId) async {
|
||||
final configuration = await _testDatabase(viewId);
|
||||
|
||||
if (configuration == null) {
|
||||
final stateCopy = state;
|
||||
emit(AiPromptDatabaseSelectorState.invalidDatabase());
|
||||
emit(stateCopy);
|
||||
return;
|
||||
}
|
||||
|
||||
final databaseView = await AiPromptSelectorCubit.getDatabaseView(viewId);
|
||||
final fields = await _getFields(viewId);
|
||||
|
||||
if (databaseView == null || fields == null) {
|
||||
final stateCopy = state;
|
||||
emit(AiPromptDatabaseSelectorState.invalidDatabase());
|
||||
emit(stateCopy);
|
||||
return;
|
||||
}
|
||||
|
||||
final config = CustomPromptDatabaseConfig.fromDbPB(
|
||||
configuration,
|
||||
databaseView,
|
||||
);
|
||||
|
||||
emit(
|
||||
AiPromptDatabaseSelectorState.selected(
|
||||
config: config,
|
||||
fields: fields,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void selectContentField(String fieldId) {
|
||||
final state = this.state;
|
||||
if (state is! _Selected) {
|
||||
return;
|
||||
}
|
||||
|
||||
final config = state.config.copyWith(
|
||||
contentFieldId: fieldId,
|
||||
);
|
||||
|
||||
emit(
|
||||
AiPromptDatabaseSelectorState.selected(
|
||||
config: config,
|
||||
fields: state.fields,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void selectExampleField(String? fieldId) {
|
||||
final state = this.state;
|
||||
if (state is! _Selected) {
|
||||
return;
|
||||
}
|
||||
|
||||
final config = CustomPromptDatabaseConfig(
|
||||
exampleFieldId: fieldId,
|
||||
view: state.config.view,
|
||||
titleFieldId: state.config.titleFieldId,
|
||||
contentFieldId: state.config.contentFieldId,
|
||||
categoryFieldId: state.config.categoryFieldId,
|
||||
);
|
||||
|
||||
emit(
|
||||
AiPromptDatabaseSelectorState.selected(
|
||||
config: config,
|
||||
fields: state.fields,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void selectCategoryField(String? fieldId) {
|
||||
final state = this.state;
|
||||
if (state is! _Selected) {
|
||||
return;
|
||||
}
|
||||
|
||||
final config = CustomPromptDatabaseConfig(
|
||||
categoryFieldId: fieldId,
|
||||
view: state.config.view,
|
||||
titleFieldId: state.config.titleFieldId,
|
||||
contentFieldId: state.config.contentFieldId,
|
||||
exampleFieldId: state.config.exampleFieldId,
|
||||
);
|
||||
|
||||
emit(
|
||||
AiPromptDatabaseSelectorState.selected(
|
||||
config: config,
|
||||
fields: state.fields,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<FieldPB>?> _getFields(String viewId) {
|
||||
return FieldBackendService.getFields(viewId: viewId).toNullable();
|
||||
}
|
||||
|
||||
Future<CustomPromptDatabaseConfigPB?> _testDatabase(
|
||||
String viewId,
|
||||
) {
|
||||
return DatabaseEventTestCustomPromptDatabaseConfiguration(
|
||||
DatabaseViewIdPB(value: viewId),
|
||||
).send().toNullable();
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class AiPromptDatabaseSelectorState with _$AiPromptDatabaseSelectorState {
|
||||
const factory AiPromptDatabaseSelectorState.loading() = _Loading;
|
||||
|
||||
const factory AiPromptDatabaseSelectorState.empty() = _Empty;
|
||||
|
||||
const factory AiPromptDatabaseSelectorState.selected({
|
||||
required CustomPromptDatabaseConfig config,
|
||||
required List<FieldPB> fields,
|
||||
}) = _Selected;
|
||||
|
||||
const factory AiPromptDatabaseSelectorState.invalidDatabase() =
|
||||
_InvalidDatabase;
|
||||
}
|
||||
@@ -23,8 +23,6 @@ class AIPromptInputBloc extends Bloc<AIPromptInputEvent, AIPromptInputState> {
|
||||
|
||||
final AIModelStateNotifier aiModelStateNotifier;
|
||||
|
||||
String? promptId;
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
await aiModelStateNotifier.dispose();
|
||||
@@ -90,11 +88,7 @@ class AIPromptInputBloc extends Bloc<AIPromptInputEvent, AIPromptInputState> {
|
||||
),
|
||||
);
|
||||
},
|
||||
updatePromptId: (promptId) {
|
||||
this.promptId = promptId;
|
||||
},
|
||||
clearMetadata: () {
|
||||
promptId = null;
|
||||
emit(
|
||||
state.copyWith(
|
||||
attachedFiles: [],
|
||||
@@ -157,8 +151,6 @@ class AIPromptInputEvent with _$AIPromptInputEvent {
|
||||
const factory AIPromptInputEvent.updateMentionedViews(List<ViewPB> views) =
|
||||
_UpdateMentionedViews;
|
||||
const factory AIPromptInputEvent.clearMetadata() = _ClearMetadata;
|
||||
const factory AIPromptInputEvent.updatePromptId(String promptId) =
|
||||
_UpdatePromptId;
|
||||
}
|
||||
|
||||
@freezed
|
||||
|
||||
@@ -1,16 +1,8 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:appflowy/workspace/application/view/prelude.dart';
|
||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
import '../../plugins/trash/application/trash_service.dart';
|
||||
import 'ai_entities.dart';
|
||||
import 'appflowy_ai_service.dart';
|
||||
|
||||
@@ -22,7 +14,7 @@ class AiPromptSelectorCubit extends Cubit<AiPromptSelectorState> {
|
||||
}) : _aiService = aiService ?? AppFlowyAIService(),
|
||||
super(AiPromptSelectorState.loading()) {
|
||||
filterTextController.addListener(_filterTextChanged);
|
||||
_init();
|
||||
_loadPrompts();
|
||||
}
|
||||
|
||||
final AppFlowyAIService _aiService;
|
||||
@@ -35,146 +27,33 @@ class AiPromptSelectorCubit extends Cubit<AiPromptSelectorState> {
|
||||
await super.close();
|
||||
}
|
||||
|
||||
void _init() async {
|
||||
void _loadPrompts() async {
|
||||
availablePrompts.addAll(await _aiService.getBuiltInPrompts());
|
||||
|
||||
final featuredPrompts =
|
||||
availablePrompts.where((prompt) => prompt.isFeatured);
|
||||
final visiblePrompts = _getFilteredPrompts(featuredPrompts);
|
||||
|
||||
emit(
|
||||
AiPromptSelectorState.ready(
|
||||
visiblePrompts: visiblePrompts.toList(),
|
||||
isCustomPromptSectionSelected: false,
|
||||
isFeaturedSectionSelected: true,
|
||||
visiblePrompts: visiblePrompts,
|
||||
showCustomPrompts: false,
|
||||
isFeaturedCategorySelected: true,
|
||||
selectedPromptId: visiblePrompts.firstOrNull?.id,
|
||||
databaseConfig: null,
|
||||
isLoadingCustomPrompts: true,
|
||||
selectedCategory: null,
|
||||
favoritePrompts: [],
|
||||
),
|
||||
);
|
||||
|
||||
loadCustomPrompts();
|
||||
}
|
||||
|
||||
void loadCustomPrompts() {
|
||||
state.maybeMap(
|
||||
ready: (readyState) async {
|
||||
emit(
|
||||
readyState.copyWith(isLoadingCustomPrompts: true),
|
||||
);
|
||||
|
||||
CustomPromptDatabaseConfig? configuration = readyState.databaseConfig;
|
||||
if (configuration == null) {
|
||||
final configResult =
|
||||
await AIEventGetCustomPromptDatabaseConfiguration()
|
||||
.send()
|
||||
.toNullable();
|
||||
if (configResult != null) {
|
||||
final view = await getDatabaseView(configResult.viewId);
|
||||
if (view != null) {
|
||||
configuration = CustomPromptDatabaseConfig.fromAiPB(
|
||||
configResult,
|
||||
view,
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
final view = await getDatabaseView(configuration.view.id);
|
||||
if (view != null) {
|
||||
configuration = configuration.copyWith(view: view);
|
||||
}
|
||||
}
|
||||
|
||||
if (configuration == null) {
|
||||
emit(
|
||||
readyState.copyWith(isLoadingCustomPrompts: false),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
availablePrompts.removeWhere((prompt) => prompt.isCustom);
|
||||
|
||||
final customPrompts =
|
||||
await _aiService.getDatabasePrompts(configuration.toDbPB());
|
||||
|
||||
if (customPrompts == null) {
|
||||
final prompts = availablePrompts.where((prompt) => prompt.isFeatured);
|
||||
final visiblePrompts = _getFilteredPrompts(prompts);
|
||||
final selectedPromptId = _getVisibleSelectedPrompt(
|
||||
visiblePrompts,
|
||||
readyState.selectedPromptId,
|
||||
);
|
||||
|
||||
emit(
|
||||
readyState.copyWith(
|
||||
visiblePrompts: visiblePrompts.toList(),
|
||||
selectedPromptId: selectedPromptId,
|
||||
databaseConfig: configuration,
|
||||
isLoadingCustomPrompts: false,
|
||||
isFeaturedSectionSelected: true,
|
||||
isCustomPromptSectionSelected: false,
|
||||
selectedCategory: null,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
availablePrompts.addAll(customPrompts);
|
||||
|
||||
final prompts = _getPromptsByCategory(readyState);
|
||||
final visiblePrompts = _getFilteredPrompts(prompts);
|
||||
final selectedPromptId = _getVisibleSelectedPrompt(
|
||||
visiblePrompts,
|
||||
readyState.selectedPromptId,
|
||||
);
|
||||
|
||||
emit(
|
||||
readyState.copyWith(
|
||||
visiblePrompts: visiblePrompts.toList(),
|
||||
databaseConfig: configuration,
|
||||
isLoadingCustomPrompts: false,
|
||||
selectedPromptId: selectedPromptId,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
orElse: () {},
|
||||
);
|
||||
}
|
||||
|
||||
void selectCustomSection() {
|
||||
state.maybeMap(
|
||||
ready: (readyState) {
|
||||
final prompts = availablePrompts.where((prompt) => prompt.isCustom);
|
||||
final visiblePrompts = _getFilteredPrompts(prompts);
|
||||
|
||||
emit(
|
||||
readyState.copyWith(
|
||||
visiblePrompts: visiblePrompts.toList(),
|
||||
selectedPromptId: visiblePrompts.firstOrNull?.id,
|
||||
isCustomPromptSectionSelected: true,
|
||||
isFeaturedSectionSelected: false,
|
||||
selectedCategory: null,
|
||||
),
|
||||
);
|
||||
},
|
||||
orElse: () {},
|
||||
);
|
||||
}
|
||||
|
||||
void selectFeaturedSection() {
|
||||
void selectFeaturedCategory() {
|
||||
state.maybeMap(
|
||||
ready: (readyState) {
|
||||
final prompts = availablePrompts.where((prompt) => prompt.isFeatured);
|
||||
final visiblePrompts = _getFilteredPrompts(prompts);
|
||||
|
||||
emit(
|
||||
readyState.copyWith(
|
||||
visiblePrompts: visiblePrompts.toList(),
|
||||
visiblePrompts: visiblePrompts,
|
||||
isFeaturedCategorySelected: true,
|
||||
selectedPromptId: visiblePrompts.firstOrNull?.id,
|
||||
isFeaturedSectionSelected: true,
|
||||
isCustomPromptSectionSelected: false,
|
||||
selectedCategory: null,
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -185,24 +64,24 @@ class AiPromptSelectorCubit extends Cubit<AiPromptSelectorState> {
|
||||
void selectCategory(AiPromptCategory? category) {
|
||||
state.maybeMap(
|
||||
ready: (readyState) {
|
||||
final prompts = category == null
|
||||
final unfilteredPrompts = category == null
|
||||
? availablePrompts
|
||||
: availablePrompts
|
||||
.where((prompt) => prompt.category.contains(category));
|
||||
final visiblePrompts = _getFilteredPrompts(prompts);
|
||||
: availablePrompts.where((prompt) => prompt.category == category);
|
||||
final prompts = _getFilteredPrompts(unfilteredPrompts);
|
||||
|
||||
final selectedPromptId = _getVisibleSelectedPrompt(
|
||||
visiblePrompts,
|
||||
readyState.selectedPromptId,
|
||||
);
|
||||
// Check if the selected prompt is still visible
|
||||
String? selectedPromptId = prompts.firstOrNull?.id;
|
||||
if (readyState.selectedPromptId != null &&
|
||||
prompts.any((prompt) => prompt.id == readyState.selectedPromptId)) {
|
||||
selectedPromptId = readyState.selectedPromptId;
|
||||
}
|
||||
|
||||
emit(
|
||||
readyState.copyWith(
|
||||
visiblePrompts: visiblePrompts.toList(),
|
||||
visiblePrompts: prompts,
|
||||
isFeaturedCategorySelected: false,
|
||||
selectedCategory: category,
|
||||
selectedPromptId: selectedPromptId,
|
||||
isFeaturedSectionSelected: false,
|
||||
isCustomPromptSectionSelected: false,
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -237,6 +116,8 @@ class AiPromptSelectorCubit extends Cubit<AiPromptSelectorState> {
|
||||
emit(
|
||||
readyState.copyWith(favoritePrompts: favoritePrompts),
|
||||
);
|
||||
|
||||
// TODO(RS): Save the updated favorite prompts to local storage or database
|
||||
},
|
||||
orElse: () {},
|
||||
);
|
||||
@@ -249,9 +130,7 @@ class AiPromptSelectorCubit extends Cubit<AiPromptSelectorState> {
|
||||
emit(
|
||||
readyState.copyWith(
|
||||
visiblePrompts: availablePrompts,
|
||||
isCustomPromptSectionSelected: false,
|
||||
isFeaturedSectionSelected: true,
|
||||
selectedPromptId: availablePrompts.firstOrNull?.id,
|
||||
selectedPromptId: null,
|
||||
selectedCategory: null,
|
||||
),
|
||||
);
|
||||
@@ -260,74 +139,27 @@ class AiPromptSelectorCubit extends Cubit<AiPromptSelectorState> {
|
||||
);
|
||||
}
|
||||
|
||||
void updateCustomPromptDatabaseConfiguration(
|
||||
CustomPromptDatabaseConfig configuration,
|
||||
) async {
|
||||
state.maybeMap(
|
||||
ready: (readyState) async {
|
||||
emit(
|
||||
readyState.copyWith(isLoadingCustomPrompts: true),
|
||||
);
|
||||
|
||||
final customPrompts =
|
||||
await _aiService.getDatabasePrompts(configuration.toDbPB());
|
||||
|
||||
if (customPrompts == null) {
|
||||
emit(AiPromptSelectorState.invalidDatabase());
|
||||
emit(readyState);
|
||||
return;
|
||||
}
|
||||
|
||||
availablePrompts
|
||||
..removeWhere((prompt) => prompt.isCustom)
|
||||
..addAll(customPrompts);
|
||||
|
||||
await AIEventSetCustomPromptDatabaseConfiguration(
|
||||
configuration.toAiPB(),
|
||||
).send().onFailure(Log.error);
|
||||
|
||||
final prompts = _getPromptsByCategory(readyState);
|
||||
final visiblePrompts = _getFilteredPrompts(prompts);
|
||||
final selectedPromptId = _getVisibleSelectedPrompt(
|
||||
visiblePrompts,
|
||||
readyState.selectedPromptId,
|
||||
);
|
||||
emit(
|
||||
readyState.copyWith(
|
||||
visiblePrompts: visiblePrompts.toList(),
|
||||
selectedPromptId: selectedPromptId,
|
||||
databaseConfig: configuration,
|
||||
isLoadingCustomPrompts: false,
|
||||
),
|
||||
);
|
||||
},
|
||||
orElse: () => {},
|
||||
);
|
||||
}
|
||||
|
||||
void _filterTextChanged() {
|
||||
state.maybeMap(
|
||||
ready: (readyState) {
|
||||
final prompts = _getPromptsByCategory(readyState);
|
||||
final visiblePrompts = _getFilteredPrompts(prompts);
|
||||
|
||||
final selectedPromptId = _getVisibleSelectedPrompt(
|
||||
visiblePrompts,
|
||||
readyState.selectedPromptId,
|
||||
);
|
||||
final unfilteredPrompts = readyState.selectedCategory == null
|
||||
? readyState.isFeaturedCategorySelected
|
||||
? availablePrompts.where((prompt) => prompt.isFeatured)
|
||||
: availablePrompts
|
||||
: availablePrompts.where(
|
||||
(prompt) => prompt.category == readyState.selectedCategory,
|
||||
);
|
||||
final filteredPrompts = _getFilteredPrompts(unfilteredPrompts);
|
||||
|
||||
emit(
|
||||
readyState.copyWith(
|
||||
visiblePrompts: visiblePrompts.toList(),
|
||||
selectedPromptId: selectedPromptId,
|
||||
),
|
||||
readyState.copyWith(visiblePrompts: filteredPrompts),
|
||||
);
|
||||
},
|
||||
orElse: () {},
|
||||
);
|
||||
}
|
||||
|
||||
Iterable<AiPrompt> _getFilteredPrompts(Iterable<AiPrompt> prompts) {
|
||||
List<AiPrompt> _getFilteredPrompts(Iterable<AiPrompt> prompts) {
|
||||
final filterText = filterTextController.value.text.trim().toLowerCase();
|
||||
|
||||
return prompts.where((prompt) {
|
||||
@@ -335,53 +167,6 @@ class AiPromptSelectorCubit extends Cubit<AiPromptSelectorState> {
|
||||
return content.contains(filterText);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
Iterable<AiPrompt> _getPromptsByCategory(_AiPromptSelectorReadyState state) {
|
||||
return availablePrompts.where((prompt) {
|
||||
if (state.selectedCategory != null) {
|
||||
return prompt.category.contains(state.selectedCategory);
|
||||
}
|
||||
if (state.isFeaturedSectionSelected) {
|
||||
return prompt.isFeatured;
|
||||
}
|
||||
if (state.isCustomPromptSectionSelected) {
|
||||
return prompt.isCustom;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
String? _getVisibleSelectedPrompt(
|
||||
Iterable<AiPrompt> visiblePrompts,
|
||||
String? currentlySelectedPromptId,
|
||||
) {
|
||||
if (visiblePrompts
|
||||
.any((prompt) => prompt.id == currentlySelectedPromptId)) {
|
||||
return currentlySelectedPromptId;
|
||||
}
|
||||
|
||||
return visiblePrompts.firstOrNull?.id;
|
||||
}
|
||||
|
||||
static Future<ViewPB?> getDatabaseView(String viewId) async {
|
||||
final view = await ViewBackendService.getView(viewId).toNullable();
|
||||
|
||||
if (view != null) {
|
||||
return view;
|
||||
}
|
||||
|
||||
final trashViews = await TrashService().readTrash().toNullable();
|
||||
final trashedItem =
|
||||
trashViews?.items.firstWhereOrNull((element) => element.id == viewId);
|
||||
|
||||
if (trashedItem == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return ViewPB()
|
||||
..id = trashedItem.id
|
||||
..name = trashedItem.name;
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
@@ -390,18 +175,13 @@ class AiPromptSelectorState with _$AiPromptSelectorState {
|
||||
|
||||
const factory AiPromptSelectorState.loading() = _AiPromptSelectorLoadingState;
|
||||
|
||||
const factory AiPromptSelectorState.invalidDatabase() =
|
||||
_AiPromptSelectorErrorState;
|
||||
|
||||
const factory AiPromptSelectorState.ready({
|
||||
required List<AiPrompt> visiblePrompts,
|
||||
required List<String> favoritePrompts,
|
||||
required bool isCustomPromptSectionSelected,
|
||||
required bool isFeaturedSectionSelected,
|
||||
required bool showCustomPrompts,
|
||||
required bool isFeaturedCategorySelected,
|
||||
required AiPromptCategory? selectedCategory,
|
||||
required String? selectedPromptId,
|
||||
required bool isLoadingCustomPrompts,
|
||||
required CustomPromptDatabaseConfig? databaseConfig,
|
||||
}) = _AiPromptSelectorReadyState;
|
||||
|
||||
bool get isLoading => this is _AiPromptSelectorLoadingState;
|
||||
@@ -412,4 +192,9 @@ class AiPromptSelectorState with _$AiPromptSelectorState {
|
||||
.firstWhereOrNull((prompt) => prompt.id == state.selectedPromptId),
|
||||
orElse: () => null,
|
||||
);
|
||||
|
||||
AiPromptCategory? get selectedCategory => maybeMap(
|
||||
ready: (state) => state.selectedCategory,
|
||||
orElse: () => null,
|
||||
);
|
||||
}
|
||||
@@ -8,9 +8,7 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/ai/operati
|
||||
import 'package:appflowy/shared/list_extension.dart';
|
||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-ai/protobuf.dart'
|
||||
hide CustomPromptDatabaseConfigurationPB;
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart';
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:fixnum/fixnum.dart' as fixnum;
|
||||
@@ -29,7 +27,6 @@ abstract class AIRepository {
|
||||
String? objectId,
|
||||
required String text,
|
||||
PredefinedFormat? format,
|
||||
String? promptId,
|
||||
List<String> sourceIds = const [],
|
||||
List<AiWriterRecord> history = const [],
|
||||
required CompletionTypePB completionType,
|
||||
@@ -44,10 +41,6 @@ abstract class AIRepository {
|
||||
|
||||
Future<List<AiPrompt>> getBuiltInPrompts();
|
||||
|
||||
Future<List<AiPrompt>?> getDatabasePrompts(
|
||||
CustomPromptDatabaseConfigPB config,
|
||||
);
|
||||
|
||||
void updateFavoritePrompts(List<String> promptIds);
|
||||
}
|
||||
|
||||
@@ -57,7 +50,6 @@ class AppFlowyAIService implements AIRepository {
|
||||
String? objectId,
|
||||
required String text,
|
||||
PredefinedFormat? format,
|
||||
String? promptId,
|
||||
List<String> sourceIds = const [],
|
||||
List<AiWriterRecord> history = const [],
|
||||
required CompletionTypePB completionType,
|
||||
@@ -84,7 +76,6 @@ class AppFlowyAIService implements AIRepository {
|
||||
text: text,
|
||||
completionType: completionType,
|
||||
format: format?.toPB(),
|
||||
promptId: promptId,
|
||||
streamPort: fixnum.Int64(stream.nativePort),
|
||||
objectId: objectId ?? '',
|
||||
ragIds: [
|
||||
@@ -126,20 +117,6 @@ class AppFlowyAIService implements AIRepository {
|
||||
return prompts;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<AiPrompt>?> getDatabasePrompts(
|
||||
CustomPromptDatabaseConfigPB config,
|
||||
) async {
|
||||
return DatabaseEventGetDatabaseCustomPrompts(config).send().fold(
|
||||
(databasePromptsPB) =>
|
||||
databasePromptsPB.items.map(AiPrompt.fromPB).toList(),
|
||||
(err) {
|
||||
Log.error(err);
|
||||
return null;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void updateFavoritePrompts(List<String> promptIds) {}
|
||||
}
|
||||
|
||||
+84
-151
@@ -8,8 +8,11 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
class AiPromptCategoryList extends StatefulWidget {
|
||||
const AiPromptCategoryList({
|
||||
super.key,
|
||||
required this.padding,
|
||||
});
|
||||
|
||||
final EdgeInsetsGeometry padding;
|
||||
|
||||
@override
|
||||
State<AiPromptCategoryList> createState() => _AiPromptCategoryListState();
|
||||
}
|
||||
@@ -20,62 +23,60 @@ class _AiPromptCategoryListState extends State<AiPromptCategoryList> {
|
||||
Widget build(BuildContext context) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
|
||||
return TextFieldTapRegion(
|
||||
groupId: "ai_prompt_category_list",
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Padding(
|
||||
padding: EdgeInsets.only(
|
||||
right: theme.spacing.l,
|
||||
),
|
||||
child: AiPromptFeaturedSection(),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(
|
||||
right: theme.spacing.l,
|
||||
),
|
||||
child: AiPromptCustomPromptSection(),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(
|
||||
top: theme.spacing.s,
|
||||
right: theme.spacing.l,
|
||||
),
|
||||
child: AFDivider(),
|
||||
),
|
||||
Expanded(
|
||||
child: ListView(
|
||||
padding: EdgeInsets.only(
|
||||
top: theme.spacing.s,
|
||||
right: theme.spacing.l,
|
||||
),
|
||||
children: [
|
||||
_buildCategoryItem(context, null),
|
||||
...sortedCategories.map(
|
||||
(category) => _buildCategoryItem(
|
||||
return Padding(
|
||||
padding: EdgeInsets.all(
|
||||
theme.spacing.l,
|
||||
),
|
||||
child: TextFieldTapRegion(
|
||||
groupId: "ai_prompt_category_list",
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
spacing: theme.spacing.s,
|
||||
children: [
|
||||
_buildFeaturedCategory(context),
|
||||
const AFDivider(),
|
||||
Expanded(
|
||||
child: ListView(
|
||||
padding: EdgeInsets.zero,
|
||||
children: [
|
||||
_buildCategoryItem(context, null),
|
||||
_buildCategoryItem(context, AiPromptCategory.development),
|
||||
_buildCategoryItem(context, AiPromptCategory.writing),
|
||||
_buildCategoryItem(context, AiPromptCategory.business),
|
||||
_buildCategoryItem(context, AiPromptCategory.marketing),
|
||||
_buildCategoryItem(
|
||||
context,
|
||||
category,
|
||||
AiPromptCategory.healthAndFitness,
|
||||
),
|
||||
),
|
||||
],
|
||||
_buildCategoryItem(context, AiPromptCategory.travel),
|
||||
_buildCategoryItem(context, AiPromptCategory.contentSeo),
|
||||
_buildCategoryItem(context, AiPromptCategory.emailMarketing),
|
||||
_buildCategoryItem(context, AiPromptCategory.paidAds),
|
||||
_buildCategoryItem(context, AiPromptCategory.prCommunication),
|
||||
_buildCategoryItem(context, AiPromptCategory.recruiting),
|
||||
_buildCategoryItem(context, AiPromptCategory.sales),
|
||||
_buildCategoryItem(context, AiPromptCategory.socialMedia),
|
||||
_buildCategoryItem(context, AiPromptCategory.strategy),
|
||||
_buildCategoryItem(context, AiPromptCategory.caseStudies),
|
||||
_buildCategoryItem(context, AiPromptCategory.salesCopy),
|
||||
_buildCategoryItem(context, AiPromptCategory.education),
|
||||
_buildCategoryItem(context, AiPromptCategory.work),
|
||||
_buildCategoryItem(
|
||||
context,
|
||||
AiPromptCategory.podcastProduction,
|
||||
),
|
||||
_buildCategoryItem(context, AiPromptCategory.copyWriting),
|
||||
_buildCategoryItem(context, AiPromptCategory.customerSuccess),
|
||||
_buildCategoryItem(context, AiPromptCategory.other),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static Iterable<AiPromptCategory> get sortedCategories {
|
||||
final categories = [...AiPromptCategory.values];
|
||||
categories
|
||||
..sort((a, b) => a.i18n.compareTo(b.i18n))
|
||||
..remove(AiPromptCategory.other)
|
||||
..add(AiPromptCategory.other);
|
||||
|
||||
return categories;
|
||||
}
|
||||
|
||||
Widget _buildCategoryItem(
|
||||
BuildContext context,
|
||||
AiPromptCategory? category,
|
||||
@@ -87,25 +88,18 @@ class _AiPromptCategoryListState extends State<AiPromptCategoryList> {
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AiPromptFeaturedSection extends StatelessWidget {
|
||||
const AiPromptFeaturedSection({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget _buildFeaturedCategory(BuildContext context) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
final isSelected = context.watch<AiPromptSelectorCubit>().state.maybeMap(
|
||||
ready: (state) => state.isFeaturedSectionSelected,
|
||||
ready: (state) => state.isFeaturedCategorySelected,
|
||||
orElse: () => false,
|
||||
);
|
||||
|
||||
return AFBaseButton(
|
||||
onTap: () {
|
||||
if (!isSelected) {
|
||||
context.read<AiPromptSelectorCubit>().selectFeaturedSection();
|
||||
context.read<AiPromptSelectorCubit>().selectFeaturedCategory();
|
||||
}
|
||||
},
|
||||
builder: (context, isHovering, disabled) {
|
||||
@@ -129,7 +123,7 @@ class AiPromptFeaturedSection extends StatelessWidget {
|
||||
return theme.fillColorScheme.themeSelect;
|
||||
}
|
||||
if (isHovering) {
|
||||
return theme.fillColorScheme.contentHover;
|
||||
return theme.fillColorScheme.primaryAlpha5;
|
||||
}
|
||||
return Colors.transparent;
|
||||
},
|
||||
@@ -137,61 +131,6 @@ class AiPromptFeaturedSection extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class AiPromptCustomPromptSection extends StatelessWidget {
|
||||
const AiPromptCustomPromptSection({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
|
||||
return BlocBuilder<AiPromptSelectorCubit, AiPromptSelectorState>(
|
||||
builder: (context, state) {
|
||||
return state.maybeMap(
|
||||
ready: (readyState) {
|
||||
final isSelected = readyState.isCustomPromptSectionSelected;
|
||||
|
||||
return AFBaseButton(
|
||||
onTap: () {
|
||||
if (!isSelected) {
|
||||
context.read<AiPromptSelectorCubit>().selectCustomSection();
|
||||
}
|
||||
},
|
||||
builder: (context, isHovering, disabled) {
|
||||
return Text(
|
||||
LocaleKeys.ai_customPrompt_custom.tr(),
|
||||
style: AppFlowyTheme.of(context).textStyle.body.standard(
|
||||
color: theme.textColorScheme.primary,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
);
|
||||
},
|
||||
borderRadius: theme.borderRadius.m,
|
||||
padding: EdgeInsets.symmetric(
|
||||
vertical: theme.spacing.s,
|
||||
horizontal: theme.spacing.m,
|
||||
),
|
||||
borderColor: (context, isHovering, disabled, isFocused) =>
|
||||
Colors.transparent,
|
||||
backgroundColor: (context, isHovering, disabled) {
|
||||
if (isSelected) {
|
||||
return theme.fillColorScheme.themeSelect;
|
||||
}
|
||||
if (isHovering) {
|
||||
return theme.fillColorScheme.contentHover;
|
||||
}
|
||||
return Colors.transparent;
|
||||
},
|
||||
);
|
||||
},
|
||||
orElse: () => const SizedBox.shrink(),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AiPromptCategoryItem extends StatelessWidget {
|
||||
const AiPromptCategoryItem({
|
||||
super.key,
|
||||
@@ -204,47 +143,41 @@ class AiPromptCategoryItem extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<AiPromptSelectorCubit, AiPromptSelectorState>(
|
||||
builder: (context, state) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
final isSelected = state.maybeMap(
|
||||
ready: (state) {
|
||||
return !state.isFeaturedSectionSelected &&
|
||||
!state.isCustomPromptSectionSelected &&
|
||||
state.selectedCategory == category;
|
||||
},
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
final isSelected = context.watch<AiPromptSelectorCubit>().state.maybeMap(
|
||||
ready: (state) =>
|
||||
!state.isFeaturedCategorySelected &&
|
||||
state.selectedCategory == category,
|
||||
orElse: () => false,
|
||||
);
|
||||
|
||||
return AFBaseButton(
|
||||
onTap: onSelect,
|
||||
builder: (context, isHovering, disabled) {
|
||||
return Text(
|
||||
category?.i18n ?? LocaleKeys.ai_customPrompt_all.tr(),
|
||||
style: AppFlowyTheme.of(context).textStyle.body.standard(
|
||||
color: theme.textColorScheme.primary,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
);
|
||||
},
|
||||
borderRadius: theme.borderRadius.m,
|
||||
padding: EdgeInsets.symmetric(
|
||||
vertical: theme.spacing.s,
|
||||
horizontal: theme.spacing.m,
|
||||
),
|
||||
borderColor: (context, isHovering, disabled, isFocused) =>
|
||||
Colors.transparent,
|
||||
backgroundColor: (context, isHovering, disabled) {
|
||||
if (isSelected) {
|
||||
return theme.fillColorScheme.themeSelect;
|
||||
}
|
||||
if (isHovering) {
|
||||
return theme.fillColorScheme.contentHover;
|
||||
}
|
||||
return Colors.transparent;
|
||||
},
|
||||
return AFBaseButton(
|
||||
onTap: onSelect,
|
||||
builder: (context, isHovering, disabled) {
|
||||
return Text(
|
||||
category?.i18n ?? LocaleKeys.ai_customPrompt_all.tr(),
|
||||
style: AppFlowyTheme.of(context).textStyle.body.standard(
|
||||
color: theme.textColorScheme.primary,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
);
|
||||
},
|
||||
borderRadius: theme.borderRadius.m,
|
||||
padding: EdgeInsets.symmetric(
|
||||
vertical: theme.spacing.s,
|
||||
horizontal: theme.spacing.m,
|
||||
),
|
||||
borderColor: (context, isHovering, disabled, isFocused) =>
|
||||
Colors.transparent,
|
||||
backgroundColor: (context, isHovering, disabled) {
|
||||
if (isSelected) {
|
||||
return theme.fillColorScheme.themeSelect;
|
||||
}
|
||||
if (isHovering) {
|
||||
return theme.fillColorScheme.primaryAlpha5;
|
||||
}
|
||||
return Colors.transparent;
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
-626
@@ -1,626 +0,0 @@
|
||||
import 'package:appflowy/ai/ai.dart';
|
||||
import 'package:appflowy/ai/service/ai_prompt_database_selector_cubit.dart';
|
||||
import 'package:appflowy/features/workspace/logic/workspace_bloc.dart';
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialog_v2.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
|
||||
import 'package:appflowy_ui/appflowy_ui.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:expandable/expandable.dart';
|
||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
Future<CustomPromptDatabaseConfig?> changeCustomPromptDatabaseConfig(
|
||||
BuildContext context, {
|
||||
CustomPromptDatabaseConfig? config,
|
||||
}) async {
|
||||
return showDialog<CustomPromptDatabaseConfig?>(
|
||||
context: context,
|
||||
builder: (_) {
|
||||
return MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider.value(
|
||||
value: context.read<UserWorkspaceBloc>(),
|
||||
),
|
||||
BlocProvider(
|
||||
create: (context) => AiPromptDatabaseSelectorCubit(
|
||||
configuration: config,
|
||||
),
|
||||
),
|
||||
],
|
||||
child: const AiPromptDatabaseModal(),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
class AiPromptDatabaseModal extends StatefulWidget {
|
||||
const AiPromptDatabaseModal({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
State<AiPromptDatabaseModal> createState() => _AiPromptDatabaseModalState();
|
||||
}
|
||||
|
||||
class _AiPromptDatabaseModalState extends State<AiPromptDatabaseModal> {
|
||||
final expandableController = ExpandableController(initialExpanded: false);
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
expandableController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
|
||||
return BlocListener<AiPromptDatabaseSelectorCubit,
|
||||
AiPromptDatabaseSelectorState>(
|
||||
listener: (context, state) {
|
||||
state.maybeMap(
|
||||
invalidDatabase: (_) {
|
||||
showSimpleAFDialog(
|
||||
context: context,
|
||||
title: LocaleKeys.ai_customPrompt_invalidDatabase.tr(),
|
||||
content: LocaleKeys.ai_customPrompt_invalidDatabaseHelp.tr(),
|
||||
primaryAction: (
|
||||
LocaleKeys.button_ok.tr(),
|
||||
(context) {},
|
||||
),
|
||||
);
|
||||
},
|
||||
empty: (_) => expandableController.expanded = false,
|
||||
selected: (_) => expandableController.expanded = true,
|
||||
orElse: () {},
|
||||
);
|
||||
},
|
||||
child: AFModal(
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: 450,
|
||||
maxHeight: 400,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
AFModalHeader(
|
||||
leading: Text(
|
||||
LocaleKeys.ai_customPrompt_configureDatabase.tr(),
|
||||
style: theme.textStyle.heading4.prominent(
|
||||
color: theme.textColorScheme.primary,
|
||||
),
|
||||
),
|
||||
trailing: [
|
||||
AFGhostButton.normal(
|
||||
onTap: () => Navigator.of(context).pop(),
|
||||
padding: EdgeInsets.all(theme.spacing.xs),
|
||||
builder: (context, isHovering, disabled) {
|
||||
return Center(
|
||||
child: FlowySvg(
|
||||
FlowySvgs.toast_close_s,
|
||||
size: Size.square(20),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
Flexible(
|
||||
child: AFModalBody(
|
||||
child: ExpandablePanel(
|
||||
controller: expandableController,
|
||||
theme: ExpandableThemeData(
|
||||
tapBodyToCollapse: false,
|
||||
hasIcon: false,
|
||||
tapBodyToExpand: false,
|
||||
tapHeaderToExpand: false,
|
||||
),
|
||||
header: const _Header(),
|
||||
collapsed: const SizedBox.shrink(),
|
||||
expanded: const _Expanded(),
|
||||
),
|
||||
),
|
||||
),
|
||||
AFModalFooter(
|
||||
trailing: [
|
||||
AFOutlinedButton.normal(
|
||||
onTap: () => Navigator.of(context).pop(),
|
||||
builder: (context, isHovering, disabled) {
|
||||
return Text(
|
||||
LocaleKeys.button_cancel.tr(),
|
||||
style: theme.textStyle.body.standard(
|
||||
color: theme.textColorScheme.primary,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
AFFilledButton.primary(
|
||||
onTap: () {
|
||||
final config = context
|
||||
.read<AiPromptDatabaseSelectorCubit>()
|
||||
.state
|
||||
.maybeMap(
|
||||
selected: (state) => state.config,
|
||||
orElse: () => null,
|
||||
);
|
||||
Navigator.of(context).pop(config);
|
||||
},
|
||||
builder: (context, isHovering, disabled) {
|
||||
return Text(
|
||||
LocaleKeys.button_done.tr(),
|
||||
style: theme.textStyle.body.enhanced(
|
||||
color: theme.textColorScheme.onFill,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Header extends StatefulWidget {
|
||||
const _Header();
|
||||
|
||||
@override
|
||||
State<_Header> createState() => _HeaderState();
|
||||
}
|
||||
|
||||
class _HeaderState extends State<_Header> {
|
||||
final popoverController = AFPopoverController();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
popoverController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
|
||||
return BlocBuilder<AiPromptDatabaseSelectorCubit,
|
||||
AiPromptDatabaseSelectorState>(
|
||||
builder: (context, state) {
|
||||
bool showNothing = false;
|
||||
String? viewName;
|
||||
state.maybeMap(
|
||||
empty: (_) {
|
||||
showNothing = false;
|
||||
viewName = null;
|
||||
},
|
||||
selected: (selectedState) {
|
||||
showNothing = false;
|
||||
viewName = selectedState.config.view.nameOrDefault;
|
||||
},
|
||||
orElse: () {
|
||||
showNothing = true;
|
||||
viewName = null;
|
||||
},
|
||||
);
|
||||
|
||||
if (showNothing) {
|
||||
return SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: theme.spacing.m,
|
||||
vertical: theme.spacing.xl,
|
||||
),
|
||||
child: Row(
|
||||
spacing: theme.spacing.s,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
LocaleKeys.ai_customPrompt_selectDatabase.tr(),
|
||||
style: theme.textStyle.body.standard(
|
||||
color: theme.textColorScheme.secondary,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: ViewSelector(
|
||||
viewSelectorCubit: BlocProvider(
|
||||
create: (context) => ViewSelectorCubit(
|
||||
getIgnoreViewType: getIgnoreViewType,
|
||||
),
|
||||
),
|
||||
child: BlocSelector<SpaceBloc, SpaceState,
|
||||
(List<ViewPB>, ViewPB?)>(
|
||||
selector: (state) => (state.spaces, state.currentSpace),
|
||||
builder: (context, state) {
|
||||
return AFPopover(
|
||||
controller: popoverController,
|
||||
decoration: BoxDecoration(
|
||||
color: theme.surfaceColorScheme.primary,
|
||||
borderRadius:
|
||||
BorderRadius.circular(theme.borderRadius.l),
|
||||
border: Border.all(
|
||||
color: theme.borderColorScheme.primary,
|
||||
),
|
||||
boxShadow: theme.shadow.medium,
|
||||
),
|
||||
padding: EdgeInsets.zero,
|
||||
anchor: AFAnchor(
|
||||
childAlignment: Alignment.topCenter,
|
||||
overlayAlignment: Alignment.bottomCenter,
|
||||
offset: Offset(0, theme.spacing.xs),
|
||||
),
|
||||
popover: (context) {
|
||||
return _PopoverContent(
|
||||
onSelectViewItem: (item) {
|
||||
context
|
||||
.read<AiPromptDatabaseSelectorCubit>()
|
||||
.selectDatabaseView(item.view.id);
|
||||
popoverController.hide();
|
||||
},
|
||||
);
|
||||
},
|
||||
child: AFOutlinedButton.normal(
|
||||
onTap: () {
|
||||
context
|
||||
.read<ViewSelectorCubit>()
|
||||
.refreshSources(state.$1, state.$2);
|
||||
popoverController.toggle();
|
||||
},
|
||||
builder: (context, isHovering, disabled) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
spacing: theme.spacing.s,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
viewName ??
|
||||
LocaleKeys
|
||||
.ai_customPrompt_selectDatabase
|
||||
.tr(),
|
||||
style: theme.textStyle.body.enhanced(
|
||||
color: theme.textColorScheme.primary,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
FlowySvg(
|
||||
FlowySvgs.toolbar_arrow_down_m,
|
||||
color: theme.iconColorScheme.primary,
|
||||
size: Size(12, 20),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
IgnoreViewType getIgnoreViewType(ViewSelectorItem item) {
|
||||
final layout = item.view.layout;
|
||||
|
||||
if (layout.isDatabaseView) {
|
||||
return IgnoreViewType.none;
|
||||
}
|
||||
if (layout.isDocumentView) {
|
||||
return hasDatabaseDescendent(item)
|
||||
? IgnoreViewType.none
|
||||
: IgnoreViewType.hide;
|
||||
}
|
||||
return IgnoreViewType.hide;
|
||||
}
|
||||
|
||||
bool hasDatabaseDescendent(ViewSelectorItem viewSelectorItem) {
|
||||
final layout = viewSelectorItem.view.layout;
|
||||
|
||||
if (layout == ViewLayoutPB.Chat) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (layout.isDatabaseView) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// document may have children
|
||||
return viewSelectorItem.children.any(
|
||||
(child) => hasDatabaseDescendent(child),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Expanded extends StatelessWidget {
|
||||
const _Expanded();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
|
||||
return BlocBuilder<AiPromptDatabaseSelectorCubit,
|
||||
AiPromptDatabaseSelectorState>(
|
||||
builder: (context, state) {
|
||||
return state.maybeMap(
|
||||
orElse: () => SizedBox.shrink(),
|
||||
selected: (selectedState) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.all(theme.spacing.m),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
spacing: theme.spacing.m,
|
||||
children: [
|
||||
FieldSelector(
|
||||
title: LocaleKeys.ai_customPrompt_title.tr(),
|
||||
currentFieldId: selectedState.config.titleFieldId,
|
||||
isDisabled: true,
|
||||
fields: selectedState.fields,
|
||||
onSelect: (id) {},
|
||||
),
|
||||
FieldSelector(
|
||||
title: LocaleKeys.ai_customPrompt_content.tr(),
|
||||
currentFieldId: selectedState.config.contentFieldId,
|
||||
fields: selectedState.fields
|
||||
.where((f) => f.fieldType == FieldType.RichText)
|
||||
.toList(),
|
||||
onSelect: (id) {
|
||||
if (id != null) {
|
||||
context
|
||||
.read<AiPromptDatabaseSelectorCubit>()
|
||||
.selectContentField(id);
|
||||
}
|
||||
},
|
||||
),
|
||||
FieldSelector(
|
||||
title: LocaleKeys.ai_customPrompt_example.tr(),
|
||||
currentFieldId: selectedState.config.exampleFieldId,
|
||||
isOptional: true,
|
||||
fields: selectedState.fields
|
||||
.where((f) => f.fieldType == FieldType.RichText)
|
||||
.toList(),
|
||||
onSelect: (id) {
|
||||
context
|
||||
.read<AiPromptDatabaseSelectorCubit>()
|
||||
.selectExampleField(id);
|
||||
},
|
||||
),
|
||||
FieldSelector(
|
||||
title: LocaleKeys.ai_customPrompt_category.tr(),
|
||||
currentFieldId: selectedState.config.categoryFieldId,
|
||||
isOptional: true,
|
||||
fields: selectedState.fields
|
||||
.where(
|
||||
(f) =>
|
||||
f.fieldType == FieldType.RichText ||
|
||||
f.fieldType == FieldType.SingleSelect ||
|
||||
f.fieldType == FieldType.MultiSelect,
|
||||
)
|
||||
.toList(),
|
||||
onSelect: (id) {
|
||||
context
|
||||
.read<AiPromptDatabaseSelectorCubit>()
|
||||
.selectCategoryField(id);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PopoverContent extends StatefulWidget {
|
||||
const _PopoverContent({
|
||||
required this.onSelectViewItem,
|
||||
});
|
||||
|
||||
final void Function(ViewSelectorItem item) onSelectViewItem;
|
||||
|
||||
@override
|
||||
State<_PopoverContent> createState() => _PopoverContentState();
|
||||
}
|
||||
|
||||
class _PopoverContentState extends State<_PopoverContent> {
|
||||
final focusNode = FocusNode();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
focusNode.requestFocus();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
focusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
|
||||
return ConstrainedBox(
|
||||
constraints: const BoxConstraints.tightFor(
|
||||
width: 300,
|
||||
height: 400,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
VSpace(
|
||||
theme.spacing.m,
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: theme.spacing.m,
|
||||
),
|
||||
child: AFTextField(
|
||||
focusNode: focusNode,
|
||||
size: AFTextFieldSize.m,
|
||||
hintText: LocaleKeys.search_label.tr(),
|
||||
controller:
|
||||
context.read<ViewSelectorCubit>().filterTextController,
|
||||
),
|
||||
),
|
||||
VSpace(
|
||||
theme.spacing.m,
|
||||
),
|
||||
AFDivider(),
|
||||
Expanded(
|
||||
child: BlocBuilder<ViewSelectorCubit, ViewSelectorState>(
|
||||
builder: (context, state) {
|
||||
return ListView(
|
||||
shrinkWrap: true,
|
||||
padding: const EdgeInsets.fromLTRB(8, 4, 8, 12),
|
||||
children: _buildVisibleSources(context, state).toList(),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Iterable<Widget> _buildVisibleSources(
|
||||
BuildContext context,
|
||||
ViewSelectorState state,
|
||||
) {
|
||||
return state.visibleSources.map(
|
||||
(e) => ViewSelectorTreeItem(
|
||||
key: ValueKey(
|
||||
'custom_prompt_database_tree_item_${e.view.id}',
|
||||
),
|
||||
viewSelectorItem: e,
|
||||
level: 0,
|
||||
isDescendentOfSpace: e.view.isSpace,
|
||||
isSelectedSection: false,
|
||||
showCheckbox: false,
|
||||
onSelected: (item) {
|
||||
if (item.view.isDocument || item.view.isSpace) {
|
||||
context.read<ViewSelectorCubit>().toggleIsExpanded(item, false);
|
||||
return;
|
||||
}
|
||||
widget.onSelectViewItem(item);
|
||||
},
|
||||
height: 30.0,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _FieldPBWrapper extends Equatable with AFDropDownMenuMixin {
|
||||
const _FieldPBWrapper(this.field);
|
||||
|
||||
final FieldPB field;
|
||||
|
||||
@override
|
||||
String get label => field.name;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [field.id];
|
||||
}
|
||||
|
||||
class FieldSelector extends StatelessWidget {
|
||||
const FieldSelector({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.currentFieldId,
|
||||
this.isDisabled = false,
|
||||
this.isOptional = false,
|
||||
this.fields = const [],
|
||||
required this.onSelect,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final String? currentFieldId;
|
||||
final bool isDisabled;
|
||||
final bool isOptional;
|
||||
final List<FieldPB> fields;
|
||||
final void Function(String? id)? onSelect;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
|
||||
final selectedField = fields.firstWhereOrNull(
|
||||
(field) => field.id == currentFieldId,
|
||||
);
|
||||
|
||||
return Row(
|
||||
spacing: theme.spacing.s,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
title,
|
||||
style: theme.textStyle.body.standard(
|
||||
color: theme.textColorScheme.secondary,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: AFDropDownMenu<_FieldPBWrapper>(
|
||||
isDisabled: isDisabled,
|
||||
items: fields.map((field) => _FieldPBWrapper(field)).toList(),
|
||||
selectedItems: [
|
||||
if (selectedField != null) _FieldPBWrapper(selectedField),
|
||||
],
|
||||
clearIcon: selectedField == null ||
|
||||
!fields.contains(selectedField) ||
|
||||
!isOptional
|
||||
? null
|
||||
: MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
onSelect?.call(null);
|
||||
},
|
||||
child: FlowySvg(
|
||||
FlowySvgs.search_clear_m,
|
||||
size: Size.square(16),
|
||||
color: theme.iconColorScheme.tertiary,
|
||||
),
|
||||
),
|
||||
),
|
||||
onSelected: (value) {
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
onSelect?.call(value.field.id);
|
||||
},
|
||||
dropdownIcon: FlowySvg(
|
||||
FlowySvgs.toolbar_arrow_down_m,
|
||||
color: theme.iconColorScheme.primary,
|
||||
size: Size(12, 20),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,6 @@
|
||||
import 'package:appflowy/ai/ai.dart';
|
||||
import 'package:appflowy/features/workspace/logic/workspace_bloc.dart';
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/workspace/application/user/prelude.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialog_v2.dart';
|
||||
import 'package:appflowy_ui/appflowy_ui.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
@@ -11,7 +8,6 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import 'ai_prompt_category_list.dart';
|
||||
import 'ai_prompt_onboarding.dart';
|
||||
import 'ai_prompt_preview.dart';
|
||||
import 'ai_prompt_visible_list.dart';
|
||||
|
||||
@@ -19,20 +15,11 @@ Future<AiPrompt?> showAiPromptModal(
|
||||
BuildContext context, {
|
||||
required AiPromptSelectorCubit aiPromptSelectorCubit,
|
||||
}) async {
|
||||
aiPromptSelectorCubit.loadCustomPrompts();
|
||||
|
||||
return showDialog<AiPrompt?>(
|
||||
context: context,
|
||||
builder: (_) {
|
||||
return MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider.value(
|
||||
value: aiPromptSelectorCubit,
|
||||
),
|
||||
BlocProvider.value(
|
||||
value: context.read<UserWorkspaceBloc>(),
|
||||
),
|
||||
],
|
||||
builder: (context) {
|
||||
return BlocProvider.value(
|
||||
value: aiPromptSelectorCubit,
|
||||
child: const AiPromptModal(),
|
||||
);
|
||||
},
|
||||
@@ -47,127 +34,96 @@ class AiPromptModal extends StatelessWidget {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
|
||||
return AFModal(
|
||||
backgroundColor: theme.backgroundColorScheme.primary,
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: 1200,
|
||||
maxHeight: 800,
|
||||
),
|
||||
child: BlocListener<AiPromptSelectorCubit, AiPromptSelectorState>(
|
||||
listener: (context, state) {
|
||||
state.maybeMap(
|
||||
invalidDatabase: (_) {
|
||||
showLoadPromptFailedDialog(context);
|
||||
},
|
||||
orElse: () {},
|
||||
);
|
||||
},
|
||||
child: Column(
|
||||
children: [
|
||||
AFModalHeader(
|
||||
leading: Text(
|
||||
LocaleKeys.ai_customPrompt_browsePrompts.tr(),
|
||||
style: theme.textStyle.heading4.prominent(
|
||||
color: theme.textColorScheme.primary,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
AFModalHeader(
|
||||
leading: Text(
|
||||
LocaleKeys.ai_customPrompt_browsePrompts.tr(),
|
||||
style: theme.textStyle.heading4.prominent(
|
||||
color: theme.textColorScheme.primary,
|
||||
),
|
||||
trailing: [
|
||||
AFGhostButton.normal(
|
||||
onTap: () => Navigator.of(context).pop(),
|
||||
padding: EdgeInsets.all(theme.spacing.xs),
|
||||
builder: (context, isHovering, disabled) {
|
||||
return Center(
|
||||
child: FlowySvg(
|
||||
FlowySvgs.toast_close_s,
|
||||
size: Size.square(20),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
Expanded(
|
||||
child: AFModalBody(
|
||||
child:
|
||||
BlocBuilder<AiPromptSelectorCubit, AiPromptSelectorState>(
|
||||
builder: (context, state) {
|
||||
return state.maybeMap(
|
||||
loading: (_) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
},
|
||||
ready: (readyState) {
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
const Expanded(
|
||||
child: AiPromptCategoryList(),
|
||||
),
|
||||
if (readyState.isCustomPromptSectionSelected &&
|
||||
readyState.databaseConfig == null)
|
||||
const Expanded(
|
||||
flex: 5,
|
||||
child: Center(
|
||||
child: AiPromptOnboarding(),
|
||||
),
|
||||
)
|
||||
else ...[
|
||||
const Expanded(
|
||||
flex: 2,
|
||||
child: AiPromptVisibleList(),
|
||||
trailing: [
|
||||
AFGhostButton.normal(
|
||||
onTap: () => Navigator.of(context).pop(),
|
||||
padding: EdgeInsets.all(theme.spacing.s),
|
||||
builder: (context, isHovering, disabled) {
|
||||
return Center(
|
||||
child: FlowySvg(
|
||||
FlowySvgs.toast_close_s,
|
||||
size: Size.square(20),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
Expanded(
|
||||
child: AFModalBody(
|
||||
child: BlocBuilder<AiPromptSelectorCubit, AiPromptSelectorState>(
|
||||
builder: (context, state) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
|
||||
return state.map(
|
||||
loading: (_) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
},
|
||||
ready: (_) {
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Expanded(
|
||||
child: AiPromptCategoryList(
|
||||
padding: EdgeInsetsDirectional.only(
|
||||
top: theme.spacing.l,
|
||||
end: theme.spacing.l,
|
||||
),
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: BlocBuilder<AiPromptSelectorCubit,
|
||||
AiPromptSelectorState>(
|
||||
builder: (context, state) {
|
||||
final selectedPrompt = state.maybeMap(
|
||||
ready: (state) {
|
||||
return state.visiblePrompts
|
||||
.firstWhereOrNull(
|
||||
(prompt) =>
|
||||
prompt.id ==
|
||||
state.selectedPromptId,
|
||||
);
|
||||
},
|
||||
orElse: () => null,
|
||||
);
|
||||
if (selectedPrompt == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return AiPromptPreview(
|
||||
prompt: selectedPrompt,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: AiPromptVisibleList(),
|
||||
),
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: BlocBuilder<AiPromptSelectorCubit,
|
||||
AiPromptSelectorState>(
|
||||
builder: (context, state) {
|
||||
final selectedPrompt = state.maybeMap(
|
||||
ready: (state) {
|
||||
return state.visiblePrompts
|
||||
.firstWhereOrNull(
|
||||
(prompt) =>
|
||||
prompt.id == state.selectedPromptId,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
},
|
||||
orElse: () => const SizedBox.shrink(),
|
||||
);
|
||||
},
|
||||
),
|
||||
orElse: () => null,
|
||||
);
|
||||
if (selectedPrompt == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return AiPromptPreview(
|
||||
prompt: selectedPrompt,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void showLoadPromptFailedDialog(
|
||||
BuildContext context,
|
||||
) {
|
||||
showSimpleAFDialog(
|
||||
context: context,
|
||||
title: LocaleKeys.ai_customPrompt_invalidDatabase.tr(),
|
||||
content: LocaleKeys.ai_customPrompt_invalidDatabaseHelp.tr(),
|
||||
primaryAction: (
|
||||
LocaleKeys.button_ok.tr(),
|
||||
(context) {},
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
import 'package:appflowy/ai/ai.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy_ui/appflowy_ui.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import 'ai_prompt_database_modal.dart';
|
||||
|
||||
class AiPromptOnboarding extends StatelessWidget {
|
||||
const AiPromptOnboarding({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
LocaleKeys.ai_customPrompt_customPrompt.tr(),
|
||||
style: theme.textStyle.heading3.standard(
|
||||
color: theme.textColorScheme.primary,
|
||||
),
|
||||
),
|
||||
VSpace(
|
||||
theme.spacing.s,
|
||||
),
|
||||
Text(
|
||||
LocaleKeys.ai_customPrompt_databasePrompts.tr(),
|
||||
style: theme.textStyle.body.standard(
|
||||
color: theme.textColorScheme.secondary,
|
||||
),
|
||||
),
|
||||
VSpace(
|
||||
theme.spacing.xxl,
|
||||
),
|
||||
AFFilledButton.primary(
|
||||
onTap: () async {
|
||||
final config = await changeCustomPromptDatabaseConfig(context);
|
||||
|
||||
if (config != null && context.mounted) {
|
||||
context
|
||||
.read<AiPromptSelectorCubit>()
|
||||
.updateCustomPromptDatabaseConfiguration(config);
|
||||
}
|
||||
},
|
||||
builder: (context, isHovering, disabled) {
|
||||
return Text(
|
||||
LocaleKeys.ai_customPrompt_selectDatabase.tr(),
|
||||
style: theme.textStyle.body.enhanced(
|
||||
color: theme.textColorScheme.onFill,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -22,8 +22,10 @@ class AiPromptPreview extends StatelessWidget {
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: theme.spacing.l,
|
||||
padding: EdgeInsets.only(
|
||||
top: theme.spacing.l,
|
||||
left: theme.spacing.l,
|
||||
right: theme.spacing.l,
|
||||
),
|
||||
child: SelectionContainer.disabled(
|
||||
child: Row(
|
||||
@@ -70,7 +72,7 @@ class AiPromptPreview extends StatelessWidget {
|
||||
if (prompt.example.isNotEmpty) ...[
|
||||
SelectionContainer.disabled(
|
||||
child: Text(
|
||||
LocaleKeys.ai_customPrompt_promptExample.tr(),
|
||||
LocaleKeys.ai_customPrompt_example.tr(),
|
||||
style: theme.textStyle.heading4.standard(
|
||||
color: theme.textColorScheme.primary,
|
||||
),
|
||||
@@ -106,7 +108,7 @@ class _PromptContent extends StatelessWidget {
|
||||
return Container(
|
||||
padding: EdgeInsets.all(theme.spacing.l),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.surfaceContainerColorScheme.layer01,
|
||||
color: theme.fillColorScheme.quaternary,
|
||||
borderRadius: BorderRadius.circular(theme.borderRadius.m),
|
||||
),
|
||||
child: Text.rich(
|
||||
@@ -130,7 +132,7 @@ class _PromptContent extends StatelessWidget {
|
||||
spans.add(
|
||||
TextSpan(
|
||||
text: part,
|
||||
style: TextStyle(color: theme.textColorScheme.featured),
|
||||
style: TextStyle(color: theme.textColorScheme.purple),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
@@ -176,7 +178,7 @@ class _PromptExample extends StatelessWidget {
|
||||
return Container(
|
||||
padding: EdgeInsets.all(theme.spacing.l),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.surfaceContainerColorScheme.layer01,
|
||||
color: theme.fillColorScheme.quaternary,
|
||||
borderRadius: BorderRadius.circular(theme.borderRadius.m),
|
||||
),
|
||||
child: AIMarkdownText(
|
||||
|
||||
+51
-225
@@ -4,16 +4,13 @@ import 'package:appflowy/ai/ai.dart';
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/util/theme_extension.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
||||
import 'package:appflowy_ui/appflowy_ui.dart';
|
||||
import 'package:diffutil_dart/diffutil.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import 'ai_prompt_database_modal.dart';
|
||||
|
||||
const Duration _listItemAnimationDuration = Duration(milliseconds: 150);
|
||||
|
||||
class AiPromptVisibleList extends StatefulWidget {
|
||||
@@ -38,12 +35,12 @@ class _AiPromptVisibleListState extends State<AiPromptVisibleList> {
|
||||
super.initState();
|
||||
cubit = context.read<AiPromptSelectorCubit>();
|
||||
final textController = cubit.filterTextController;
|
||||
filterIsEmpty = textController.text.isEmpty;
|
||||
textController.addListener(handleFilterTextChanged);
|
||||
final prompts = cubit.state.maybeMap(
|
||||
ready: (value) => value.visiblePrompts,
|
||||
orElse: () => <AiPrompt>[],
|
||||
);
|
||||
filterIsEmpty = textController.text.isEmpty;
|
||||
textController.addListener(handleFilterTextChanged);
|
||||
oldList.addAll(prompts);
|
||||
}
|
||||
|
||||
@@ -56,149 +53,56 @@ class _AiPromptVisibleListState extends State<AiPromptVisibleList> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
final spacing = AppFlowyTheme.of(context).spacing;
|
||||
return BlocListener<AiPromptSelectorCubit, AiPromptSelectorState>(
|
||||
listener: (context, state) {
|
||||
final list = state.maybeMap(
|
||||
ready: (state) => state.visiblePrompts,
|
||||
orElse: () => <AiPrompt>[],
|
||||
);
|
||||
handleVisiblePromptListChanged(list);
|
||||
},
|
||||
child: Column(
|
||||
children: [
|
||||
VSpace(spacing.l),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: spacing.l),
|
||||
child: buildSearchField(context),
|
||||
),
|
||||
VSpace(spacing.s),
|
||||
Expanded(
|
||||
child: AnimatedList(
|
||||
controller: scrollController,
|
||||
padding: EdgeInsets.all(spacing.l),
|
||||
key: listKey,
|
||||
initialItemCount: oldList.length,
|
||||
itemBuilder: (context, index, animation) {
|
||||
final cubit = context.read<AiPromptSelectorCubit>();
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
BlocConsumer<AiPromptSelectorCubit, AiPromptSelectorState>(
|
||||
listener: (context, state) {
|
||||
state.maybeMap(
|
||||
ready: (state) {
|
||||
handleVisiblePromptListChanged(state.visiblePrompts);
|
||||
},
|
||||
orElse: () {},
|
||||
);
|
||||
},
|
||||
buildWhen: (p, c) {
|
||||
return p.maybeMap(
|
||||
ready: (pr) => c.maybeMap(
|
||||
ready: (cr) =>
|
||||
pr.databaseConfig?.view.id != cr.databaseConfig?.view.id ||
|
||||
pr.isLoadingCustomPrompts != cr.isLoadingCustomPrompts ||
|
||||
pr.isCustomPromptSectionSelected !=
|
||||
cr.isCustomPromptSectionSelected,
|
||||
orElse: () => false,
|
||||
),
|
||||
orElse: () => true,
|
||||
);
|
||||
},
|
||||
builder: (context, state) {
|
||||
return state.maybeMap(
|
||||
ready: (readyState) {
|
||||
if (!readyState.isCustomPromptSectionSelected) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return Container(
|
||||
margin: EdgeInsets.only(
|
||||
left: theme.spacing.l,
|
||||
right: theme.spacing.l,
|
||||
bottom: theme.spacing.l,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(theme.borderRadius.m),
|
||||
color: theme.surfaceContainerColorScheme.layer01,
|
||||
),
|
||||
padding: EdgeInsets.all(theme.spacing.m),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text.rich(
|
||||
TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text:
|
||||
"${LocaleKeys.ai_customPrompt_promptDatabase.tr()}: ",
|
||||
style: TextStyle(fontWeight: FontWeight.w500),
|
||||
),
|
||||
TextSpan(
|
||||
text: readyState
|
||||
.databaseConfig?.view.nameOrDefault ??
|
||||
"",
|
||||
),
|
||||
],
|
||||
),
|
||||
style: theme.textStyle.body.standard(
|
||||
color: theme.textColorScheme.primary,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
return cubit.state.maybeMap(
|
||||
ready: (state) {
|
||||
final prompt = state.visiblePrompts[index];
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(
|
||||
top: index == 0 ? 0 : spacing.s,
|
||||
bottom: index == state.visiblePrompts.length - 1
|
||||
? 0
|
||||
: spacing.s,
|
||||
),
|
||||
ConstrainedBox(
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: 150,
|
||||
),
|
||||
child: AFOutlinedButton.normal(
|
||||
builder: (context, isHovering, disabled) {
|
||||
return Row(
|
||||
spacing: theme.spacing.s,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (readyState.isLoadingCustomPrompts)
|
||||
buildLoadingIndicator(theme),
|
||||
Flexible(
|
||||
child: Text(
|
||||
readyState.isLoadingCustomPrompts
|
||||
? LocaleKeys.ai_customPrompt_loading
|
||||
.tr()
|
||||
: LocaleKeys.button_change.tr(),
|
||||
maxLines: 1,
|
||||
style: theme.textStyle.body.enhanced(
|
||||
color: theme.textColorScheme.primary,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
onTap: () async {
|
||||
final newConfig =
|
||||
await changeCustomPromptDatabaseConfig(
|
||||
context,
|
||||
config: readyState.databaseConfig,
|
||||
);
|
||||
if (newConfig != null && context.mounted) {
|
||||
context
|
||||
.read<AiPromptSelectorCubit>()
|
||||
.updateCustomPromptDatabaseConfiguration(
|
||||
newConfig,
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
child: _AiPromptListItem(
|
||||
animation: animation,
|
||||
prompt: prompt,
|
||||
isSelected: state.selectedPromptId == prompt.id,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
orElse: () => const SizedBox.shrink(),
|
||||
);
|
||||
},
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: theme.spacing.l),
|
||||
child: buildSearchField(context),
|
||||
),
|
||||
Expanded(
|
||||
child: TextFieldTapRegion(
|
||||
groupId: "ai_prompt_category_list",
|
||||
child: BlocBuilder<AiPromptSelectorCubit, AiPromptSelectorState>(
|
||||
builder: (context, state) {
|
||||
return state.maybeMap(
|
||||
ready: (readyState) {
|
||||
if (readyState.visiblePrompts.isEmpty) {
|
||||
return buildEmptyPrompts();
|
||||
}
|
||||
return buildPromptList();
|
||||
);
|
||||
},
|
||||
orElse: () => const SizedBox.shrink(),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -226,13 +130,10 @@ class _AiPromptVisibleListState extends State<AiPromptVisibleList> {
|
||||
.read<AiPromptSelectorCubit>()
|
||||
.filterTextController
|
||||
.clear(),
|
||||
child: MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: FlowySvg(
|
||||
FlowySvgs.search_clear_m,
|
||||
color: theme.iconColorScheme.tertiary,
|
||||
size: const Size.square(20),
|
||||
),
|
||||
child: FlowySvg(
|
||||
FlowySvgs.toast_close_s,
|
||||
color: theme.textColorScheme.secondary,
|
||||
size: const Size.square(20),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -240,81 +141,6 @@ class _AiPromptVisibleListState extends State<AiPromptVisibleList> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildEmptyPrompts() {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
FlowySvg(
|
||||
FlowySvgs.m_home_search_icon_m,
|
||||
color: theme.iconColorScheme.secondary,
|
||||
size: Size.square(24),
|
||||
),
|
||||
VSpace(theme.spacing.m),
|
||||
Text(
|
||||
LocaleKeys.ai_customPrompt_noResults.tr(),
|
||||
style: theme.textStyle.body
|
||||
.standard(color: theme.textColorScheme.secondary),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildPromptList() {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
|
||||
return AnimatedList(
|
||||
controller: scrollController,
|
||||
padding: EdgeInsets.all(theme.spacing.l),
|
||||
key: listKey,
|
||||
initialItemCount: oldList.length,
|
||||
itemBuilder: (context, index, animation) {
|
||||
return BlocBuilder<AiPromptSelectorCubit, AiPromptSelectorState>(
|
||||
builder: (context, state) {
|
||||
return state.maybeMap(
|
||||
ready: (state) {
|
||||
final prompt = state.visiblePrompts[index];
|
||||
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(
|
||||
top: index == 0 ? 0 : theme.spacing.s,
|
||||
bottom: index == state.visiblePrompts.length - 1
|
||||
? 0
|
||||
: theme.spacing.s,
|
||||
),
|
||||
child: _AiPromptListItem(
|
||||
animation: animation,
|
||||
prompt: prompt,
|
||||
isSelected: state.selectedPromptId == prompt.id,
|
||||
),
|
||||
);
|
||||
},
|
||||
orElse: () => const SizedBox.shrink(),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildLoadingIndicator(AppFlowyThemeData theme) {
|
||||
return SizedBox.square(
|
||||
dimension: 20,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(2.5),
|
||||
child: CircularProgressIndicator(
|
||||
color: theme.iconColorScheme.tertiary,
|
||||
strokeWidth: 2.0,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void handleVisiblePromptListChanged(
|
||||
List<AiPrompt> newList,
|
||||
) {
|
||||
@@ -432,16 +258,16 @@ class _AiPromptListItemState extends State<_AiPromptListItem> {
|
||||
padding: EdgeInsets.all(theme.spacing.m),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(theme.borderRadius.m),
|
||||
color: Colors.transparent,
|
||||
border: Border.all(
|
||||
color: widget.isSelected
|
||||
? isHovering
|
||||
? theme.borderColorScheme.themeThickHover
|
||||
: theme.borderColorScheme.themeThick
|
||||
: isHovering
|
||||
? theme.borderColorScheme.primaryHover
|
||||
: theme.borderColorScheme.primary,
|
||||
? theme.borderColorScheme.greyTertiaryHover
|
||||
: theme.borderColorScheme.greyTertiary,
|
||||
),
|
||||
color: theme.surfaceColorScheme.primary,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
|
||||
@@ -4,7 +4,6 @@ import 'package:appflowy/plugins/ai_chat/application/chat_user_cubit.dart';
|
||||
import 'package:appflowy/plugins/ai_chat/presentation/layout_define.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/util/theme_extension.dart';
|
||||
import 'package:appflowy/workspace/application/command_palette/command_palette_bloc.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
|
||||
import 'package:appflowy_ui/appflowy_ui.dart';
|
||||
import 'package:flowy_infra/file_picker/file_picker_service.dart';
|
||||
@@ -15,13 +14,6 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import 'browse_prompts_button.dart';
|
||||
|
||||
typedef OnPromptInputSubmitted = void Function(
|
||||
String input,
|
||||
PredefinedFormat? predefinedFormat,
|
||||
Map<String, dynamic> metadata,
|
||||
String? promptId,
|
||||
);
|
||||
|
||||
class DesktopPromptInput extends StatefulWidget {
|
||||
const DesktopPromptInput({
|
||||
super.key,
|
||||
@@ -39,7 +31,8 @@ class DesktopPromptInput extends StatefulWidget {
|
||||
final bool isStreaming;
|
||||
final AiPromptInputTextEditingController textController;
|
||||
final void Function() onStopStreaming;
|
||||
final OnPromptInputSubmitted onSubmitted;
|
||||
final void Function(String, PredefinedFormat?, Map<String, dynamic>)
|
||||
onSubmitted;
|
||||
final ValueNotifier<List<String>> selectedSourcesNotifier;
|
||||
final void Function(List<String>) onUpdateSelectedSources;
|
||||
final bool hideDecoration;
|
||||
@@ -64,6 +57,7 @@ class _DesktopPromptInputState extends State<DesktopPromptInput> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
chatUserCubit.fetchUserProfile();
|
||||
|
||||
widget.textController.addListener(handleTextControllerChanged);
|
||||
focusNode
|
||||
@@ -83,7 +77,6 @@ class _DesktopPromptInputState extends State<DesktopPromptInput> {
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
focusNode.requestFocus();
|
||||
checkForAskingAI();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -235,28 +228,6 @@ class _DesktopPromptInputState extends State<DesktopPromptInput> {
|
||||
);
|
||||
}
|
||||
|
||||
void checkForAskingAI() {
|
||||
final paletteBloc = context.read<CommandPaletteBloc?>(),
|
||||
paletteState = paletteBloc?.state;
|
||||
if (paletteBloc == null || paletteState == null) return;
|
||||
final isAskingAI = paletteState.askAI;
|
||||
if (!isAskingAI) return;
|
||||
paletteBloc.add(CommandPaletteEvent.askedAI());
|
||||
final query = paletteState.query ?? '';
|
||||
if (query.isEmpty) return;
|
||||
final sources = (paletteState.askAISources ?? []).map((e) => e.id).toList();
|
||||
final metadata =
|
||||
context.read<AIPromptInputBloc?>()?.consumeMetadata() ?? {};
|
||||
final promptBloc = context.read<AIPromptInputBloc?>();
|
||||
final promptId = promptBloc?.promptId;
|
||||
final promptState = promptBloc?.state;
|
||||
final predefinedFormat = promptState?.predefinedFormat;
|
||||
if (sources.isNotEmpty) {
|
||||
widget.onUpdateSelectedSources(sources);
|
||||
}
|
||||
widget.onSubmitted.call(query, predefinedFormat, metadata, promptId ?? '');
|
||||
}
|
||||
|
||||
void startMentionPageFromButton() {
|
||||
if (overlayController.isShowing) {
|
||||
return;
|
||||
@@ -316,7 +287,6 @@ class _DesktopPromptInputState extends State<DesktopPromptInput> {
|
||||
userInput,
|
||||
showPredefinedFormats ? predefinedFormat : null,
|
||||
metadata,
|
||||
bloc.promptId,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -535,9 +505,9 @@ class _DesktopPromptInputState extends State<DesktopPromptInput> {
|
||||
|
||||
void handleOnSelectPrompt(AiPrompt prompt) {
|
||||
final bloc = context.read<AIPromptInputBloc>();
|
||||
bloc
|
||||
..add(AIPromptInputEvent.updateMentionedViews([]))
|
||||
..add(AIPromptInputEvent.updatePromptId(prompt.id));
|
||||
bloc.add(
|
||||
AIPromptInputEvent.updateMentionedViews([]),
|
||||
);
|
||||
|
||||
final content = AiPromptInputTextEditingController.replace(prompt.content);
|
||||
|
||||
|
||||
+2
-2
@@ -56,9 +56,9 @@ class AiPromptInputTextEditingController extends TextEditingController {
|
||||
TextSpan(
|
||||
text: restore(string),
|
||||
style: theme.textStyle.body.standard().copyWith(
|
||||
color: theme.textColorScheme.featured,
|
||||
color: theme.textColorScheme.purple,
|
||||
backgroundColor:
|
||||
theme.fillColorScheme.featuredThick.withAlpha(51),
|
||||
theme.fillColorScheme.purpleThick.withAlpha(51),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
+39
-104
@@ -1,18 +1,13 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:appflowy/ai/service/view_selector_cubit.dart';
|
||||
import 'package:appflowy/features/workspace/logic/workspace_bloc.dart';
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/base/flowy_search_text_field.dart';
|
||||
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
|
||||
import 'package:appflowy/plugins/ai_chat/application/chat_select_sources_cubit.dart';
|
||||
import 'package:appflowy/plugins/base/drag_handler.dart';
|
||||
import 'package:appflowy/plugins/document/application/document_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
|
||||
import 'package:appflowy_ui/appflowy_ui.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -37,18 +32,7 @@ class PromptInputMobileSelectSourcesButton extends StatefulWidget {
|
||||
|
||||
class _PromptInputMobileSelectSourcesButtonState
|
||||
extends State<PromptInputMobileSelectSourcesButton> {
|
||||
late final cubit = ViewSelectorCubit(
|
||||
maxSelectedParentPageCount: 3,
|
||||
getIgnoreViewType: (item) {
|
||||
if (item.view.isSpace) {
|
||||
return IgnoreViewType.none;
|
||||
}
|
||||
if (item.view.layout != ViewLayoutPB.Document) {
|
||||
return IgnoreViewType.hide;
|
||||
}
|
||||
return IgnoreViewType.none;
|
||||
},
|
||||
);
|
||||
late final cubit = ChatSettingsCubit();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -68,8 +52,6 @@ class _PromptInputMobileSelectSourcesButtonState
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
|
||||
return BlocBuilder<UserWorkspaceBloc, UserWorkspaceState>(
|
||||
builder: (context, state) {
|
||||
final userProfile = context.read<UserWorkspaceBloc>().state.userProfile;
|
||||
@@ -100,26 +82,6 @@ class _PromptInputMobileSelectSourcesButtonState
|
||||
color: Theme.of(context).iconTheme.color,
|
||||
size: const Size.square(20.0),
|
||||
),
|
||||
const HSpace(2.0),
|
||||
ValueListenableBuilder(
|
||||
valueListenable: widget.selectedSourcesNotifier,
|
||||
builder: (context, selectedSourceIds, _) {
|
||||
final documentId =
|
||||
context.read<DocumentBloc?>()?.documentId;
|
||||
final label = documentId != null &&
|
||||
selectedSourceIds.length == 1 &&
|
||||
selectedSourceIds[0] == documentId
|
||||
? LocaleKeys.chat_currentPage.tr()
|
||||
: selectedSourceIds.length.toString();
|
||||
return FlowyText(
|
||||
label,
|
||||
fontSize: 14,
|
||||
figmaLineHeight: 20,
|
||||
color: Theme.of(context).hintColor,
|
||||
);
|
||||
},
|
||||
),
|
||||
const HSpace(2.0),
|
||||
FlowySvg(
|
||||
FlowySvgs.ai_source_drop_down_s,
|
||||
color: Theme.of(context).hintColor,
|
||||
@@ -128,15 +90,12 @@ class _PromptInputMobileSelectSourcesButtonState
|
||||
],
|
||||
),
|
||||
onTap: () async {
|
||||
unawaited(
|
||||
context
|
||||
.read<ViewSelectorCubit>()
|
||||
.refreshSources(state.spaces, state.currentSpace),
|
||||
);
|
||||
|
||||
context
|
||||
.read<ChatSettingsCubit>()
|
||||
.refreshSources(state.spaces, state.currentSpace);
|
||||
await showMobileBottomSheet<void>(
|
||||
context,
|
||||
backgroundColor: theme.surfaceColorScheme.primary,
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
maxChildSize: 0.98,
|
||||
enableDraggableScrollable: true,
|
||||
scrollableWidgetBuilder: (_, scrollController) {
|
||||
@@ -170,24 +129,39 @@ class _PromptInputMobileSelectSourcesButtonState
|
||||
}
|
||||
}
|
||||
|
||||
class _MobileSelectSourcesSheetBody extends StatelessWidget {
|
||||
class _MobileSelectSourcesSheetBody extends StatefulWidget {
|
||||
const _MobileSelectSourcesSheetBody({
|
||||
required this.scrollController,
|
||||
});
|
||||
|
||||
final ScrollController scrollController;
|
||||
|
||||
@override
|
||||
State<_MobileSelectSourcesSheetBody> createState() =>
|
||||
_MobileSelectSourcesSheetBodyState();
|
||||
}
|
||||
|
||||
class _MobileSelectSourcesSheetBodyState
|
||||
extends State<_MobileSelectSourcesSheetBody> {
|
||||
final textController = TextEditingController();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
textController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CustomScrollView(
|
||||
controller: scrollController,
|
||||
controller: widget.scrollController,
|
||||
shrinkWrap: true,
|
||||
slivers: [
|
||||
SliverPersistentHeader(
|
||||
pinned: true,
|
||||
delegate: _Header(
|
||||
child: ColoredBox(
|
||||
color: AppFlowyTheme.of(context).surfaceColorScheme.primary,
|
||||
color: Theme.of(context).cardColor,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
@@ -209,9 +183,10 @@ class _MobileSelectSourcesSheetBody extends StatelessWidget {
|
||||
child: SizedBox(
|
||||
height: 44.0,
|
||||
child: FlowySearchTextField(
|
||||
controller: context
|
||||
.read<ViewSelectorCubit>()
|
||||
.filterTextController,
|
||||
controller: textController,
|
||||
onChanged: (value) => context
|
||||
.read<ChatSettingsCubit>()
|
||||
.updateFilter(value),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -221,67 +196,27 @@ class _MobileSelectSourcesSheetBody extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
BlocBuilder<ViewSelectorCubit, ViewSelectorState>(
|
||||
BlocBuilder<ChatSettingsCubit, ChatSettingsState>(
|
||||
builder: (context, state) {
|
||||
final sources = state.visibleSources
|
||||
.where((e) => e.ignoreStatus != IgnoreViewType.hide);
|
||||
return SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
childCount: state.selectedSources.length,
|
||||
childCount: sources.length,
|
||||
(context, index) {
|
||||
final source = state.selectedSources.elementAt(index);
|
||||
return ViewSelectorTreeItem(
|
||||
key: ValueKey(
|
||||
'selected_select_sources_tree_item_${source.view.id}',
|
||||
),
|
||||
viewSelectorItem: source,
|
||||
level: 0,
|
||||
isDescendentOfSpace: source.view.isSpace,
|
||||
isSelectedSection: true,
|
||||
onSelected: (item) {
|
||||
context
|
||||
.read<ViewSelectorCubit>()
|
||||
.toggleSelectedStatus(item, true);
|
||||
},
|
||||
height: 40.0,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
BlocBuilder<ViewSelectorCubit, ViewSelectorState>(
|
||||
builder: (context, state) {
|
||||
if (state.selectedSources.isNotEmpty &&
|
||||
state.visibleSources.isNotEmpty) {
|
||||
return SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: AFDivider(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return const SliverToBoxAdapter();
|
||||
},
|
||||
),
|
||||
BlocBuilder<ViewSelectorCubit, ViewSelectorState>(
|
||||
builder: (context, state) {
|
||||
return SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
childCount: state.visibleSources.length,
|
||||
(context, index) {
|
||||
final source = state.visibleSources.elementAt(index);
|
||||
return ViewSelectorTreeItem(
|
||||
final source = sources.elementAt(index);
|
||||
return ChatSourceTreeItem(
|
||||
key: ValueKey(
|
||||
'visible_select_sources_tree_item_${source.view.id}',
|
||||
),
|
||||
viewSelectorItem: source,
|
||||
chatSource: source,
|
||||
level: 0,
|
||||
isDescendentOfSpace: source.view.isSpace,
|
||||
isSelectedSection: false,
|
||||
onSelected: (item) {
|
||||
onSelected: (chatSource) {
|
||||
context
|
||||
.read<ViewSelectorCubit>()
|
||||
.toggleSelectedStatus(item, false);
|
||||
.read<ChatSettingsCubit>()
|
||||
.toggleSelectedStatus(chatSource);
|
||||
},
|
||||
height: 40.0,
|
||||
);
|
||||
|
||||
@@ -2,13 +2,15 @@ import 'dart:math';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/ai_chat/application/chat_select_sources_cubit.dart';
|
||||
import 'package:appflowy/plugins/document/application/document_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.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/view.pb.dart';
|
||||
import 'package:appflowy_ui/appflowy_ui.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
@@ -16,8 +18,6 @@ import 'package:flowy_infra_ui/style_widget/hover.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../../service/view_selector_cubit.dart';
|
||||
import '../view_selector.dart';
|
||||
import 'layout_define.dart';
|
||||
import 'mention_page_menu.dart';
|
||||
|
||||
@@ -38,24 +38,7 @@ class PromptInputDesktopSelectSourcesButton extends StatefulWidget {
|
||||
|
||||
class _PromptInputDesktopSelectSourcesButtonState
|
||||
extends State<PromptInputDesktopSelectSourcesButton> {
|
||||
late final cubit = ViewSelectorCubit(
|
||||
maxSelectedParentPageCount: 3,
|
||||
getIgnoreViewType: (item) {
|
||||
final view = item.view;
|
||||
|
||||
if (view.isSpace) {
|
||||
return IgnoreViewType.none;
|
||||
}
|
||||
if (view.layout == ViewLayoutPB.Chat) {
|
||||
return IgnoreViewType.hide;
|
||||
}
|
||||
if (view.layout != ViewLayoutPB.Document) {
|
||||
return IgnoreViewType.disable;
|
||||
}
|
||||
|
||||
return IgnoreViewType.none;
|
||||
},
|
||||
);
|
||||
late final cubit = ChatSettingsCubit();
|
||||
final popoverController = PopoverController();
|
||||
|
||||
@override
|
||||
@@ -76,10 +59,23 @@ class _PromptInputDesktopSelectSourcesButtonState
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ViewSelector(
|
||||
viewSelectorCubit: BlocProvider.value(
|
||||
value: cubit,
|
||||
),
|
||||
final userWorkspaceBloc = context.read<UserWorkspaceBloc>();
|
||||
final userProfile = userWorkspaceBloc.state.userProfile;
|
||||
final workspaceId =
|
||||
userWorkspaceBloc.state.currentWorkspace?.workspaceId ?? '';
|
||||
|
||||
return MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider(
|
||||
create: (context) => SpaceBloc(
|
||||
userProfile: userProfile,
|
||||
workspaceId: workspaceId,
|
||||
)..add(const SpaceEvent.initial(openFirstPage: false)),
|
||||
),
|
||||
BlocProvider.value(
|
||||
value: cubit,
|
||||
),
|
||||
],
|
||||
child: BlocBuilder<SpaceBloc, SpaceState>(
|
||||
builder: (context, state) {
|
||||
return AppFlowyPopover(
|
||||
@@ -90,18 +86,18 @@ class _PromptInputDesktopSelectSourcesButtonState
|
||||
controller: popoverController,
|
||||
onOpen: () {
|
||||
context
|
||||
.read<ViewSelectorCubit>()
|
||||
.read<ChatSettingsCubit>()
|
||||
.refreshSources(state.spaces, state.currentSpace);
|
||||
},
|
||||
onClose: () {
|
||||
widget.onUpdateSelectedSources(cubit.selectedSourceIds);
|
||||
context
|
||||
.read<ViewSelectorCubit>()
|
||||
.read<ChatSettingsCubit>()
|
||||
.refreshSources(state.spaces, state.currentSpace);
|
||||
},
|
||||
popupBuilder: (_) {
|
||||
return BlocProvider.value(
|
||||
value: context.read<ViewSelectorCubit>(),
|
||||
value: context.read<ChatSettingsCubit>(),
|
||||
child: const _PopoverContent(),
|
||||
);
|
||||
},
|
||||
@@ -190,26 +186,20 @@ class _PopoverContent extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<ViewSelectorCubit, ViewSelectorState>(
|
||||
return BlocBuilder<ChatSettingsCubit, ChatSettingsState>(
|
||||
builder: (context, state) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(8, 12, 8, 8),
|
||||
child: AFTextField(
|
||||
size: AFTextFieldSize.m,
|
||||
controller:
|
||||
context.read<ViewSelectorCubit>().filterTextController,
|
||||
hintText: LocaleKeys.search_label.tr(),
|
||||
child: SpaceSearchField(
|
||||
width: 600,
|
||||
onSearch: (context, value) =>
|
||||
context.read<ChatSettingsCubit>().updateFilter(value),
|
||||
),
|
||||
),
|
||||
AFDivider(
|
||||
startIndent: theme.spacing.l,
|
||||
endIndent: theme.spacing.l,
|
||||
),
|
||||
_buildDivider(),
|
||||
Flexible(
|
||||
child: ListView(
|
||||
shrinkWrap: true,
|
||||
@@ -218,10 +208,9 @@ class _PopoverContent extends StatelessWidget {
|
||||
..._buildSelectedSources(context, state),
|
||||
if (state.selectedSources.isNotEmpty &&
|
||||
state.visibleSources.isNotEmpty)
|
||||
AFDivider(
|
||||
spacing: 4.0,
|
||||
startIndent: theme.spacing.l,
|
||||
endIndent: theme.spacing.l,
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4.0),
|
||||
child: _buildDivider(),
|
||||
),
|
||||
..._buildVisibleSources(context, state),
|
||||
],
|
||||
@@ -233,53 +222,70 @@ class _PopoverContent extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDivider() {
|
||||
return const Divider(
|
||||
height: 1.0,
|
||||
thickness: 1.0,
|
||||
indent: 8.0,
|
||||
endIndent: 8.0,
|
||||
);
|
||||
}
|
||||
|
||||
Iterable<Widget> _buildSelectedSources(
|
||||
BuildContext context,
|
||||
ViewSelectorState state,
|
||||
ChatSettingsState state,
|
||||
) {
|
||||
return state.selectedSources.map(
|
||||
(e) => ViewSelectorTreeItem(
|
||||
key: ValueKey(
|
||||
'selected_select_sources_tree_item_${e.view.id}',
|
||||
),
|
||||
viewSelectorItem: e,
|
||||
level: 0,
|
||||
isDescendentOfSpace: e.view.isSpace,
|
||||
isSelectedSection: true,
|
||||
onSelected: (item) {
|
||||
context.read<ViewSelectorCubit>().toggleSelectedStatus(item, true);
|
||||
},
|
||||
height: 30.0,
|
||||
),
|
||||
);
|
||||
return state.selectedSources
|
||||
.where((e) => e.ignoreStatus != IgnoreViewType.hide)
|
||||
.map(
|
||||
(e) => ChatSourceTreeItem(
|
||||
key: ValueKey(
|
||||
'selected_select_sources_tree_item_${e.view.id}',
|
||||
),
|
||||
chatSource: e,
|
||||
level: 0,
|
||||
isDescendentOfSpace: e.view.isSpace,
|
||||
isSelectedSection: true,
|
||||
onSelected: (chatSource) {
|
||||
context
|
||||
.read<ChatSettingsCubit>()
|
||||
.toggleSelectedStatus(chatSource);
|
||||
},
|
||||
height: 30.0,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Iterable<Widget> _buildVisibleSources(
|
||||
BuildContext context,
|
||||
ViewSelectorState state,
|
||||
ChatSettingsState state,
|
||||
) {
|
||||
return state.visibleSources.map(
|
||||
(e) => ViewSelectorTreeItem(
|
||||
key: ValueKey(
|
||||
'visible_select_sources_tree_item_${e.view.id}',
|
||||
),
|
||||
viewSelectorItem: e,
|
||||
level: 0,
|
||||
isDescendentOfSpace: e.view.isSpace,
|
||||
isSelectedSection: false,
|
||||
onSelected: (item) {
|
||||
context.read<ViewSelectorCubit>().toggleSelectedStatus(item, false);
|
||||
},
|
||||
height: 30.0,
|
||||
),
|
||||
);
|
||||
return state.visibleSources
|
||||
.where((e) => e.ignoreStatus != IgnoreViewType.hide)
|
||||
.map(
|
||||
(e) => ChatSourceTreeItem(
|
||||
key: ValueKey(
|
||||
'visible_select_sources_tree_item_${e.view.id}',
|
||||
),
|
||||
chatSource: e,
|
||||
level: 0,
|
||||
isDescendentOfSpace: e.view.isSpace,
|
||||
isSelectedSection: false,
|
||||
onSelected: (chatSource) {
|
||||
context
|
||||
.read<ChatSettingsCubit>()
|
||||
.toggleSelectedStatus(chatSource);
|
||||
},
|
||||
height: 30.0,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ViewSelectorTreeItem extends StatefulWidget {
|
||||
const ViewSelectorTreeItem({
|
||||
class ChatSourceTreeItem extends StatefulWidget {
|
||||
const ChatSourceTreeItem({
|
||||
super.key,
|
||||
required this.viewSelectorItem,
|
||||
required this.chatSource,
|
||||
required this.level,
|
||||
required this.isDescendentOfSpace,
|
||||
required this.isSelectedSection,
|
||||
@@ -290,7 +296,7 @@ class ViewSelectorTreeItem extends StatefulWidget {
|
||||
this.showCheckbox = true,
|
||||
});
|
||||
|
||||
final ViewSelectorItem viewSelectorItem;
|
||||
final ChatSource chatSource;
|
||||
|
||||
/// nested level of the view item
|
||||
final int level;
|
||||
@@ -299,9 +305,9 @@ class ViewSelectorTreeItem extends StatefulWidget {
|
||||
|
||||
final bool isSelectedSection;
|
||||
|
||||
final void Function(ViewSelectorItem viewSelectorItem) onSelected;
|
||||
final void Function(ChatSource chatSource) onSelected;
|
||||
|
||||
final void Function(ViewSelectorItem viewSelectorItem)? onAdd;
|
||||
final void Function(ChatSource chatSource)? onAdd;
|
||||
|
||||
final bool showSaveButton;
|
||||
|
||||
@@ -310,16 +316,16 @@ class ViewSelectorTreeItem extends StatefulWidget {
|
||||
final bool showCheckbox;
|
||||
|
||||
@override
|
||||
State<ViewSelectorTreeItem> createState() => _ViewSelectorTreeItemState();
|
||||
State<ChatSourceTreeItem> createState() => _ChatSourceTreeItemState();
|
||||
}
|
||||
|
||||
class _ViewSelectorTreeItemState extends State<ViewSelectorTreeItem> {
|
||||
class _ChatSourceTreeItemState extends State<ChatSourceTreeItem> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final child = SizedBox(
|
||||
height: widget.height,
|
||||
child: ViewSelectorTreeItemInner(
|
||||
viewSelectorItem: widget.viewSelectorItem,
|
||||
child: ChatSourceTreeItemInner(
|
||||
chatSource: widget.chatSource,
|
||||
level: widget.level,
|
||||
isDescendentOfSpace: widget.isDescendentOfSpace,
|
||||
isSelectedSection: widget.isSelectedSection,
|
||||
@@ -330,30 +336,33 @@ class _ViewSelectorTreeItemState extends State<ViewSelectorTreeItem> {
|
||||
),
|
||||
);
|
||||
|
||||
final disabledEnabledChild = widget.viewSelectorItem.isDisabled
|
||||
? FlowyTooltip(
|
||||
message: widget.showCheckbox
|
||||
? switch (widget.viewSelectorItem.view.layout) {
|
||||
ViewLayoutPB.Document =>
|
||||
LocaleKeys.chat_sourcesLimitReached.tr(),
|
||||
_ => LocaleKeys.chat_sourceUnsupported.tr(),
|
||||
}
|
||||
: "",
|
||||
child: Opacity(
|
||||
opacity: 0.5,
|
||||
child: MouseRegion(
|
||||
cursor: SystemMouseCursors.forbidden,
|
||||
child: IgnorePointer(child: child),
|
||||
),
|
||||
),
|
||||
)
|
||||
: child;
|
||||
final disabledEnabledChild =
|
||||
widget.chatSource.ignoreStatus == IgnoreViewType.disable
|
||||
? FlowyTooltip(
|
||||
message: widget.showCheckbox
|
||||
? switch (widget.chatSource.view.layout) {
|
||||
ViewLayoutPB.Document =>
|
||||
LocaleKeys.chat_sourcesLimitReached.tr(),
|
||||
_ => LocaleKeys.chat_sourceUnsupported.tr(),
|
||||
}
|
||||
: "",
|
||||
child: Opacity(
|
||||
opacity: 0.5,
|
||||
child: MouseRegion(
|
||||
cursor: SystemMouseCursors.forbidden,
|
||||
child: IgnorePointer(child: child),
|
||||
),
|
||||
),
|
||||
)
|
||||
: child;
|
||||
|
||||
return ValueListenableBuilder(
|
||||
valueListenable: widget.viewSelectorItem.isExpandedNotifier,
|
||||
valueListenable: widget.chatSource.isExpandedNotifier,
|
||||
builder: (context, isExpanded, child) {
|
||||
// filter the child views that should be ignored
|
||||
final childViews = widget.viewSelectorItem.children;
|
||||
final childViews = widget.chatSource.children
|
||||
.where((e) => e.ignoreStatus != IgnoreViewType.hide)
|
||||
.toList();
|
||||
|
||||
if (!isExpanded || childViews.isEmpty) {
|
||||
return disabledEnabledChild;
|
||||
@@ -364,11 +373,11 @@ class _ViewSelectorTreeItemState extends State<ViewSelectorTreeItem> {
|
||||
children: [
|
||||
disabledEnabledChild,
|
||||
...childViews.map(
|
||||
(childSource) => ViewSelectorTreeItem(
|
||||
(childSource) => ChatSourceTreeItem(
|
||||
key: ValueKey(
|
||||
'select_sources_tree_item_${childSource.view.id}',
|
||||
),
|
||||
viewSelectorItem: childSource,
|
||||
chatSource: childSource,
|
||||
level: widget.level + 1,
|
||||
isDescendentOfSpace: widget.isDescendentOfSpace,
|
||||
isSelectedSection: widget.isSelectedSection,
|
||||
@@ -386,10 +395,10 @@ class _ViewSelectorTreeItemState extends State<ViewSelectorTreeItem> {
|
||||
}
|
||||
}
|
||||
|
||||
class ViewSelectorTreeItemInner extends StatelessWidget {
|
||||
const ViewSelectorTreeItemInner({
|
||||
class ChatSourceTreeItemInner extends StatelessWidget {
|
||||
const ChatSourceTreeItemInner({
|
||||
super.key,
|
||||
required this.viewSelectorItem,
|
||||
required this.chatSource,
|
||||
required this.level,
|
||||
required this.isDescendentOfSpace,
|
||||
required this.isSelectedSection,
|
||||
@@ -399,29 +408,34 @@ class ViewSelectorTreeItemInner extends StatelessWidget {
|
||||
this.onAdd,
|
||||
});
|
||||
|
||||
final ViewSelectorItem viewSelectorItem;
|
||||
final ChatSource chatSource;
|
||||
final int level;
|
||||
final bool isDescendentOfSpace;
|
||||
final bool isSelectedSection;
|
||||
final bool showCheckbox;
|
||||
final bool showSaveButton;
|
||||
final void Function(ViewSelectorItem)? onSelected;
|
||||
final void Function(ViewSelectorItem)? onAdd;
|
||||
final void Function(ChatSource)? onSelected;
|
||||
final void Function(ChatSource)? onAdd;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: () => onSelected?.call(viewSelectorItem),
|
||||
onTap: () {
|
||||
if (!isSelectedSection) {
|
||||
onSelected?.call(chatSource);
|
||||
}
|
||||
},
|
||||
child: FlowyHover(
|
||||
cursor: isSelectedSection ? SystemMouseCursors.basic : null,
|
||||
style: HoverStyle(
|
||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||
hoverColor: isSelectedSection
|
||||
? Colors.transparent
|
||||
: AFThemeExtension.of(context).lightGreyHover,
|
||||
),
|
||||
builder: (context, onHover) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
|
||||
final isSaveButtonVisible =
|
||||
showSaveButton && !viewSelectorItem.view.isSpace;
|
||||
showSaveButton && !chatSource.view.isSpace;
|
||||
final isAddButtonVisible = onAdd != null;
|
||||
return Row(
|
||||
children: [
|
||||
@@ -429,30 +443,29 @@ class ViewSelectorTreeItemInner extends StatelessWidget {
|
||||
HSpace(max(20.0 * level - (isDescendentOfSpace ? 2 : 0), 0)),
|
||||
// builds the >, ^ or · button
|
||||
ToggleIsExpandedButton(
|
||||
viewSelectorItem: viewSelectorItem,
|
||||
chatSource: chatSource,
|
||||
isSelectedSection: isSelectedSection,
|
||||
),
|
||||
const HSpace(2.0),
|
||||
// checkbox
|
||||
if (!viewSelectorItem.view.isSpace && showCheckbox) ...[
|
||||
if (!chatSource.view.isSpace && showCheckbox) ...[
|
||||
SourceSelectedStatusCheckbox(
|
||||
viewSelectorItem: viewSelectorItem,
|
||||
chatSource: chatSource,
|
||||
),
|
||||
const HSpace(4.0),
|
||||
],
|
||||
// icon
|
||||
MentionViewIcon(
|
||||
view: viewSelectorItem.view,
|
||||
view: chatSource.view,
|
||||
),
|
||||
const HSpace(6.0),
|
||||
// title
|
||||
Expanded(
|
||||
child: Text(
|
||||
viewSelectorItem.view.nameOrDefault,
|
||||
child: FlowyText(
|
||||
chatSource.view.nameOrDefault,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.textStyle.body.standard(
|
||||
color: theme.textColorScheme.primary,
|
||||
),
|
||||
fontSize: 14.0,
|
||||
figmaLineHeight: 18.0,
|
||||
),
|
||||
),
|
||||
if (onHover && (isSaveButtonVisible || isAddButtonVisible)) ...[
|
||||
@@ -466,7 +479,7 @@ class ViewSelectorTreeItemInner extends StatelessWidget {
|
||||
size: const Size.square(16),
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
onPressed: () => onSelected?.call(viewSelectorItem),
|
||||
onPressed: () => onSelected?.call(chatSource),
|
||||
),
|
||||
if (isSaveButtonVisible && isAddButtonVisible)
|
||||
const HSpace(4.0),
|
||||
@@ -479,7 +492,7 @@ class ViewSelectorTreeItemInner extends StatelessWidget {
|
||||
size: const Size.square(16),
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
onPressed: () => onAdd?.call(viewSelectorItem),
|
||||
onPressed: () => onAdd?.call(chatSource),
|
||||
),
|
||||
const HSpace(4.0),
|
||||
],
|
||||
@@ -494,30 +507,27 @@ class ViewSelectorTreeItemInner extends StatelessWidget {
|
||||
class ToggleIsExpandedButton extends StatelessWidget {
|
||||
const ToggleIsExpandedButton({
|
||||
super.key,
|
||||
required this.viewSelectorItem,
|
||||
required this.chatSource,
|
||||
required this.isSelectedSection,
|
||||
});
|
||||
|
||||
final ViewSelectorItem viewSelectorItem;
|
||||
final ChatSource chatSource;
|
||||
final bool isSelectedSection;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (isReferencedDatabaseView(
|
||||
viewSelectorItem.view,
|
||||
viewSelectorItem.parentView,
|
||||
)) {
|
||||
if (isReferencedDatabaseView(chatSource.view, chatSource.parentView)) {
|
||||
return const _DotIconWidget();
|
||||
}
|
||||
|
||||
if (viewSelectorItem.children.isEmpty) {
|
||||
if (chatSource.children.isEmpty) {
|
||||
return const SizedBox.square(dimension: 16.0);
|
||||
}
|
||||
|
||||
return FlowyHover(
|
||||
child: GestureDetector(
|
||||
child: ValueListenableBuilder(
|
||||
valueListenable: viewSelectorItem.isExpandedNotifier,
|
||||
valueListenable: chatSource.isExpandedNotifier,
|
||||
builder: (context, value, _) => FlowySvg(
|
||||
value
|
||||
? FlowySvgs.view_item_expand_s
|
||||
@@ -526,8 +536,8 @@ class ToggleIsExpandedButton extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
onTap: () => context
|
||||
.read<ViewSelectorCubit>()
|
||||
.toggleIsExpanded(viewSelectorItem, isSelectedSection),
|
||||
.read<ChatSettingsCubit>()
|
||||
.toggleIsExpanded(chatSource, isSelectedSection),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -555,20 +565,20 @@ class _DotIconWidget extends StatelessWidget {
|
||||
class SourceSelectedStatusCheckbox extends StatelessWidget {
|
||||
const SourceSelectedStatusCheckbox({
|
||||
super.key,
|
||||
required this.viewSelectorItem,
|
||||
required this.chatSource,
|
||||
});
|
||||
|
||||
final ViewSelectorItem viewSelectorItem;
|
||||
final ChatSource chatSource;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ValueListenableBuilder(
|
||||
valueListenable: viewSelectorItem.selectedStatusNotifier,
|
||||
valueListenable: chatSource.selectedStatusNotifier,
|
||||
builder: (context, selectedStatus, _) => FlowySvg(
|
||||
switch (selectedStatus) {
|
||||
ViewSelectedStatus.unselected => FlowySvgs.uncheck_s,
|
||||
ViewSelectedStatus.selected => FlowySvgs.check_filled_s,
|
||||
ViewSelectedStatus.partiallySelected => FlowySvgs.check_partial_s,
|
||||
SourceSelectedStatus.unselected => FlowySvgs.uncheck_s,
|
||||
SourceSelectedStatus.selected => FlowySvgs.check_filled_s,
|
||||
SourceSelectedStatus.partiallySelected => FlowySvgs.check_partial_s,
|
||||
},
|
||||
size: const Size.square(18.0),
|
||||
blendMode: null,
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
import 'package:appflowy/features/workspace/logic/workspace_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/user/prelude.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:provider/single_child_widget.dart';
|
||||
|
||||
class ViewSelector extends StatelessWidget {
|
||||
const ViewSelector({
|
||||
super.key,
|
||||
required this.viewSelectorCubit,
|
||||
required this.child,
|
||||
});
|
||||
|
||||
final SingleChildWidget viewSelectorCubit;
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final userWorkspaceBloc = context.read<UserWorkspaceBloc>();
|
||||
final userProfile = userWorkspaceBloc.state.userProfile;
|
||||
final workspaceId =
|
||||
userWorkspaceBloc.state.currentWorkspace?.workspaceId ?? '';
|
||||
|
||||
return MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider(
|
||||
create: (context) {
|
||||
return SpaceBloc(
|
||||
userProfile: userProfile,
|
||||
workspaceId: workspaceId,
|
||||
)..add(const SpaceEvent.initial(openFirstPage: false));
|
||||
},
|
||||
),
|
||||
viewSelectorCubit,
|
||||
],
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2,9 +2,7 @@ import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
abstract class KeyValueStorage {
|
||||
Future<void> set(String key, String value);
|
||||
Future<void> setBool(String key, bool value);
|
||||
Future<String?> get(String key);
|
||||
Future<bool?> getBool(String key);
|
||||
Future<T?> getWithFormat<T>(
|
||||
String key,
|
||||
T Function(String value) formatter,
|
||||
@@ -54,12 +52,6 @@ class DartKeyValue implements KeyValueStorage {
|
||||
await sharedPreferences.setString(key, value);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setBool(String key, bool value) async {
|
||||
await _initSharedPreferencesIfNeeded();
|
||||
await sharedPreferences.setBool(key, value);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> clear() async {
|
||||
await _initSharedPreferencesIfNeeded();
|
||||
@@ -70,11 +62,4 @@ class DartKeyValue implements KeyValueStorage {
|
||||
Future<void> _initSharedPreferencesIfNeeded() async {
|
||||
_sharedPreferences ??= await SharedPreferences.getInstance();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool?> getBool(String key) async {
|
||||
await _initSharedPreferencesIfNeeded();
|
||||
final value = sharedPreferences.getBool(key);
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -120,14 +120,4 @@ class KVKeys {
|
||||
///
|
||||
/// The value is a json list of id
|
||||
static const String compactModeIds = 'compactModeIds';
|
||||
|
||||
/// The key for saving "send notification" toggle value for @ menu
|
||||
///
|
||||
/// The value is bool value
|
||||
static const String atMenuSendNotification = 'atMenuSendNotification';
|
||||
|
||||
/// v0.9.4: has the user clicked the upgrade to pro button
|
||||
/// The value is a boolean string
|
||||
static const String hasClickedUpgradeToProButton =
|
||||
'hasClickedUpgradeToProButton';
|
||||
}
|
||||
-24
@@ -1,24 +0,0 @@
|
||||
import 'package:appflowy/features/mension_person/data/models/models.dart';
|
||||
|
||||
class PersonListCache {
|
||||
final Map<String, List<Person>> _cache = {};
|
||||
|
||||
void updatePersonList(String workspaceId, List<Person> persons) {
|
||||
_cache[workspaceId] = List.of(persons);
|
||||
}
|
||||
|
||||
List<Person>? getPersons(String workspaceId) {
|
||||
final persons = _cache[workspaceId];
|
||||
if (persons == null) return null;
|
||||
return List.of(persons);
|
||||
}
|
||||
|
||||
void updatePerson(String workspaceId, Person person) {
|
||||
final persons = _cache[workspaceId];
|
||||
if (persons == null) return;
|
||||
final index = persons.indexWhere((p) => p.id == person.id);
|
||||
if (index == -1) return;
|
||||
persons[index] = person;
|
||||
_cache[workspaceId] = persons;
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'person.dart';
|
||||
|
||||
class InviteInfo {
|
||||
InviteInfo({
|
||||
required this.email,
|
||||
this.role = PersonRole.member,
|
||||
this.contactDetail,
|
||||
});
|
||||
|
||||
final String email;
|
||||
final PersonRole role;
|
||||
final ContactDetail? contactDetail;
|
||||
|
||||
InviteInfo copyWith({
|
||||
String? email,
|
||||
PersonRole? role,
|
||||
ValueGetter<ContactDetail?>? contactDetail,
|
||||
}) {
|
||||
return InviteInfo(
|
||||
email: email ?? this.email,
|
||||
role: role ?? this.role,
|
||||
contactDetail:
|
||||
contactDetail != null ? contactDetail() : this.contactDetail,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ContactDetail {
|
||||
ContactDetail({
|
||||
this.name = '',
|
||||
this.description = '',
|
||||
});
|
||||
|
||||
final String name;
|
||||
final String description;
|
||||
|
||||
ContactDetail copyWith({
|
||||
String? name,
|
||||
String? description,
|
||||
}) {
|
||||
return ContactDetail(
|
||||
name: name ?? this.name,
|
||||
description: description ?? this.description,
|
||||
);
|
||||
}
|
||||
}
|
||||
-43
@@ -1,43 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class MentionMenuItem {
|
||||
MentionMenuItem({required this.onExecute, required this.id});
|
||||
|
||||
final String id;
|
||||
final VoidCallback onExecute;
|
||||
}
|
||||
|
||||
enum MentionMenuType {
|
||||
person,
|
||||
page,
|
||||
dateAndReminder,
|
||||
}
|
||||
|
||||
class MentionItemMap {
|
||||
final Map<MentionMenuType, List<MentionMenuItem>> _map = {
|
||||
MentionMenuType.person: [],
|
||||
MentionMenuType.page: [],
|
||||
MentionMenuType.dateAndReminder: [],
|
||||
};
|
||||
|
||||
void addToPerson(MentionMenuItem item) {
|
||||
final items = _map[MentionMenuType.person] ?? [];
|
||||
items.add(item);
|
||||
_map[MentionMenuType.person] = items;
|
||||
}
|
||||
|
||||
void addToPage(MentionMenuItem item) {
|
||||
final items = _map[MentionMenuType.page] ?? [];
|
||||
items.add(item);
|
||||
_map[MentionMenuType.page] = items;
|
||||
}
|
||||
|
||||
void addToDateAndReminder(MentionMenuItem item) {
|
||||
final items = _map[MentionMenuType.dateAndReminder] ?? [];
|
||||
items.add(item);
|
||||
_map[MentionMenuType.dateAndReminder] = items;
|
||||
}
|
||||
|
||||
List<MentionMenuItem> get items =>
|
||||
_map.values.expand((items) => items).toList();
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export 'person.dart';
|
||||
export 'mention_menu_item.dart';
|
||||
export 'invite.dart';
|
||||
@@ -1,34 +0,0 @@
|
||||
class Person {
|
||||
Person({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.email,
|
||||
required this.role,
|
||||
this.avatarUrl,
|
||||
this.coverImageUrl,
|
||||
this.description,
|
||||
this.invited,
|
||||
});
|
||||
|
||||
final String id;
|
||||
final String name;
|
||||
final String email;
|
||||
final PersonRole role;
|
||||
final String? avatarUrl;
|
||||
final String? coverImageUrl;
|
||||
final String? description;
|
||||
final bool? invited;
|
||||
}
|
||||
|
||||
enum PersonRole {
|
||||
member,
|
||||
guest,
|
||||
contact,
|
||||
}
|
||||
|
||||
class PersonWithAccess {
|
||||
PersonWithAccess({required this.person, required this.access});
|
||||
|
||||
final Person person;
|
||||
final bool access;
|
||||
}
|
||||
-23
@@ -1,23 +0,0 @@
|
||||
import 'package:appflowy/features/mension_person/data/models/models.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
|
||||
abstract class MentionRepository {
|
||||
/// Gets the list of persons
|
||||
Future<FlowyResult<List<Person>, FlowyError>> getPersons({
|
||||
required String workspaceId,
|
||||
required String query,
|
||||
});
|
||||
|
||||
Future<FlowyResult<PersonWithAccess, FlowyError>> getPerson({
|
||||
required String workspaceId,
|
||||
required String documentId,
|
||||
required String personId,
|
||||
});
|
||||
|
||||
/// Gets the list of persons
|
||||
Future<FlowyResult<Person, FlowyError>> invitePerson({
|
||||
required String workspaceId,
|
||||
required InviteInfo info,
|
||||
});
|
||||
}
|
||||
-139
@@ -1,139 +0,0 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:appflowy/features/mension_person/data/models/invite.dart';
|
||||
import 'package:appflowy/features/mension_person/data/models/person.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/code.pbenum.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
import 'mention_repository.dart';
|
||||
|
||||
class MockMentionRepository extends MentionRepository {
|
||||
@override
|
||||
Future<FlowyResult<List<Person>, FlowyError>> getPersons({
|
||||
required String workspaceId,
|
||||
required String query,
|
||||
}) async {
|
||||
final persons = _MockState.getInstance().persons;
|
||||
if (query.trim().isNotEmpty) {
|
||||
final formatedQuery = query.trim().toLowerCase();
|
||||
final filteredPersons = persons
|
||||
.where(
|
||||
(p) =>
|
||||
p.name.toLowerCase().contains(formatedQuery) ||
|
||||
p.email.toLowerCase().contains(formatedQuery),
|
||||
)
|
||||
.toList();
|
||||
return FlowySuccess(filteredPersons);
|
||||
}
|
||||
return FlowySuccess(persons);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<FlowyResult<Person, FlowyError>> invitePerson({
|
||||
required String workspaceId,
|
||||
required InviteInfo info,
|
||||
}) async {
|
||||
try {
|
||||
final person = _MockState.getInstance().invitePerson(info);
|
||||
return FlowySuccess(person);
|
||||
} on FormatException catch (e) {
|
||||
return FlowyFailure(
|
||||
FlowyError(code: ErrorCode.EmailAlreadyExists, msg: e.message),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<FlowyResult<PersonWithAccess, FlowyError>> getPerson({
|
||||
required String workspaceId,
|
||||
required String documentId,
|
||||
required String personId,
|
||||
}) async {
|
||||
final persons = _MockState.getInstance().persons;
|
||||
final person = persons.where((p) => p.id == personId).firstOrNull;
|
||||
if (person == null) {
|
||||
return FlowyFailure(
|
||||
FlowyError(code: ErrorCode.RecordNotFound, msg: 'Person not found'),
|
||||
);
|
||||
}
|
||||
return FlowySuccess(
|
||||
PersonWithAccess(person: person, access: Random().nextBool()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _MockState {
|
||||
_MockState._();
|
||||
|
||||
static _MockState? _instance;
|
||||
|
||||
static _MockState getInstance() {
|
||||
_instance ??= _MockState._();
|
||||
return _instance!;
|
||||
}
|
||||
|
||||
final List<Person> persons = [
|
||||
Person(
|
||||
id: '1',
|
||||
name: 'Andrew Christian',
|
||||
role: PersonRole.member,
|
||||
email: 'andrewchristian@appflowy.io',
|
||||
coverImageUrl: _coverImageUrl,
|
||||
avatarUrl: 'https://avatar.iran.liara.run/public',
|
||||
),
|
||||
Person(
|
||||
id: '2',
|
||||
name: 'Andrew Tate',
|
||||
role: PersonRole.member,
|
||||
email: 'andrewtate@appflowy.io',
|
||||
description: 'A famous internet personality ',
|
||||
coverImageUrl: _coverImageUrl,
|
||||
avatarUrl: 'https://avatar.iran.liara.run/public/boy',
|
||||
),
|
||||
Person(
|
||||
id: '3',
|
||||
name: 'Emma Johnson',
|
||||
role: PersonRole.member,
|
||||
email: 'emmajohnson@appflowy.io',
|
||||
avatarUrl: 'https://avatar.iran.liara.run/public/girl',
|
||||
),
|
||||
Person(
|
||||
id: '4',
|
||||
name: 'Michael Brown',
|
||||
role: PersonRole.member,
|
||||
email: 'michaelbrown@appflowy.io',
|
||||
avatarUrl: 'https://avatar.iran.liara.run/public/boy/13',
|
||||
),
|
||||
Person(
|
||||
id: '5',
|
||||
name: 'Nathan Brooks',
|
||||
role: PersonRole.member,
|
||||
email: 'nathanbrooks@appflowy.io',
|
||||
avatarUrl: 'https://avatar.iran.liara.run/public/boy/10',
|
||||
),
|
||||
];
|
||||
|
||||
Person invitePerson(InviteInfo info) {
|
||||
final index = persons.indexWhere((p) => p.email == info.email);
|
||||
if (index != -1) {
|
||||
throw FormatException('Person with email ${info.email} already exists');
|
||||
}
|
||||
final person = Person(
|
||||
id: DateTime.now().millisecondsSinceEpoch.toString(),
|
||||
name: info.contactDetail?.name ?? info.email,
|
||||
role: info.role,
|
||||
email: info.email,
|
||||
description: info.contactDetail?.description ?? '',
|
||||
avatarUrl: randomAvatarUrl,
|
||||
coverImageUrl: Random().nextBool() ? null : _coverImageUrl,
|
||||
);
|
||||
persons.add(person);
|
||||
return person;
|
||||
}
|
||||
|
||||
String get randomAvatarUrl =>
|
||||
'https://avatar.iran.liara.run/public/${Random().nextBool() ? 'boy' : 'girl'}/${Random().nextInt(100)}';
|
||||
}
|
||||
|
||||
const String _coverImageUrl =
|
||||
'https://images.unsplash.com/photo-1748882145961-536cc88fd117?q=80&w=2624&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D';
|
||||
@@ -1,170 +0,0 @@
|
||||
import 'package:appflowy/core/config/kv.dart';
|
||||
import 'package:appflowy/core/config/kv_keys.dart';
|
||||
import 'package:appflowy/features/mension_person/data/cache/person_list_cache.dart';
|
||||
import 'package:appflowy/features/mension_person/data/models/models.dart';
|
||||
import 'package:appflowy/features/mension_person/data/repositories/mention_repository.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'mention_bloc.freezed.dart';
|
||||
|
||||
// bool _showMorePages = false, _showMorePersons = false;
|
||||
|
||||
class MentionBloc extends Bloc<MentionEvent, MentionState> {
|
||||
MentionBloc(this.repository, this.workspaceId)
|
||||
: super(MentionState.initial()) {
|
||||
_personListCache = getIt<PersonListCache>();
|
||||
on<Initial>(_onInitial);
|
||||
on<Query>(_onQuery);
|
||||
on<GetPersons>(_onGetPersons);
|
||||
on<UpdatePersonList>(_onUpdatePersonList);
|
||||
on<ShowMorePersons>(_onShowMorePersons);
|
||||
on<ShowMorePages>(_onShowMorePages);
|
||||
on<ToggleSendNotification>(_onToggleSendNotification);
|
||||
on<AddVisibleItem>(_onAddVisibleItem);
|
||||
on<RemoveVisibleItem>(_onRemoveVisibleItem);
|
||||
on<SelectItem>(_onSelectItem);
|
||||
add(MentionEvent.init());
|
||||
add(MentionEvent.getPersons(workspaceId: workspaceId));
|
||||
}
|
||||
final MentionRepository repository;
|
||||
final String workspaceId;
|
||||
late final PersonListCache _personListCache;
|
||||
|
||||
Future<void> _onInitial(
|
||||
Initial event,
|
||||
Emitter<MentionState> emit,
|
||||
) async {
|
||||
final sendNotification =
|
||||
await getIt<KeyValueStorage>().getBool(KVKeys.atMenuSendNotification) ??
|
||||
false;
|
||||
if (!isClosed) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
sendNotification: sendNotification,
|
||||
showMorePersons: false,
|
||||
showMorePage: false,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onQuery(
|
||||
Query event,
|
||||
Emitter<MentionState> emit,
|
||||
) async {
|
||||
emit(state.copyWith(query: event.text, selectedId: ''));
|
||||
add(MentionEvent.getPersons(workspaceId: workspaceId));
|
||||
}
|
||||
|
||||
Future<void> _onGetPersons(
|
||||
GetPersons event,
|
||||
Emitter<MentionState> emit,
|
||||
) async {
|
||||
final localList = _personListCache.getPersons(workspaceId) ?? [];
|
||||
if (localList.isNotEmpty && state.query.isEmpty) {
|
||||
emit(state.copyWith(persons: localList));
|
||||
}
|
||||
final persons = (await repository.getPersons(
|
||||
workspaceId: event.workspaceId,
|
||||
query: state.query,
|
||||
))
|
||||
.toNullable();
|
||||
if (persons == null) return;
|
||||
add(MentionEvent.updatePersonList(persons));
|
||||
if (persons.isNotEmpty && state.query.isEmpty) {
|
||||
_personListCache.updatePersonList(workspaceId, persons);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onUpdatePersonList(
|
||||
UpdatePersonList event,
|
||||
Emitter<MentionState> emit,
|
||||
) async {
|
||||
emit(state.copyWith(persons: event.persons));
|
||||
}
|
||||
|
||||
Future<void> _onShowMorePersons(
|
||||
ShowMorePersons event,
|
||||
Emitter<MentionState> emit,
|
||||
) async {
|
||||
emit(state.copyWith(showMorePersons: true, selectedId: event.lastId));
|
||||
// _showMorePersons = true;
|
||||
}
|
||||
|
||||
Future<void> _onShowMorePages(
|
||||
ShowMorePages event,
|
||||
Emitter<MentionState> emit,
|
||||
) async {
|
||||
emit(state.copyWith(showMorePage: true, selectedId: event.lastId));
|
||||
// _showMorePages = true;
|
||||
}
|
||||
|
||||
Future<void> _onToggleSendNotification(
|
||||
ToggleSendNotification event,
|
||||
Emitter<MentionState> emit,
|
||||
) async {
|
||||
final value = !state.sendNotification;
|
||||
emit(state.copyWith(sendNotification: value));
|
||||
await getIt<KeyValueStorage>()
|
||||
.setBool(KVKeys.atMenuSendNotification, value);
|
||||
}
|
||||
|
||||
Future<void> _onAddVisibleItem(
|
||||
AddVisibleItem event,
|
||||
Emitter<MentionState> emit,
|
||||
) async {
|
||||
final set = Set.of(state.visibleItems);
|
||||
set.add(event.id);
|
||||
emit(state.copyWith(visibleItems: set));
|
||||
}
|
||||
|
||||
Future<void> _onRemoveVisibleItem(
|
||||
RemoveVisibleItem event,
|
||||
Emitter<MentionState> emit,
|
||||
) async {
|
||||
final set = Set.of(state.visibleItems);
|
||||
set.remove(event.id);
|
||||
emit(state.copyWith(visibleItems: set));
|
||||
}
|
||||
|
||||
Future<void> _onSelectItem(
|
||||
SelectItem event,
|
||||
Emitter<MentionState> emit,
|
||||
) async {
|
||||
emit(state.copyWith(selectedId: event.id));
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class MentionEvent with _$MentionEvent {
|
||||
const factory MentionEvent.init() = Initial;
|
||||
const factory MentionEvent.getPersons({required String workspaceId}) =
|
||||
GetPersons;
|
||||
const factory MentionEvent.updatePersonList(List<Person> persons) =
|
||||
UpdatePersonList;
|
||||
const factory MentionEvent.query(String text) = Query;
|
||||
const factory MentionEvent.showMorePersons(String lastId) = ShowMorePersons;
|
||||
const factory MentionEvent.showMorePages(String lastId) = ShowMorePages;
|
||||
const factory MentionEvent.toggleSendNotification() = ToggleSendNotification;
|
||||
const factory MentionEvent.addVisibleItem(String id) = AddVisibleItem;
|
||||
const factory MentionEvent.removeVisibleItem(String id) = RemoveVisibleItem;
|
||||
const factory MentionEvent.selectItem(String id) = SelectItem;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class MentionState with _$MentionState {
|
||||
const factory MentionState({
|
||||
@Default([]) List<Person> persons,
|
||||
@Default(false) bool sendNotification,
|
||||
@Default(null) String? focusId,
|
||||
@Default('') String query,
|
||||
@Default('') String selectedId,
|
||||
@Default(false) bool showMorePersons,
|
||||
@Default(false) bool showMorePage,
|
||||
@Default({}) Set<String> visibleItems,
|
||||
}) = _MentionState;
|
||||
|
||||
factory MentionState.initial() => const MentionState();
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
import 'package:appflowy/features/mension_person/data/cache/person_list_cache.dart';
|
||||
import 'package:appflowy/features/mension_person/data/models/person.dart';
|
||||
import 'package:appflowy/features/mension_person/data/repositories/mention_repository.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'person_bloc.freezed.dart';
|
||||
|
||||
class PersonBloc extends Bloc<PersonEvent, PersonState> {
|
||||
PersonBloc({
|
||||
required this.documentId,
|
||||
required this.personId,
|
||||
required this.repository,
|
||||
required this.workspaceId,
|
||||
}) : super(PersonState.initial()) {
|
||||
_personListCache = getIt<PersonListCache>();
|
||||
on<Initial>(_onInitial);
|
||||
on<UpdatePerson>(_onUpdatePerson);
|
||||
}
|
||||
final String documentId;
|
||||
final String personId;
|
||||
final String workspaceId;
|
||||
final MentionRepository repository;
|
||||
late final PersonListCache _personListCache;
|
||||
|
||||
Future<void> _onInitial(
|
||||
Initial event,
|
||||
Emitter<PersonState> emit,
|
||||
) async {
|
||||
final persons = _personListCache.getPersons(workspaceId) ?? [];
|
||||
final localPerson = persons.where((p) => p.id == personId).firstOrNull;
|
||||
if (localPerson != null) {
|
||||
add(PersonEvent.updatePerson(localPerson));
|
||||
}
|
||||
|
||||
(await repository.getPerson(
|
||||
documentId: documentId,
|
||||
personId: personId,
|
||||
workspaceId: workspaceId,
|
||||
))
|
||||
.fold((t) {
|
||||
emit(state.copyWith(person: t.person, access: t.access));
|
||||
_personListCache.updatePerson(workspaceId, t.person);
|
||||
}, (e) {
|
||||
showToastNotification(
|
||||
message: e.msg,
|
||||
type: ToastificationType.error,
|
||||
);
|
||||
Log.error('Error fetching person: $e');
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _onUpdatePerson(
|
||||
UpdatePerson event,
|
||||
Emitter<PersonState> emit,
|
||||
) async {
|
||||
emit(state.copyWith(person: event.person));
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class PersonEvent with _$PersonEvent {
|
||||
const factory PersonEvent.initial() = Initial;
|
||||
const factory PersonEvent.updatePerson(Person person) = UpdatePerson;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class PersonState with _$PersonState {
|
||||
const factory PersonState({
|
||||
@Default(null) Person? person,
|
||||
@Default(null) String? documentId,
|
||||
@Default(false) bool access,
|
||||
}) = _PersonState;
|
||||
|
||||
factory PersonState.initial() => const PersonState();
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
import 'package:appflowy/features/mension_person/data/models/mention_menu_item.dart';
|
||||
import 'package:appflowy/features/mension_person/data/repositories/mock_mention_repository.dart';
|
||||
import 'package:appflowy/features/mension_person/logic/mention_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
|
||||
import 'package:appflowy_ui/appflowy_ui.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'widgets/date_reminder_list.dart';
|
||||
import 'widgets/mention_menu_scroller.dart';
|
||||
import 'widgets/mention_menu_shortcuts.dart';
|
||||
import 'widgets/page_list.dart';
|
||||
import 'widgets/person/person_list.dart';
|
||||
|
||||
typedef MentionChildBuilder = Widget Function(
|
||||
BuildContext context,
|
||||
Widget child,
|
||||
);
|
||||
|
||||
class MentionMenu extends StatelessWidget {
|
||||
const MentionMenu({
|
||||
super.key,
|
||||
this.width = 400,
|
||||
this.maxHeight = 400,
|
||||
this.builder,
|
||||
});
|
||||
final double width;
|
||||
final double maxHeight;
|
||||
final MentionChildBuilder? builder;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final workspaceId =
|
||||
context.read<UserWorkspaceBloc>().state.currentWorkspace?.workspaceId ??
|
||||
'';
|
||||
return BlocProvider(
|
||||
create: (_) => MentionBloc(MockMentionRepository(), workspaceId),
|
||||
child: BlocBuilder<MentionBloc, MentionState>(
|
||||
builder: (context, state) {
|
||||
final itemMap = MentionItemMap();
|
||||
final child = Provider<MentionItemMap>.value(
|
||||
value: itemMap,
|
||||
child: MentionMenuScroller(
|
||||
builder: (_, controller) {
|
||||
return BlocListener<MentionBloc, MentionState>(
|
||||
listener: (context, state) {
|
||||
if (!controller.hasClients || !context.mounted) return;
|
||||
controller.jumpTo(0);
|
||||
},
|
||||
listenWhen: (previous, current) =>
|
||||
previous.query != current.query,
|
||||
child: MentionMenuShortcuts(
|
||||
scrollController: controller,
|
||||
itemMap: itemMap,
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(maxHeight: maxHeight),
|
||||
child: AFMenu(
|
||||
width: width,
|
||||
padding: EdgeInsets.zero,
|
||||
builder: (context, children) {
|
||||
return FlowyScrollbar(
|
||||
controller: controller,
|
||||
child: SingleChildScrollView(
|
||||
controller: controller,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: children,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
children: [
|
||||
PersonList(),
|
||||
PageList(),
|
||||
DateReminderList(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
return builder?.call(context, child) ?? child;
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension MentionMenuItemBackgroundColor on BuildContext {
|
||||
Color mentionItemBGColor(
|
||||
BuildContext context,
|
||||
bool isHovering,
|
||||
bool selected,
|
||||
bool disabled,
|
||||
) {
|
||||
final theme = AppFlowyTheme.of(this);
|
||||
return isHovering || selected
|
||||
? theme.fillColorScheme.contentHover
|
||||
: theme.fillColorScheme.content;
|
||||
}
|
||||
}
|
||||
-201
@@ -1,201 +0,0 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:appflowy/plugins/document/application/document_bloc.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/menu/menu_extension.dart';
|
||||
import 'package:appflowy/plugins/inline_actions/inline_actions_menu.dart';
|
||||
import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'mention_menu.dart';
|
||||
|
||||
abstract class MentionMenuService implements InlineActionsMenuService {
|
||||
MultiBlocProvider buildMultiBlocProvider(WidgetBuilder builder);
|
||||
}
|
||||
|
||||
class DesktopMentionMenuService extends MentionMenuService {
|
||||
DesktopMentionMenuService({
|
||||
required this.context,
|
||||
required this.editorState,
|
||||
required this.workspaceBloc,
|
||||
required this.documentBloc,
|
||||
required this.reminderBloc,
|
||||
this.startCharAmount = 1,
|
||||
});
|
||||
|
||||
final BuildContext context;
|
||||
final EditorState editorState;
|
||||
final UserWorkspaceBloc workspaceBloc;
|
||||
final DocumentBloc documentBloc;
|
||||
final ReminderBloc reminderBloc;
|
||||
final int startCharAmount;
|
||||
|
||||
OverlayEntry? _menuEntry;
|
||||
|
||||
@override
|
||||
void dismiss() {
|
||||
if (_menuEntry != null) {
|
||||
editorState.service.keyboardService?.enable();
|
||||
editorState.service.scrollService?.enable();
|
||||
keepEditorFocusNotifier.decrease();
|
||||
}
|
||||
|
||||
_menuEntry?.remove();
|
||||
_menuEntry = null;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> show() async {
|
||||
final completer = Completer<void>();
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||
_show(
|
||||
MentionMenuBuilderInfo(
|
||||
builder: (service, ltrb) => _buildMentionMenu(ltrb),
|
||||
menuSize: Size.square(400),
|
||||
),
|
||||
);
|
||||
completer.complete();
|
||||
});
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
@override
|
||||
InlineActionsMenuStyle get style => throw UnimplementedError();
|
||||
|
||||
void _show(MentionMenuBuilderInfo builderInfo) {
|
||||
dismiss();
|
||||
|
||||
final menuPosition = editorState.calculateMenuOffset(
|
||||
menuSize: builderInfo.menuSize,
|
||||
menuOffset: Offset.zero,
|
||||
);
|
||||
if (menuPosition == null) return;
|
||||
final ltrb = menuPosition.ltrb;
|
||||
|
||||
final editorSize = editorState.renderBox!.size;
|
||||
_menuEntry = OverlayEntry(
|
||||
builder: (context) => Material(
|
||||
color: Colors.transparent,
|
||||
child: SizedBox(
|
||||
height: editorSize.height,
|
||||
width: editorSize.width,
|
||||
// GestureDetector handles clicks outside of the context menu,
|
||||
// to dismiss the context menu.
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: dismiss,
|
||||
child: Stack(
|
||||
children: [
|
||||
ltrb.buildPositioned(
|
||||
child: builderInfo.builder.call(this, ltrb),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Overlay.of(context).insert(_menuEntry!);
|
||||
|
||||
final editorService = editorState.service;
|
||||
|
||||
editorService.keyboardService?.disable(showCursor: true);
|
||||
editorService.scrollService?.disable();
|
||||
}
|
||||
|
||||
Widget _buildMentionMenu(LTRB ltrb) {
|
||||
final editorHeight = editorState.renderBox!.size.height;
|
||||
return buildMultiBlocProvider(
|
||||
(_) => Provider(
|
||||
create: (_) => MentionMenuServiceInfo(
|
||||
onDismiss: dismiss,
|
||||
startCharAmount: startCharAmount,
|
||||
startOffset: editorState.selection?.endIndex ?? 0,
|
||||
editorState: editorState,
|
||||
top: ltrb.top ?? (editorHeight - (ltrb.bottom ?? 0.0) - 400),
|
||||
onMenuReplace: (info) {
|
||||
keepEditorFocusNotifier.increase();
|
||||
_show(info);
|
||||
},
|
||||
),
|
||||
dispose: (context, value) => value.dispose(),
|
||||
child: MentionMenu(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
MultiBlocProvider buildMultiBlocProvider(WidgetBuilder builder) {
|
||||
return MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider.value(value: workspaceBloc),
|
||||
BlocProvider.value(value: documentBloc),
|
||||
BlocProvider.value(value: reminderBloc),
|
||||
],
|
||||
child: BlocBuilder<UserWorkspaceBloc, UserWorkspaceState>(
|
||||
builder: (context, __) => builder.call(context),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
typedef MentionMenuBuilder = Widget Function(
|
||||
MentionMenuService service,
|
||||
LTRB ltrb,
|
||||
);
|
||||
|
||||
class MentionMenuBuilderInfo {
|
||||
MentionMenuBuilderInfo({
|
||||
required this.builder,
|
||||
required this.menuSize,
|
||||
});
|
||||
|
||||
final MentionMenuBuilder builder;
|
||||
final Size menuSize;
|
||||
}
|
||||
|
||||
class MentionMenuServiceInfo {
|
||||
MentionMenuServiceInfo({
|
||||
required this.onDismiss,
|
||||
required this.startCharAmount,
|
||||
required this.startOffset,
|
||||
required this.editorState,
|
||||
required this.top,
|
||||
required this.onMenuReplace,
|
||||
});
|
||||
|
||||
final VoidCallback onDismiss;
|
||||
final int startCharAmount;
|
||||
final int startOffset;
|
||||
final EditorState editorState;
|
||||
final double top;
|
||||
final Map<String, ValueGetter<double>> _itemYMap = {};
|
||||
final ValueChanged<MentionMenuBuilderInfo> onMenuReplace;
|
||||
|
||||
void addItemHeightGetter(String id, ValueGetter<double> yGetter) {
|
||||
_itemYMap[id] = yGetter;
|
||||
}
|
||||
|
||||
void removeItemHeightGetter(String id) => _itemYMap.remove(id);
|
||||
|
||||
void dispose() {
|
||||
_itemYMap.clear();
|
||||
}
|
||||
|
||||
double? getItemPositionY(String id) => _itemYMap[id]?.call();
|
||||
|
||||
bool isTopArea(String id) {
|
||||
final itemY = getItemPositionY(id);
|
||||
if (itemY == null) return false;
|
||||
return itemY <= top + 200;
|
||||
}
|
||||
|
||||
TextRange textRange(String queryText) => TextRange(
|
||||
start: startOffset - startCharAmount,
|
||||
end: queryText.length + startCharAmount,
|
||||
);
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
extension FocusNodeExtension on FocusNode {
|
||||
Future<void> makeSureHasFocus(ValueGetter<bool> cancelCondition) async {
|
||||
final focusNode = this;
|
||||
if (cancelCondition.call() || focusNode.hasFocus) return;
|
||||
focusNode.requestFocus();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
makeSureHasFocus(cancelCondition);
|
||||
});
|
||||
}
|
||||
}
|
||||
-173
@@ -1,173 +0,0 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:appflowy/features/mension_person/logic/mention_bloc.dart';
|
||||
import 'package:appflowy/plugins/document/application/document_bloc.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/menu/menu_extension.dart';
|
||||
import 'package:appflowy/plugins/inline_actions/inline_actions_menu.dart';
|
||||
import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'mention_menu.dart';
|
||||
import 'mention_menu_service.dart';
|
||||
import 'widgets/mobile/mobile_input_widget.dart';
|
||||
|
||||
class MobileMentionMenuService extends MentionMenuService {
|
||||
MobileMentionMenuService({
|
||||
required this.context,
|
||||
required this.editorState,
|
||||
required this.workspaceBloc,
|
||||
required this.documentBloc,
|
||||
required this.reminderBloc,
|
||||
this.startCharAmount = 1,
|
||||
});
|
||||
|
||||
final BuildContext context;
|
||||
final EditorState editorState;
|
||||
final UserWorkspaceBloc workspaceBloc;
|
||||
final DocumentBloc documentBloc;
|
||||
final ReminderBloc reminderBloc;
|
||||
final int startCharAmount;
|
||||
|
||||
OverlayEntry? _menuEntry;
|
||||
|
||||
@override
|
||||
void dismiss() {
|
||||
if (_menuEntry != null) {
|
||||
editorState.service.keyboardService?.enable();
|
||||
editorState.service.scrollService?.enable();
|
||||
keepEditorFocusNotifier.decrease();
|
||||
}
|
||||
|
||||
_menuEntry?.remove();
|
||||
_menuEntry = null;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> show() async {
|
||||
final completer = Completer<void>();
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||
final screenSize = MediaQuery.of(context).size;
|
||||
_show(
|
||||
MentionMenuBuilderInfo(
|
||||
builder: (service, ltrb) => _buildMentionMenu(ltrb),
|
||||
menuSize: Size(screenSize.width - 40, 240),
|
||||
),
|
||||
);
|
||||
completer.complete();
|
||||
});
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
@override
|
||||
InlineActionsMenuStyle get style => throw UnimplementedError();
|
||||
|
||||
void _show(MentionMenuBuilderInfo builderInfo) {
|
||||
dismiss();
|
||||
|
||||
final menuPosition = editorState.calculateMenuOffset(
|
||||
menuSize: builderInfo.menuSize,
|
||||
menuOffset: Offset.zero,
|
||||
);
|
||||
if (menuPosition == null) return;
|
||||
final ltrb = menuPosition.ltrb;
|
||||
|
||||
final editorSize = editorState.renderBox!.size;
|
||||
_menuEntry = OverlayEntry(
|
||||
builder: (context) {
|
||||
final screenHeight = MediaQuery.of(context).size.height;
|
||||
final diffHeight = screenHeight - editorSize.height;
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
child: SizedBox(
|
||||
height: editorSize.height,
|
||||
width: editorSize.width,
|
||||
// GestureDetector handles clicks outside of the context menu,
|
||||
// to dismiss the context menu.
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: dismiss,
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
left: 20,
|
||||
right: 20,
|
||||
top: ltrb.top,
|
||||
bottom: ltrb.bottom == null
|
||||
? null
|
||||
: (diffHeight + ltrb.bottom!),
|
||||
child: builderInfo.builder.call(this, ltrb),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Overlay.of(context).insert(_menuEntry!);
|
||||
|
||||
final editorService = editorState.service;
|
||||
|
||||
editorService.keyboardService?.disable(showCursor: true);
|
||||
editorService.scrollService?.disable();
|
||||
}
|
||||
|
||||
Widget _buildMentionMenu(LTRB ltrb) {
|
||||
final editorHeight = editorState.renderBox!.size.height;
|
||||
final startOffset = editorState.selection?.endIndex ?? 0;
|
||||
return buildMultiBlocProvider(
|
||||
(context) {
|
||||
final screenSize = MediaQuery.of(context).size;
|
||||
return Provider(
|
||||
create: (_) => MentionMenuServiceInfo(
|
||||
onDismiss: dismiss,
|
||||
startCharAmount: startCharAmount,
|
||||
startOffset: startOffset,
|
||||
editorState: editorState,
|
||||
top: ltrb.top ?? (editorHeight - (ltrb.bottom ?? 0.0) - 400),
|
||||
onMenuReplace: (info) {
|
||||
keepEditorFocusNotifier.increase();
|
||||
_show(info);
|
||||
},
|
||||
),
|
||||
dispose: (context, value) => value.dispose(),
|
||||
child: MentionMenu(
|
||||
width: screenSize.width - 40,
|
||||
maxHeight: 240,
|
||||
builder: (context, child) {
|
||||
final mentionBloc = context.read<MentionBloc>();
|
||||
return MobileInputWidget(
|
||||
onDismiss: dismiss,
|
||||
editorState: editorState,
|
||||
onQuery: (v) {
|
||||
mentionBloc.add(MentionEvent.query(v));
|
||||
},
|
||||
startOffset: startOffset,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
MultiBlocProvider buildMultiBlocProvider(WidgetBuilder builder) {
|
||||
return MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider.value(value: workspaceBloc),
|
||||
BlocProvider.value(value: documentBloc),
|
||||
BlocProvider.value(value: reminderBloc),
|
||||
],
|
||||
child: BlocBuilder<UserWorkspaceBloc, UserWorkspaceState>(
|
||||
builder: (context, __) => builder.call(context),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
-149
@@ -1,149 +0,0 @@
|
||||
import 'package:appflowy/features/mension_person/data/models/mention_menu_item.dart';
|
||||
import 'package:appflowy/features/mension_person/logic/mention_bloc.dart';
|
||||
import 'package:appflowy/features/mension_person/presentation/mention_menu.dart';
|
||||
import 'package:appflowy/features/mension_person/presentation/mention_menu_service.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/inline_actions/handlers/date_reference.dart';
|
||||
import 'package:appflowy/plugins/inline_actions/handlers/reminder_reference.dart';
|
||||
import 'package:appflowy_ui/appflowy_ui.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import 'item_visibility_detector.dart';
|
||||
|
||||
class DateReminderList extends StatelessWidget {
|
||||
const DateReminderList({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = AppFlowyTheme.of(context), spacing = theme.spacing;
|
||||
final mentionState = context.read<MentionBloc>().state,
|
||||
query = mentionState.query,
|
||||
itemMap = context.read<MentionItemMap>();
|
||||
final items = [
|
||||
MentionMenuItem(
|
||||
id: LocaleKeys.document_mentionMenu_dateToday.tr(),
|
||||
onExecute: () => onDateInsert(context, DateTime.now()),
|
||||
),
|
||||
MentionMenuItem(
|
||||
id: LocaleKeys.document_mentionMenu_dateTomorrow.tr(),
|
||||
onExecute: () => onDateInsert(
|
||||
context,
|
||||
DateTime.now().add(const Duration(days: 1)),
|
||||
),
|
||||
),
|
||||
MentionMenuItem(
|
||||
id: LocaleKeys.document_mentionMenu_dateYesterday.tr(),
|
||||
onExecute: () => onDateInsert(
|
||||
context,
|
||||
DateTime.now().subtract(const Duration(days: 1)),
|
||||
),
|
||||
),
|
||||
MentionMenuItem(
|
||||
id: LocaleKeys.document_mentionMenu_reminderTomorrow9Am.tr(),
|
||||
onExecute: () {
|
||||
final now = DateTime.now();
|
||||
onReminderInsert(
|
||||
context,
|
||||
DateTime(now.year, now.month, now.day + 1, 9),
|
||||
true,
|
||||
);
|
||||
},
|
||||
),
|
||||
MentionMenuItem(
|
||||
id: LocaleKeys.document_mentionMenu_reminder1Week.tr(),
|
||||
onExecute: () => onReminderInsert(
|
||||
context,
|
||||
DateTime.now().add(const Duration(days: 7)),
|
||||
false,
|
||||
),
|
||||
),
|
||||
];
|
||||
List<MentionMenuItem> filterItems = List.of(items);
|
||||
if (query.isNotEmpty) {
|
||||
filterItems = filterItems
|
||||
.where((item) => item.id.toLowerCase().contains(query.toLowerCase()))
|
||||
.toList();
|
||||
}
|
||||
|
||||
if (filterItems.isEmpty) return const SizedBox.shrink();
|
||||
final children = List.generate(filterItems.length, (index) {
|
||||
final item = filterItems[index];
|
||||
itemMap.addToDateAndReminder(item);
|
||||
return MentionMenuItenVisibilityDetector(
|
||||
id: item.id,
|
||||
child: AFTextMenuItem(
|
||||
title: item.id,
|
||||
selected: context.read<MentionBloc>().state.selectedId == item.id,
|
||||
onTap: item.onExecute,
|
||||
backgroundColor: context.mentionItemBGColor,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
AFDivider(),
|
||||
Padding(
|
||||
padding: EdgeInsets.all(spacing.m),
|
||||
child: AFMenuSection(
|
||||
title: LocaleKeys.document_mentionMenu_dateAndReminder.tr(),
|
||||
children: children,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> onDateInsert(
|
||||
BuildContext context,
|
||||
DateTime date,
|
||||
) async {
|
||||
final mentionInfo = context.read<MentionMenuServiceInfo>(),
|
||||
editorState = mentionInfo.editorState,
|
||||
query = context.read<MentionBloc>().state.query;
|
||||
final selection = editorState.selection;
|
||||
if (selection == null || !selection.isCollapsed) return;
|
||||
|
||||
final node = editorState.getNodeAtPath(selection.end.path);
|
||||
final delta = node?.delta;
|
||||
if (node == null || delta == null) return;
|
||||
final range = mentionInfo.textRange(query);
|
||||
|
||||
onDismiss(mentionInfo);
|
||||
await editorState.insertDateReference(date, range.start, range.end);
|
||||
}
|
||||
|
||||
Future<void> onReminderInsert(
|
||||
BuildContext context,
|
||||
DateTime date,
|
||||
bool includeTime,
|
||||
) async {
|
||||
final mentionInfo = context.read<MentionMenuServiceInfo>(),
|
||||
editorState = mentionInfo.editorState,
|
||||
query = context.read<MentionBloc>().state.query;
|
||||
final selection = editorState.selection;
|
||||
if (selection == null || !selection.isCollapsed) return;
|
||||
|
||||
final node = editorState.getNodeAtPath(selection.end.path);
|
||||
final delta = node?.delta;
|
||||
if (node == null || delta == null) return;
|
||||
|
||||
final range = mentionInfo.textRange(query);
|
||||
onDismiss(mentionInfo);
|
||||
await editorState.insertReminderReference(
|
||||
context,
|
||||
date,
|
||||
range.start,
|
||||
range.end,
|
||||
includeTime: includeTime,
|
||||
);
|
||||
}
|
||||
|
||||
void onDismiss(MentionMenuServiceInfo info) {
|
||||
info.onDismiss.call();
|
||||
}
|
||||
}
|
||||
-140
@@ -1,140 +0,0 @@
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
typedef HoverMenuBuilder = Widget Function(
|
||||
BuildContext context,
|
||||
PointerEnterEventListener onEnter,
|
||||
PointerExitEventListener onExit,
|
||||
);
|
||||
|
||||
class HoverMenu extends StatefulWidget {
|
||||
const HoverMenu({
|
||||
super.key,
|
||||
required this.menuConstraints,
|
||||
required this.triggerSize,
|
||||
required this.child,
|
||||
required this.menuBuilder,
|
||||
this.delayToShow = const Duration(milliseconds: 50),
|
||||
this.delayToHide = const Duration(milliseconds: 300),
|
||||
this.direction = PopoverDirection.topWithLeftAligned,
|
||||
this.offset = Offset.zero,
|
||||
this.enable = true,
|
||||
this.onEnter,
|
||||
this.onExit,
|
||||
});
|
||||
|
||||
final BoxConstraints menuConstraints;
|
||||
final Size triggerSize;
|
||||
final Widget child;
|
||||
final HoverMenuBuilder menuBuilder;
|
||||
final Duration delayToShow;
|
||||
final Duration delayToHide;
|
||||
final PopoverDirection direction;
|
||||
final Offset offset;
|
||||
final PointerEnterEventListener? onEnter;
|
||||
final PointerExitEventListener? onExit;
|
||||
final bool enable;
|
||||
|
||||
@override
|
||||
State<HoverMenu> createState() => _HoverMenuState();
|
||||
}
|
||||
|
||||
class _HoverMenuState extends State<HoverMenu> {
|
||||
final controller = PopoverController();
|
||||
bool isHoverMenuShowing = false;
|
||||
bool isHovering = false;
|
||||
|
||||
BoxConstraints get menuConstraints => widget.menuConstraints;
|
||||
Size get triggerSize => widget.triggerSize;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
controller.close();
|
||||
isHoverMenuShowing = false;
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return widget.enable
|
||||
? buildHoverMouseRegion(
|
||||
buildPopover(),
|
||||
cursor: SystemMouseCursors.click,
|
||||
)
|
||||
: widget.child;
|
||||
}
|
||||
|
||||
Widget buildPopover() {
|
||||
return AppFlowyPopover(
|
||||
controller: controller,
|
||||
direction: widget.direction,
|
||||
offset: widget.offset,
|
||||
onOpen: () {
|
||||
keepEditorFocusNotifier.increase();
|
||||
isHoverMenuShowing = true;
|
||||
},
|
||||
onClose: () {
|
||||
keepEditorFocusNotifier.decrease();
|
||||
isHoverMenuShowing = false;
|
||||
},
|
||||
margin: EdgeInsets.zero,
|
||||
constraints: menuConstraints.copyWith(
|
||||
maxHeight: menuConstraints.maxHeight + triggerSize.height,
|
||||
),
|
||||
decorationColor: Colors.transparent,
|
||||
popoverDecoration: BoxDecoration(),
|
||||
popupBuilder: (context) => buildHoverMouseRegion(
|
||||
widget.menuBuilder.call(context, onEnter, onExit),
|
||||
),
|
||||
child: widget.child,
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildHoverMouseRegion(
|
||||
Widget child, {
|
||||
MouseCursor cursor = MouseCursor.defer,
|
||||
}) {
|
||||
return MouseRegion(
|
||||
cursor: cursor,
|
||||
onEnter: onEnter,
|
||||
onExit: onExit,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
void onEnter(PointerEnterEvent e) {
|
||||
widget.onEnter?.call(e);
|
||||
isHovering = true;
|
||||
Future.delayed(widget.delayToShow, () {
|
||||
if (isHovering && !isHoverMenuShowing) {
|
||||
showHoverMenu();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void onExit(PointerExitEvent e) {
|
||||
widget.onExit?.call(e);
|
||||
isHovering = false;
|
||||
tryToDismissenu();
|
||||
}
|
||||
|
||||
void showHoverMenu() {
|
||||
if (isHoverMenuShowing || !mounted) {
|
||||
return;
|
||||
}
|
||||
keepEditorFocusNotifier.increase();
|
||||
controller.show();
|
||||
isHoverMenuShowing = true;
|
||||
}
|
||||
|
||||
void tryToDismissenu() {
|
||||
Future.delayed(widget.delayToHide, () {
|
||||
if (isHovering) return;
|
||||
keepEditorFocusNotifier.decrease();
|
||||
controller.close();
|
||||
isHoverMenuShowing = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
-236
@@ -1,236 +0,0 @@
|
||||
import 'package:appflowy/features/mension_person/data/models/models.dart';
|
||||
import 'package:appflowy/features/mension_person/presentation/mention_menu_service.dart';
|
||||
import 'package:appflowy/features/mension_person/presentation/menu_extension.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy_ui/appflowy_ui.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'invite_menu.dart';
|
||||
|
||||
class ContactDetailMenu extends StatefulWidget {
|
||||
const ContactDetailMenu({
|
||||
super.key,
|
||||
required this.info,
|
||||
required this.onInfoChanged,
|
||||
});
|
||||
|
||||
final InviteInfo info;
|
||||
final ValueChanged<InviteInfo> onInfoChanged;
|
||||
|
||||
@override
|
||||
State<ContactDetailMenu> createState() => _ContactDetailMenuState();
|
||||
}
|
||||
|
||||
class _ContactDetailMenuState extends State<ContactDetailMenu> {
|
||||
late FocusNode nameFocusNode = FocusNode(onKeyEvent: onFocusKeyEvent);
|
||||
late FocusScopeNode menuFocusNode =
|
||||
FocusScopeNode(onKeyEvent: onFocusKeyEvent);
|
||||
late TextEditingController nameController =
|
||||
TextEditingController(text: info.email);
|
||||
late TextEditingController descriptionController =
|
||||
TextEditingController(text: detail.description);
|
||||
|
||||
late InviteInfo info = widget.info;
|
||||
|
||||
late ContactDetail detail =
|
||||
info.contactDetail ?? ContactDetail(name: info.email);
|
||||
final nameKey = GlobalKey<AFTextFieldState>();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
nameFocusNode.makeSureHasFocus(() => !mounted);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
nameController.dispose();
|
||||
descriptionController.dispose();
|
||||
nameFocusNode.dispose();
|
||||
menuFocusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = AppFlowyTheme.of(context), spacing = theme.spacing;
|
||||
return FocusScope(
|
||||
node: menuFocusNode,
|
||||
child: GestureDetector(
|
||||
onTap: () {},
|
||||
child: SizedBox(
|
||||
width: 400,
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: theme.surfaceColorScheme.layer02,
|
||||
borderRadius: BorderRadius.circular(spacing.xl),
|
||||
boxShadow: theme.shadow.medium,
|
||||
),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
vertical: spacing.xl,
|
||||
horizontal: spacing.xxl,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
buildTitle(context),
|
||||
VSpace(spacing.l),
|
||||
buildSubtitle(LocaleKeys.document_mentionMenu_name.tr()),
|
||||
VSpace(spacing.xs),
|
||||
buildNameField(),
|
||||
VSpace(spacing.xxl),
|
||||
buildSubtitle(
|
||||
LocaleKeys.document_mentionMenu_aboutContact.tr(),
|
||||
),
|
||||
VSpace(spacing.xs),
|
||||
buildDescriptionField(),
|
||||
VSpace(spacing.xxl),
|
||||
Row(
|
||||
children: [
|
||||
Spacer(),
|
||||
buildBackButton(),
|
||||
HSpace(spacing.l),
|
||||
buildApplyButton(),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildTitle(BuildContext context) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
return Text(
|
||||
LocaleKeys.document_mentionMenu_contactDetail.tr(),
|
||||
style: theme.textStyle.heading4.prominent(
|
||||
color: theme.textColorScheme.primary,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildSubtitle(String title) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
return Text(
|
||||
title,
|
||||
style: theme.textStyle.caption.enhanced(
|
||||
color: theme.textColorScheme.secondary,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildNameField() {
|
||||
return SizedBox(
|
||||
width: 360,
|
||||
child: AFTextField(
|
||||
key: nameKey,
|
||||
hintText: LocaleKeys.document_mentionMenu_contactInputHint.tr(),
|
||||
size: AFTextFieldSize.m,
|
||||
focusNode: nameFocusNode,
|
||||
controller: nameController,
|
||||
onChanged: (text) {
|
||||
detail = detail.copyWith(name: text);
|
||||
updateInfo(info.copyWith(contactDetail: () => detail));
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildDescriptionField() {
|
||||
return SizedBox(
|
||||
width: 360,
|
||||
height: 68,
|
||||
child: AFTextField(
|
||||
maxLines: null,
|
||||
expands: true,
|
||||
textAlignVertical: TextAlignVertical.top,
|
||||
size: AFTextFieldSize.m,
|
||||
keyboardType: TextInputType.multiline,
|
||||
controller: descriptionController,
|
||||
hintText: LocaleKeys.document_mentionMenu_aboutContactHint.tr(),
|
||||
onChanged: (text) {
|
||||
detail = detail.copyWith(description: text);
|
||||
updateInfo(info.copyWith(contactDetail: () => detail));
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildBackButton() {
|
||||
return AFOutlinedTextButton.normal(
|
||||
text: LocaleKeys.document_mentionMenu_back.tr(),
|
||||
onTap: onBack,
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildApplyButton() {
|
||||
return Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: AFFilledTextButton.primary(
|
||||
text: LocaleKeys.settings_appearance_documentSettings_apply.tr(),
|
||||
onTap: onApply,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void onBack() {
|
||||
final serviceInfo = context.read<MentionMenuServiceInfo?>();
|
||||
if (serviceInfo == null) return;
|
||||
serviceInfo.onMenuReplace.call(
|
||||
MentionMenuBuilderInfo(
|
||||
builder: (service, lrbt) => service.buildMultiBlocProvider(
|
||||
(context) => Provider.value(
|
||||
value: serviceInfo,
|
||||
child: InviteMenu(
|
||||
info: widget.info,
|
||||
onInfoChanged: (v) => widget.onInfoChanged(v),
|
||||
),
|
||||
),
|
||||
),
|
||||
menuSize: Size.square(400),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void onApply() {
|
||||
if (detail.name.isEmpty) {
|
||||
nameKey.currentState?.syncError(
|
||||
errorText: LocaleKeys.document_mentionMenu_contactInputError.tr(),
|
||||
);
|
||||
return;
|
||||
}
|
||||
nameKey.currentState?.clearError();
|
||||
widget.onInfoChanged.call(info);
|
||||
}
|
||||
|
||||
void onDismiss() {
|
||||
final mentionInfo = context.read<MentionMenuServiceInfo?>();
|
||||
mentionInfo?.onDismiss.call();
|
||||
}
|
||||
|
||||
void updateInfo(InviteInfo newInfo) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
info = newInfo;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
KeyEventResult onFocusKeyEvent(FocusNode node, KeyEvent key) {
|
||||
if (key is! KeyDownEvent) return KeyEventResult.ignored;
|
||||
if (key.logicalKey == LogicalKeyboardKey.escape) {
|
||||
onDismiss();
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
}
|
||||
-333
@@ -1,333 +0,0 @@
|
||||
import 'package:appflowy/features/mension_person/data/models/models.dart';
|
||||
import 'package:appflowy/features/mension_person/presentation/mention_menu.dart';
|
||||
import 'package:appflowy/features/mension_person/presentation/mention_menu_service.dart';
|
||||
import 'package:appflowy/features/mension_person/presentation/menu_extension.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy_ui/appflowy_ui.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:string_validator/string_validator.dart';
|
||||
|
||||
import 'contact_detail_menu.dart';
|
||||
|
||||
class PersonRoleDownMenuItem with AFDropDownMenuMixin {
|
||||
const PersonRoleDownMenuItem({
|
||||
required this.role,
|
||||
});
|
||||
|
||||
final PersonRole role;
|
||||
|
||||
@override
|
||||
String get label => role.displayName;
|
||||
}
|
||||
|
||||
class InviteMenu extends StatefulWidget {
|
||||
const InviteMenu({
|
||||
super.key,
|
||||
required this.info,
|
||||
required this.onInfoChanged,
|
||||
});
|
||||
|
||||
final InviteInfo info;
|
||||
final ValueChanged<InviteInfo> onInfoChanged;
|
||||
|
||||
@override
|
||||
State<InviteMenu> createState() => _InviteMenuState();
|
||||
}
|
||||
|
||||
class _InviteMenuState extends State<InviteMenu> {
|
||||
late FocusNode emailFocusNode = FocusNode(onKeyEvent: onFocusKeyEvent);
|
||||
late FocusScopeNode menuFocusNode =
|
||||
FocusScopeNode(onKeyEvent: onFocusKeyEvent);
|
||||
late TextEditingController emailController =
|
||||
TextEditingController(text: info.email);
|
||||
|
||||
late InviteInfo info = widget.info;
|
||||
final emailKey = GlobalKey<AFTextFieldState>();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
emailFocusNode.makeSureHasFocus(() => !mounted);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
emailController.dispose();
|
||||
emailFocusNode.dispose();
|
||||
menuFocusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = AppFlowyTheme.of(context), spacing = theme.spacing;
|
||||
return GestureDetector(
|
||||
onTap: () {},
|
||||
child: FocusScope(
|
||||
node: menuFocusNode,
|
||||
child: ConstrainedBox(
|
||||
constraints:
|
||||
BoxConstraints(maxHeight: 400, maxWidth: 400, minWidth: 400),
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: theme.surfaceColorScheme.layer02,
|
||||
borderRadius: BorderRadius.circular(spacing.xl),
|
||||
boxShadow: theme.shadow.medium,
|
||||
),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
vertical: spacing.xl,
|
||||
horizontal: spacing.xxl,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
buildTitle(context),
|
||||
VSpace(spacing.l),
|
||||
buildSubtitle(LocaleKeys.document_mentionMenu_email.tr()),
|
||||
VSpace(spacing.xs),
|
||||
buildEmailField(),
|
||||
VSpace(spacing.l),
|
||||
buildSubtitle(LocaleKeys.document_mentionMenu_type.tr()),
|
||||
VSpace(spacing.xs),
|
||||
buildRoleSelector(),
|
||||
VSpace(spacing.xxl),
|
||||
Row(
|
||||
children: [
|
||||
Spacer(),
|
||||
buildBackButton(),
|
||||
HSpace(spacing.l),
|
||||
buildAddButton(),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildTitle(BuildContext context) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
return Text(
|
||||
LocaleKeys.document_mentionMenu_invitePerson.tr(),
|
||||
style: theme.textStyle.heading4.prominent(
|
||||
color: theme.textColorScheme.primary,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildSubtitle(String title) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
return Text(
|
||||
title,
|
||||
style: theme.textStyle.caption.enhanced(
|
||||
color: theme.textColorScheme.secondary,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildEmailField() {
|
||||
return SizedBox(
|
||||
width: 360,
|
||||
child: AFTextField(
|
||||
key: emailKey,
|
||||
size: AFTextFieldSize.m,
|
||||
focusNode: emailFocusNode,
|
||||
controller: emailController,
|
||||
hintText: LocaleKeys.document_mentionMenu_emailInputHint.tr(),
|
||||
onChanged: (text) {
|
||||
updateInfo(info.copyWith(email: text));
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildRoleSelector() {
|
||||
return SizedBox(
|
||||
width: 360,
|
||||
child: AFDropDownMenu<PersonRoleDownMenuItem>(
|
||||
items: roleItems(),
|
||||
selectedItems: [info.role.buildItem()],
|
||||
itemBuilder: (context, item, isSelected, onSelected) =>
|
||||
buildRoleItem(context, item, onSelected),
|
||||
onSelected: (value) {
|
||||
updateInfo(info.copyWith(role: value?.role));
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildRoleItem(
|
||||
BuildContext context,
|
||||
PersonRoleDownMenuItem item,
|
||||
ValueChanged<PersonRoleDownMenuItem>? onSelected,
|
||||
) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
final isSelected = info.role == item.role;
|
||||
return AFBaseButton(
|
||||
padding: EdgeInsets.symmetric(
|
||||
vertical: theme.spacing.s,
|
||||
horizontal: theme.spacing.m,
|
||||
),
|
||||
borderRadius: theme.borderRadius.m,
|
||||
borderColor: (context, isHovering, disabled, isFocused) {
|
||||
return Colors.transparent;
|
||||
},
|
||||
showFocusRing: false,
|
||||
builder: (context, _, __) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
item.role.displayName,
|
||||
style: theme.textStyle.body
|
||||
.standard(color: theme.textColorScheme.primary),
|
||||
),
|
||||
Text(
|
||||
item.role.description,
|
||||
style: theme.textStyle.caption
|
||||
.standard(color: theme.textColorScheme.secondary),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
backgroundColor: (context, isHovering, _) {
|
||||
if (isHovering || isSelected) {
|
||||
return theme.fillColorScheme.contentHover;
|
||||
}
|
||||
return Colors.transparent;
|
||||
},
|
||||
onTap: () => onSelected?.call(item),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildBackButton() {
|
||||
return AFOutlinedTextButton.normal(
|
||||
text: LocaleKeys.document_mentionMenu_back.tr(),
|
||||
onTap: onBack,
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildAddButton() {
|
||||
final isContact = info.role == PersonRole.contact;
|
||||
return AFFilledTextButton.primary(
|
||||
text: isContact
|
||||
? LocaleKeys.button_add.tr()
|
||||
: LocaleKeys.document_mentionMenu_invite.tr(),
|
||||
onTap: onApply,
|
||||
);
|
||||
}
|
||||
|
||||
List<PersonRoleDownMenuItem> roleItems() =>
|
||||
PersonRole.values.map((role) => role.buildItem()).toList();
|
||||
|
||||
void onBack() {
|
||||
final serviceInfo = context.read<MentionMenuServiceInfo?>();
|
||||
if (serviceInfo == null) return;
|
||||
serviceInfo.onMenuReplace.call(
|
||||
MentionMenuBuilderInfo(
|
||||
builder: (service, lrbt) => service.buildMultiBlocProvider(
|
||||
(_) => Provider.value(
|
||||
value: serviceInfo,
|
||||
child: MentionMenu(),
|
||||
),
|
||||
),
|
||||
menuSize: Size.square(400),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void updateInfo(InviteInfo newInfo) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
info = newInfo;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void onApply() {
|
||||
final isContact = info.role == PersonRole.contact;
|
||||
final email = info.email.trim();
|
||||
|
||||
if (email.isEmpty || !isEmail(email)) {
|
||||
emailKey.currentState?.syncError(
|
||||
errorText: LocaleKeys.document_mentionMenu_emailInputError.tr(),
|
||||
);
|
||||
return;
|
||||
}
|
||||
emailKey.currentState?.clearError();
|
||||
if (isContact) {
|
||||
final serviceInfo = context.read<MentionMenuServiceInfo?>();
|
||||
if (serviceInfo == null) return;
|
||||
serviceInfo.onMenuReplace.call(
|
||||
MentionMenuBuilderInfo(
|
||||
builder: (service, lrbt) => service.buildMultiBlocProvider(
|
||||
(_) => Provider.value(
|
||||
value: serviceInfo,
|
||||
child: ContactDetailMenu(
|
||||
info: info,
|
||||
onInfoChanged: widget.onInfoChanged,
|
||||
),
|
||||
),
|
||||
),
|
||||
menuSize: Size(400, 300),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
widget.onInfoChanged.call(info);
|
||||
}
|
||||
}
|
||||
|
||||
void onDismiss() {
|
||||
emailFocusNode.unfocus();
|
||||
menuFocusNode.unfocus();
|
||||
final mentionInfo = context.read<MentionMenuServiceInfo?>();
|
||||
mentionInfo?.onDismiss.call();
|
||||
}
|
||||
|
||||
KeyEventResult onFocusKeyEvent(FocusNode node, KeyEvent key) {
|
||||
if (key is! KeyDownEvent) return KeyEventResult.ignored;
|
||||
if (key.logicalKey == LogicalKeyboardKey.escape) {
|
||||
onDismiss();
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
}
|
||||
|
||||
extension PersonRoleExtension on PersonRole {
|
||||
PersonRoleDownMenuItem buildItem() {
|
||||
return PersonRoleDownMenuItem(role: this);
|
||||
}
|
||||
|
||||
String get displayName {
|
||||
switch (this) {
|
||||
case PersonRole.member:
|
||||
return LocaleKeys.document_mentionMenu_member.tr();
|
||||
case PersonRole.guest:
|
||||
return LocaleKeys.document_mentionMenu_guest.tr();
|
||||
case PersonRole.contact:
|
||||
return LocaleKeys.document_mentionMenu_contact.tr();
|
||||
}
|
||||
}
|
||||
|
||||
String get description {
|
||||
switch (this) {
|
||||
case PersonRole.member:
|
||||
return LocaleKeys.document_mentionMenu_memberDescription.tr();
|
||||
case PersonRole.guest:
|
||||
return LocaleKeys.document_mentionMenu_guestDescription.tr();
|
||||
case PersonRole.contact:
|
||||
return LocaleKeys.document_mentionMenu_contactDescription.tr();
|
||||
}
|
||||
}
|
||||
}
|
||||
-133
@@ -1,133 +0,0 @@
|
||||
import 'package:appflowy/features/mension_person/data/models/models.dart';
|
||||
import 'package:appflowy/features/mension_person/logic/mention_bloc.dart';
|
||||
import 'package:appflowy/features/mension_person/presentation/mention_menu.dart';
|
||||
import 'package:appflowy/features/mension_person/presentation/mention_menu_service.dart';
|
||||
import 'package:appflowy/features/mension_person/presentation/widgets/item_visibility_detector.dart';
|
||||
import 'package:appflowy/features/mension_person/presentation/widgets/person/person_list.dart';
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/application/document_bloc.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_ui/appflowy_ui.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:universal_platform/universal_platform.dart';
|
||||
|
||||
import 'invite_menu.dart';
|
||||
import '../mobile/mobile_invite_menu.dart';
|
||||
|
||||
class PersonListInviteItem extends StatelessWidget {
|
||||
const PersonListInviteItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final state = context.read<MentionBloc>().state,
|
||||
query = state.query,
|
||||
itemMap = context.read<MentionItemMap>();
|
||||
final id = LocaleKeys.document_mentionMenu_add.tr(args: ['add person']);
|
||||
final displayQuery = query.isEmpty ? '' : ' "$query"';
|
||||
itemMap.addToPerson(
|
||||
MentionMenuItem(id: id, onExecute: () => invitePerson(context)),
|
||||
);
|
||||
return MentionMenuItenVisibilityDetector(
|
||||
id: id,
|
||||
child: AFTextMenuItem(
|
||||
selected: state.selectedId == id,
|
||||
leading: SizedBox.square(
|
||||
dimension: 24,
|
||||
child: Center(
|
||||
child: FlowySvg(
|
||||
FlowySvgs.mention_invite_user_m,
|
||||
size: const Size.square(20.0),
|
||||
),
|
||||
),
|
||||
),
|
||||
title: LocaleKeys.document_mentionMenu_add.tr(args: [displayQuery]),
|
||||
backgroundColor: context.mentionItemBGColor,
|
||||
onTap: () => UniversalPlatform.isMobile
|
||||
? showMobileInviteMenu(context)
|
||||
: invitePerson(context),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void invitePerson(BuildContext context) {
|
||||
final serviceInfo = context.read<MentionMenuServiceInfo>(),
|
||||
documentBloc = context.read<DocumentBloc?>();
|
||||
final mentionBloc = context.read<MentionBloc>(),
|
||||
mentionState = mentionBloc.state,
|
||||
query = mentionState.query;
|
||||
|
||||
serviceInfo.onMenuReplace.call(
|
||||
MentionMenuBuilderInfo(
|
||||
builder: (service, lrbt) => service.buildMultiBlocProvider(
|
||||
(_) => Provider.value(
|
||||
value: serviceInfo,
|
||||
child: InviteMenu(
|
||||
info: InviteInfo(email: query),
|
||||
onInfoChanged: (v) => serviceInfo.editorState.onInviteInfoApply(
|
||||
inviteInfo: v,
|
||||
serviceInfo: serviceInfo,
|
||||
documentBloc: documentBloc,
|
||||
mentionBloc: mentionBloc,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
menuSize: Size(400, 400),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension PersonInviteEditorStateExtension on EditorState {
|
||||
Future<void> onInviteInfoApply({
|
||||
required InviteInfo inviteInfo,
|
||||
required MentionMenuServiceInfo serviceInfo,
|
||||
DocumentBloc? documentBloc,
|
||||
Selection? selection,
|
||||
required MentionBloc mentionBloc,
|
||||
}) async {
|
||||
final mSelection = selection ?? this.selection;
|
||||
if (mSelection == null || !mSelection.isCollapsed || documentBloc == null) {
|
||||
return;
|
||||
}
|
||||
final mentionState = mentionBloc.state, query = mentionState.query;
|
||||
final node = getNodeAtPath(mSelection.end.path);
|
||||
final delta = node?.delta;
|
||||
if (node == null || delta == null) return;
|
||||
final range = serviceInfo.textRange(query);
|
||||
await (await mentionBloc.repository.invitePerson(
|
||||
workspaceId: mentionBloc.workspaceId,
|
||||
info: inviteInfo,
|
||||
))
|
||||
.fold((person) async {
|
||||
serviceInfo.onDismiss.call();
|
||||
await insertPerson(
|
||||
person,
|
||||
documentBloc.documentId,
|
||||
range,
|
||||
mentionState.sendNotification,
|
||||
mSelection,
|
||||
);
|
||||
final isContact = person.role == PersonRole.contact;
|
||||
if (isContact) {
|
||||
showToastNotification(
|
||||
message: LocaleKeys.document_mentionMenu_addContactToast
|
||||
.tr(args: [person.name]),
|
||||
);
|
||||
} else {
|
||||
showToastNotification(
|
||||
message: LocaleKeys.document_mentionMenu_inviteEmailSent.tr(),
|
||||
);
|
||||
}
|
||||
}, (error) {
|
||||
showToastNotification(
|
||||
message: error.msg,
|
||||
type: ToastificationType.error,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
-41
@@ -1,41 +0,0 @@
|
||||
import 'package:appflowy/features/mension_person/logic/mention_bloc.dart';
|
||||
import 'package:appflowy/features/mension_person/presentation/mention_menu_service.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:visibility_detector/visibility_detector.dart';
|
||||
|
||||
class MentionMenuItenVisibilityDetector extends StatelessWidget {
|
||||
const MentionMenuItenVisibilityDetector({
|
||||
super.key,
|
||||
required this.id,
|
||||
required this.child,
|
||||
});
|
||||
|
||||
final String id;
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final renderBox = context.findRenderObject();
|
||||
final menuServiceInfo = context.read<MentionMenuServiceInfo>();
|
||||
if (renderBox is RenderBox) {
|
||||
menuServiceInfo.addItemHeightGetter(
|
||||
id,
|
||||
() => renderBox.localToGlobal(Offset.zero).dy,
|
||||
);
|
||||
}
|
||||
final bloc = context.read<MentionBloc>();
|
||||
return MouseRegion(
|
||||
onEnter: (e) => bloc.add(MentionEvent.selectItem(id)),
|
||||
child: VisibilityDetector(
|
||||
key: ValueKey(id),
|
||||
child: child,
|
||||
onVisibilityChanged: (info) {
|
||||
if (info.visibleFraction == 0.0) {
|
||||
menuServiceInfo.removeItemHeightGetter(id);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
-30
@@ -1,30 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class MentionMenuScroller extends StatefulWidget {
|
||||
const MentionMenuScroller({super.key, required this.builder});
|
||||
|
||||
final MentionMenuScrollerBuilder builder;
|
||||
|
||||
@override
|
||||
State<MentionMenuScroller> createState() => _MentionMenuScrollerState();
|
||||
}
|
||||
|
||||
class _MentionMenuScrollerState extends State<MentionMenuScroller> {
|
||||
final controller = ScrollController();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return widget.builder.call(context, controller);
|
||||
}
|
||||
}
|
||||
|
||||
typedef MentionMenuScrollerBuilder = Widget Function(
|
||||
BuildContext context,
|
||||
ScrollController controller,
|
||||
);
|
||||
-285
@@ -1,285 +0,0 @@
|
||||
import 'dart:math';
|
||||
import 'package:appflowy/features/mension_person/data/models/mention_menu_item.dart';
|
||||
import 'package:appflowy/features/mension_person/logic/mention_bloc.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../mention_menu_service.dart';
|
||||
|
||||
class MentionMenuShortcuts extends StatefulWidget {
|
||||
const MentionMenuShortcuts({
|
||||
super.key,
|
||||
required this.child,
|
||||
required this.scrollController,
|
||||
required this.itemMap,
|
||||
});
|
||||
|
||||
final Widget child;
|
||||
final ScrollController scrollController;
|
||||
final MentionItemMap itemMap;
|
||||
@override
|
||||
State<MentionMenuShortcuts> createState() => _MentionMenuShortcutsState();
|
||||
}
|
||||
|
||||
class _MentionMenuShortcutsState extends State<MentionMenuShortcuts> {
|
||||
final focusNode = FocusNode();
|
||||
int startOffset = 0;
|
||||
|
||||
ScrollController get scrollController => widget.scrollController;
|
||||
|
||||
MentionItemMap get itemMap => widget.itemMap;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted) return;
|
||||
focusNode.requestFocus();
|
||||
final mentionMenuServiceInfo = context.read<MentionMenuServiceInfo?>();
|
||||
startOffset =
|
||||
mentionMenuServiceInfo?.editorState.selection?.endIndex ?? 0;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
focusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Focus(
|
||||
focusNode: focusNode,
|
||||
onKeyEvent: (node, event) => onKeyEvent(node, event, context),
|
||||
child: widget.child,
|
||||
);
|
||||
}
|
||||
|
||||
KeyEventResult onKeyEvent(
|
||||
FocusNode node,
|
||||
KeyEvent event,
|
||||
BuildContext context,
|
||||
) {
|
||||
if (event is! KeyDownEvent && event is! KeyRepeatEvent) {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
final menuBloc = context.read<MentionBloc>(),
|
||||
menuState = menuBloc.state,
|
||||
queryText = menuState.query;
|
||||
final mentionMenuServiceInfo = context.read<MentionMenuServiceInfo>();
|
||||
final editorState = mentionMenuServiceInfo.editorState,
|
||||
onDismiss = mentionMenuServiceInfo.onDismiss;
|
||||
const moveKeys = [
|
||||
LogicalKeyboardKey.arrowUp,
|
||||
LogicalKeyboardKey.arrowDown,
|
||||
LogicalKeyboardKey.tab,
|
||||
];
|
||||
|
||||
if (event.logicalKey == LogicalKeyboardKey.enter) {
|
||||
final item =
|
||||
itemMap.items.where((e) => e.id == menuState.selectedId).firstOrNull;
|
||||
if (item != null) {
|
||||
item.onExecute.call();
|
||||
} else {
|
||||
onDismiss();
|
||||
}
|
||||
return KeyEventResult.handled;
|
||||
} else if (event.logicalKey == LogicalKeyboardKey.escape) {
|
||||
// Workaround to bring focus back to editor
|
||||
editorState.updateSelectionWithReason(editorState.selection);
|
||||
onDismiss();
|
||||
} else if (event.logicalKey == LogicalKeyboardKey.backspace) {
|
||||
if (queryText.isEmpty) {
|
||||
if (_canDeleteLastCharacter(context)) {
|
||||
editorState.deleteBackward();
|
||||
} else {
|
||||
// Workaround for editor regaining focus
|
||||
editorState.apply(
|
||||
editorState.transaction..afterSelection = editorState.selection,
|
||||
);
|
||||
}
|
||||
onDismiss();
|
||||
} else {
|
||||
// widget.onSelectionUpdate();
|
||||
editorState.deleteBackward();
|
||||
_deleteCharacterAtSelection(context);
|
||||
}
|
||||
|
||||
return KeyEventResult.handled;
|
||||
} else if (event.character != null &&
|
||||
![
|
||||
...moveKeys,
|
||||
LogicalKeyboardKey.arrowLeft,
|
||||
LogicalKeyboardKey.arrowRight,
|
||||
].contains(event.logicalKey)) {
|
||||
/// Prevents dismissal of context menu by notifying the parent
|
||||
/// that the selection change occurred from the handler.
|
||||
// widget.onSelectionUpdate();
|
||||
|
||||
// Interpolation to avoid having a getter for private variable
|
||||
_insertCharacter(event.character!, context);
|
||||
return KeyEventResult.handled;
|
||||
} else if (moveKeys.contains(event.logicalKey)) {
|
||||
_moveSelection(event.logicalKey, context);
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
|
||||
if ([LogicalKeyboardKey.arrowLeft, LogicalKeyboardKey.arrowRight]
|
||||
.contains(event.logicalKey)) {
|
||||
// widget.onSelectionUpdate();
|
||||
|
||||
event.logicalKey == LogicalKeyboardKey.arrowLeft
|
||||
? editorState.moveCursorForward()
|
||||
: editorState.moveCursorBackward(SelectionMoveRange.character);
|
||||
|
||||
/// If cursor moves before @ then dismiss menu
|
||||
/// If cursor moves after @search.length then dismiss menu
|
||||
final selection = editorState.selection;
|
||||
if (selection != null &&
|
||||
(selection.endIndex < startOffset ||
|
||||
selection.endIndex > (startOffset + queryText.length))) {
|
||||
onDismiss();
|
||||
}
|
||||
|
||||
/// Workaround: When using the move cursor methods, it seems the
|
||||
/// focus goes back to the editor, this makes sure this handler
|
||||
/// receives the next keypress.
|
||||
///
|
||||
focusNode.requestFocus();
|
||||
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
|
||||
void _moveSelection(LogicalKeyboardKey key, BuildContext context) {
|
||||
final menuBloc = context.read<MentionBloc>(), menuState = menuBloc.state;
|
||||
final items = itemMap.items;
|
||||
final index = items.indexWhere((e) => e.id == menuState.selectedId);
|
||||
int newIndex = index;
|
||||
final isUp = key == LogicalKeyboardKey.arrowUp ||
|
||||
(key == LogicalKeyboardKey.tab &&
|
||||
HardwareKeyboard.instance.isShiftPressed);
|
||||
final isDown =
|
||||
[LogicalKeyboardKey.arrowDown, LogicalKeyboardKey.tab].contains(key);
|
||||
if (!isUp && !isDown) return;
|
||||
if (index < 0) {
|
||||
newIndex = isUp ? items.length - 1 : 0;
|
||||
} else {
|
||||
if (isUp) {
|
||||
newIndex = index == 0 ? items.length - 1 : index - 1;
|
||||
} else if (isDown) {
|
||||
newIndex = index == items.length - 1 ? 0 : index + 1;
|
||||
}
|
||||
}
|
||||
final item = items[newIndex];
|
||||
menuBloc.add(MentionEvent.selectItem(item.id));
|
||||
_scrollToItem(index, newIndex, context);
|
||||
}
|
||||
|
||||
void _scrollToItem(
|
||||
int from,
|
||||
int to,
|
||||
BuildContext context,
|
||||
) {
|
||||
if (!context.mounted) return;
|
||||
final items = itemMap.items;
|
||||
|
||||
/// scroll to the end
|
||||
if (to == items.length - 1) {
|
||||
scrollController.jumpTo(scrollController.position.maxScrollExtent);
|
||||
return;
|
||||
} else if (to == 0) {
|
||||
/// scroll to the start
|
||||
scrollController.jumpTo(0);
|
||||
return;
|
||||
}
|
||||
|
||||
final menuInfo = context.read<MentionMenuServiceInfo>();
|
||||
final toId = items[to].id, isTopArea = menuInfo.isTopArea(toId);
|
||||
|
||||
final currentPosition = scrollController.position.pixels;
|
||||
if (isTopArea && from > to) {
|
||||
scrollController.jumpTo(max(0, currentPosition - 50));
|
||||
} else if (!isTopArea && from < to) {
|
||||
scrollController.jumpTo(
|
||||
min(currentPosition + 50, scrollController.position.maxScrollExtent),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _insertCharacter(String character, BuildContext context) {
|
||||
final menuBloc = context.read<MentionBloc>(), menuState = menuBloc.state;
|
||||
final mentionMenuServiceInfo = context.read<MentionMenuServiceInfo>();
|
||||
final editorState = mentionMenuServiceInfo.editorState;
|
||||
editorState.insertTextAtCurrentSelection(character);
|
||||
|
||||
final selection = editorState.selection;
|
||||
if (selection == null || !selection.isCollapsed) {
|
||||
return;
|
||||
}
|
||||
|
||||
final delta = editorState.getNodeAtPath(selection.end.path)?.delta;
|
||||
if (delta == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final oldText = menuState.query;
|
||||
|
||||
final query = editorState
|
||||
.getTextInSelection(
|
||||
selection.copyWith(
|
||||
start: selection.start.copyWith(offset: startOffset),
|
||||
end: selection.start
|
||||
.copyWith(offset: startOffset + oldText.length + 1),
|
||||
),
|
||||
)
|
||||
.join();
|
||||
onQuery(context, query);
|
||||
}
|
||||
|
||||
bool _canDeleteLastCharacter(BuildContext context) {
|
||||
final mentionMenuServiceInfo = context.read<MentionMenuServiceInfo>(),
|
||||
editorState = mentionMenuServiceInfo.editorState,
|
||||
selection = editorState.selection;
|
||||
if (selection == null || !selection.isCollapsed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final delta = editorState.getNodeAtPath(selection.start.path)?.delta;
|
||||
if (delta == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return delta.isNotEmpty;
|
||||
}
|
||||
|
||||
void _deleteCharacterAtSelection(BuildContext context) {
|
||||
final menuBloc = context.read<MentionBloc>(), menuState = menuBloc.state;
|
||||
|
||||
final mentionMenuServiceInfo = context.read<MentionMenuServiceInfo>(),
|
||||
editorState = mentionMenuServiceInfo.editorState,
|
||||
selection = editorState.selection;
|
||||
if (selection == null || !selection.isCollapsed) return;
|
||||
|
||||
final node = editorState.getNodeAtPath(selection.end.path);
|
||||
final delta = node?.delta;
|
||||
if (node == null || delta == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final oldText = menuState.query;
|
||||
final query = delta.toPlainText().substring(
|
||||
startOffset,
|
||||
startOffset - 1 + oldText.length,
|
||||
);
|
||||
onQuery(context, query);
|
||||
}
|
||||
|
||||
void onQuery(BuildContext context, String text) =>
|
||||
context.read<MentionBloc>().add(MentionEvent.query(text));
|
||||
}
|
||||
-193
@@ -1,193 +0,0 @@
|
||||
import 'package:appflowy/features/mension_person/data/models/invite.dart';
|
||||
import 'package:appflowy/features/mension_person/presentation/menu_extension.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart';
|
||||
import 'package:appflowy/plugins/base/drag_handler.dart';
|
||||
import 'package:appflowy_ui/appflowy_ui.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
void showMobileContactDetailMenu({
|
||||
required BuildContext context,
|
||||
required InviteInfo info,
|
||||
required ValueChanged<InviteInfo> onInfoChanged,
|
||||
}) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
showMobileBottomSheet(
|
||||
context,
|
||||
dragHandleBuilder: (_) => const DragHandleV2(),
|
||||
showDragHandle: true,
|
||||
showDivider: false,
|
||||
isDragEnabled: false,
|
||||
enableDraggableScrollable: true,
|
||||
initialChildSize: 0.9,
|
||||
minChildSize: 0.9,
|
||||
maxChildSize: 0.9,
|
||||
backgroundColor: theme.surfaceColorScheme.primary,
|
||||
builder: (_) => MobileContactDetailMenu(
|
||||
info: info,
|
||||
onInfoChanged: onInfoChanged,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class MobileContactDetailMenu extends StatefulWidget {
|
||||
const MobileContactDetailMenu({
|
||||
super.key,
|
||||
required this.info,
|
||||
required this.onInfoChanged,
|
||||
});
|
||||
|
||||
final InviteInfo info;
|
||||
final ValueChanged<InviteInfo> onInfoChanged;
|
||||
|
||||
@override
|
||||
State<MobileContactDetailMenu> createState() =>
|
||||
_MobileContactDetailMenuState();
|
||||
}
|
||||
|
||||
class _MobileContactDetailMenuState extends State<MobileContactDetailMenu> {
|
||||
late InviteInfo info = widget.info;
|
||||
|
||||
late ContactDetail detail =
|
||||
info.contactDetail ?? ContactDetail(name: info.email);
|
||||
final nameKey = GlobalKey<AFTextFieldState>();
|
||||
late FocusNode nameFocusNode = FocusNode();
|
||||
late TextEditingController nameController =
|
||||
TextEditingController(text: info.email);
|
||||
late TextEditingController descriptionController =
|
||||
TextEditingController(text: detail.description);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
nameFocusNode.makeSureHasFocus(() => !mounted);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
nameController.dispose();
|
||||
descriptionController.dispose();
|
||||
nameFocusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = AppFlowyTheme.of(context), spacing = theme.spacing;
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
buildHeader(),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: spacing.xl),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
buildSubtitle(LocaleKeys.document_mentionMenu_name.tr()),
|
||||
VSpace(spacing.m),
|
||||
buildNameField(),
|
||||
VSpace(spacing.xl),
|
||||
buildSubtitle(LocaleKeys.document_mentionMenu_aboutContact.tr()),
|
||||
VSpace(spacing.m),
|
||||
buildDescriptionField(),
|
||||
VSpace(124),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildHeader() {
|
||||
return BottomSheetHeader(
|
||||
showBackButton: true,
|
||||
showDoneButton: true,
|
||||
showCloseButton: false,
|
||||
showRemoveButton: false,
|
||||
title: LocaleKeys.document_mentionMenu_contactDetail.tr(),
|
||||
doneButtonBuilder: (context) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: onApply,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(right: theme.spacing.xl),
|
||||
child: Text(
|
||||
LocaleKeys.document_mentionMenu_invite.tr(),
|
||||
style: theme.textStyle.body
|
||||
.standard(color: theme.textColorScheme.action),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildSubtitle(String title) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
return Text(
|
||||
title,
|
||||
style: theme.textStyle.caption
|
||||
.enhanced(color: theme.textColorScheme.secondary),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildNameField() {
|
||||
return SizedBox(
|
||||
width: MediaQuery.of(context).size.width,
|
||||
child: AFTextField(
|
||||
key: nameKey,
|
||||
hintText: LocaleKeys.document_mentionMenu_contactInputHint.tr(),
|
||||
focusNode: nameFocusNode,
|
||||
controller: nameController,
|
||||
onChanged: (text) {
|
||||
detail = detail.copyWith(name: text);
|
||||
updateInfo(info.copyWith(contactDetail: () => detail));
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildDescriptionField() {
|
||||
return SizedBox(
|
||||
width: MediaQuery.of(context).size.width,
|
||||
height: 132,
|
||||
child: AFTextField(
|
||||
maxLines: null,
|
||||
expands: true,
|
||||
textAlignVertical: TextAlignVertical.top,
|
||||
keyboardType: TextInputType.multiline,
|
||||
controller: descriptionController,
|
||||
hintText: LocaleKeys.document_mentionMenu_aboutContactHint.tr(),
|
||||
onChanged: (text) {
|
||||
detail = detail.copyWith(description: text);
|
||||
updateInfo(info.copyWith(contactDetail: () => detail));
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void updateInfo(InviteInfo newInfo) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
info = newInfo;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void onApply() {
|
||||
if (detail.name.isEmpty) {
|
||||
nameKey.currentState?.syncError(
|
||||
errorText: LocaleKeys.document_mentionMenu_contactInputError.tr(),
|
||||
);
|
||||
return;
|
||||
}
|
||||
nameKey.currentState?.clearError();
|
||||
widget.onInfoChanged.call(info);
|
||||
Navigator.pop(context);
|
||||
}
|
||||
}
|
||||
-80
@@ -1,80 +0,0 @@
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class MobileInputWidget extends StatefulWidget {
|
||||
const MobileInputWidget({
|
||||
super.key,
|
||||
required this.editorState,
|
||||
required this.onDismiss,
|
||||
required this.onQuery,
|
||||
required this.startOffset,
|
||||
required this.child,
|
||||
});
|
||||
final EditorState editorState;
|
||||
final VoidCallback onDismiss;
|
||||
final ValueChanged<String> onQuery;
|
||||
|
||||
final int startOffset;
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
State<MobileInputWidget> createState() => _MobileInputWidgetState();
|
||||
}
|
||||
|
||||
class _MobileInputWidgetState extends State<MobileInputWidget> {
|
||||
final _focusNode = FocusNode(debugLabel: 'mobile_input_widget');
|
||||
int invalidCounter = 0;
|
||||
String query = '';
|
||||
|
||||
EditorState get editorState => widget.editorState;
|
||||
|
||||
ValueChanged<String> get onQuery => widget.onQuery;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback(
|
||||
(_) => _focusNode.requestFocus(),
|
||||
);
|
||||
keepEditorFocusNotifier.increase();
|
||||
editorState.selectionNotifier.addListener(onSelectionChanged);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
editorState.selectionNotifier.removeListener(onSelectionChanged);
|
||||
_focusNode.dispose();
|
||||
keepEditorFocusNotifier.decrease();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Focus(focusNode: _focusNode, child: widget.child);
|
||||
}
|
||||
|
||||
void onSelectionChanged() {
|
||||
final selection = editorState.selection;
|
||||
if (selection == null) {
|
||||
dismiss();
|
||||
return;
|
||||
}
|
||||
if (!selection.isCollapsed) {
|
||||
dismiss();
|
||||
return;
|
||||
}
|
||||
final startOffset = widget.startOffset;
|
||||
final endOffset = selection.end.offset;
|
||||
if (endOffset < startOffset) {
|
||||
dismiss();
|
||||
return;
|
||||
}
|
||||
final node = editorState.getNodeAtPath(selection.start.path);
|
||||
final text = node?.delta?.toPlainText() ?? '';
|
||||
final search = text.substring(startOffset, endOffset);
|
||||
query = search;
|
||||
onQuery.call(query);
|
||||
}
|
||||
|
||||
void dismiss() => widget.onDismiss.call();
|
||||
}
|
||||
-301
@@ -1,301 +0,0 @@
|
||||
import 'package:appflowy/features/mension_person/data/models/invite.dart';
|
||||
import 'package:appflowy/features/mension_person/data/models/person.dart';
|
||||
import 'package:appflowy/features/mension_person/logic/mention_bloc.dart';
|
||||
import 'package:appflowy/features/mension_person/presentation/mention_menu_service.dart';
|
||||
import 'package:appflowy/features/mension_person/presentation/menu_extension.dart';
|
||||
import 'package:appflowy/features/mension_person/presentation/widgets/invite/invite_menu.dart';
|
||||
import 'package:appflowy/features/mension_person/presentation/widgets/invite/person_list_invite_item.dart';
|
||||
import 'package:appflowy/features/workspace/logic/workspace_bloc.dart';
|
||||
import 'package:appflowy/generated/flowy_svgs.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/plugins/base/drag_handler.dart';
|
||||
import 'package:appflowy/plugins/document/application/document_bloc.dart';
|
||||
import 'package:appflowy_ui/appflowy_ui.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:string_validator/string_validator.dart';
|
||||
|
||||
import 'mobile_contact_detail_menu.dart';
|
||||
|
||||
void showMobileInviteMenu(BuildContext context) {
|
||||
final theme = AppFlowyTheme.of(context),
|
||||
documentBloc = context.read<DocumentBloc?>(),
|
||||
mentionBloc = context.read<MentionBloc>(),
|
||||
workspaceBloc = context.read<UserWorkspaceBloc?>(),
|
||||
serviceInfo = context.read<MentionMenuServiceInfo>(),
|
||||
editorState = serviceInfo.editorState,
|
||||
selection = editorState.selection;
|
||||
showMobileBottomSheet(
|
||||
context,
|
||||
dragHandleBuilder: (_) => const DragHandleV2(),
|
||||
showDragHandle: true,
|
||||
showDivider: false,
|
||||
backgroundColor: theme.surfaceColorScheme.primary,
|
||||
isDragEnabled: false,
|
||||
enableDraggableScrollable: true,
|
||||
initialChildSize: 0.9,
|
||||
minChildSize: 0.9,
|
||||
maxChildSize: 0.9,
|
||||
builder: (_) => MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider.value(value: mentionBloc),
|
||||
if (documentBloc != null) BlocProvider.value(value: documentBloc),
|
||||
if (workspaceBloc != null) BlocProvider.value(value: workspaceBloc),
|
||||
],
|
||||
child: MobileInviteMenu(
|
||||
info: InviteInfo(email: mentionBloc.state.query),
|
||||
onInfoChanged: (v) => editorState.onInviteInfoApply(
|
||||
inviteInfo: v,
|
||||
serviceInfo: serviceInfo,
|
||||
selection: selection,
|
||||
documentBloc: documentBloc,
|
||||
mentionBloc: mentionBloc,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
serviceInfo.onDismiss.call();
|
||||
}
|
||||
|
||||
class MobileInviteMenu extends StatefulWidget {
|
||||
const MobileInviteMenu({
|
||||
super.key,
|
||||
required this.info,
|
||||
required this.onInfoChanged,
|
||||
});
|
||||
|
||||
final InviteInfo info;
|
||||
final ValueChanged<InviteInfo> onInfoChanged;
|
||||
|
||||
@override
|
||||
State<MobileInviteMenu> createState() => _MobileInviteMenuState();
|
||||
}
|
||||
|
||||
class _MobileInviteMenuState extends State<MobileInviteMenu> {
|
||||
late FocusNode emailFocusNode = FocusNode();
|
||||
late TextEditingController emailController =
|
||||
TextEditingController(text: info.email);
|
||||
|
||||
late InviteInfo info = widget.info;
|
||||
final emailKey = GlobalKey<AFTextFieldState>();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
emailFocusNode.makeSureHasFocus(() => !mounted);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
emailController.dispose();
|
||||
emailFocusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = AppFlowyTheme.of(context), spacing = theme.spacing;
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
buildHeader(),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: spacing.xl),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
buildSubtitle(LocaleKeys.document_mentionMenu_email.tr()),
|
||||
VSpace(spacing.m),
|
||||
buildEmailField(),
|
||||
VSpace(spacing.xl),
|
||||
buildSubtitle(LocaleKeys.document_mentionMenu_type.tr()),
|
||||
VSpace(spacing.m),
|
||||
...buildRoles(),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildHeader() {
|
||||
return BottomSheetHeader(
|
||||
showBackButton: false,
|
||||
showDoneButton: true,
|
||||
showCloseButton: true,
|
||||
showRemoveButton: false,
|
||||
title: LocaleKeys.document_mentionMenu_invitePerson.tr(),
|
||||
doneButtonBuilder: (context) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
final isContact = info.role == PersonRole.contact;
|
||||
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: onApply,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(right: theme.spacing.xl),
|
||||
child: Text(
|
||||
isContact
|
||||
? LocaleKeys.button_next.tr()
|
||||
: LocaleKeys.document_mentionMenu_invite.tr(),
|
||||
style: theme.textStyle.body
|
||||
.standard(color: theme.textColorScheme.action),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildSubtitle(String title) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
return Text(
|
||||
title,
|
||||
style: theme.textStyle.caption.enhanced(
|
||||
color: theme.textColorScheme.secondary,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildEmailField() {
|
||||
return SizedBox(
|
||||
width: MediaQuery.of(context).size.width,
|
||||
child: AFTextField(
|
||||
key: emailKey,
|
||||
focusNode: emailFocusNode,
|
||||
controller: emailController,
|
||||
hintText: LocaleKeys.document_mentionMenu_emailInputHint.tr(),
|
||||
onChanged: (text) {
|
||||
updateInfo(info.copyWith(email: text));
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> buildRoles() {
|
||||
final theme = AppFlowyTheme.of(context), spacing = theme.spacing;
|
||||
final items = List.generate(PersonRole.values.length, (index) {
|
||||
final role = PersonRole.values[index];
|
||||
return _RoleItem(
|
||||
selected: info.role == role,
|
||||
role: role,
|
||||
onTap: () => updateInfo(info.copyWith(role: role)),
|
||||
);
|
||||
});
|
||||
final List<Widget> results = [];
|
||||
for (int i = 0; i < items.length; i++) {
|
||||
results.add(items[i]);
|
||||
if (i != items.length - 1) results.add(VSpace(spacing.m));
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
void updateInfo(InviteInfo newInfo) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
info = newInfo;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void onApply() {
|
||||
final isContact = info.role == PersonRole.contact;
|
||||
final email = info.email.trim();
|
||||
if (email.isEmpty || !isEmail(email)) {
|
||||
emailKey.currentState?.syncError(
|
||||
errorText: LocaleKeys.document_mentionMenu_emailInputError.tr(),
|
||||
);
|
||||
return;
|
||||
}
|
||||
emailKey.currentState?.clearError();
|
||||
if (isContact) {
|
||||
showMobileContactDetailMenu(
|
||||
context: context,
|
||||
info: info,
|
||||
onInfoChanged: (v) {
|
||||
Navigator.pop(context);
|
||||
widget.onInfoChanged.call(v);
|
||||
},
|
||||
);
|
||||
} else {
|
||||
widget.onInfoChanged.call(info);
|
||||
Navigator.pop(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _RoleItem extends StatelessWidget {
|
||||
const _RoleItem({
|
||||
required this.selected,
|
||||
required this.role,
|
||||
required this.onTap,
|
||||
});
|
||||
final bool selected;
|
||||
final PersonRole role;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = AppFlowyTheme.of(context), spacing = theme.spacing;
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: onTap,
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(spacing.m),
|
||||
border: Border.all(
|
||||
color: selected
|
||||
? theme.borderColorScheme.themeThick
|
||||
: theme.borderColorScheme.primary,
|
||||
),
|
||||
),
|
||||
child: SizedBox(
|
||||
height: 80,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
vertical: spacing.l,
|
||||
horizontal: spacing.xl,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
selected
|
||||
? FlowySvg(
|
||||
FlowySvgs.radio_button_selected_m,
|
||||
blendMode: null,
|
||||
size: Size.square(20),
|
||||
)
|
||||
: FlowySvg(
|
||||
FlowySvgs.radio_button_unselected_m,
|
||||
color: theme.borderColorScheme.primary,
|
||||
size: Size.square(20),
|
||||
),
|
||||
HSpace(spacing.l),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
role.displayName,
|
||||
style: theme.textStyle.body
|
||||
.enhanced(color: theme.textColorScheme.primary),
|
||||
),
|
||||
Text(
|
||||
role.description,
|
||||
style: theme.textStyle.caption
|
||||
.standard(color: theme.textColorScheme.secondary),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
-216
@@ -1,216 +0,0 @@
|
||||
import 'package:appflowy/core/helpers/url_launcher.dart';
|
||||
import 'package:appflowy/features/mension_person/data/models/person.dart';
|
||||
import 'package:appflowy/features/mension_person/logic/person_bloc.dart';
|
||||
import 'package:appflowy/features/mension_person/presentation/widgets/person/person_profile_card.dart';
|
||||
import 'package:appflowy/features/mension_person/presentation/widgets/person/person_role_badge.dart';
|
||||
import 'package:appflowy/features/mension_person/presentation/widgets/profile_card_more_button.dart';
|
||||
import 'package:appflowy_ui/appflowy_ui.dart';
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class MobilePersonProfileCard extends StatelessWidget {
|
||||
const MobilePersonProfileCard({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return buildCard(context);
|
||||
}
|
||||
|
||||
Widget buildCard(BuildContext context) {
|
||||
final theme = AppFlowyTheme.of(context),
|
||||
spacing = theme.spacing,
|
||||
xl = spacing.xl;
|
||||
|
||||
final personState = context.read<PersonBloc>().state,
|
||||
person = personState.person;
|
||||
if (person == null) {
|
||||
return Center(
|
||||
child: CircularProgressIndicator.adaptive(),
|
||||
);
|
||||
}
|
||||
|
||||
final hasCover = person.coverImageUrl?.isNotEmpty ?? false;
|
||||
final sizeWidth = MediaQuery.of(context).size.width;
|
||||
return Stack(
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
buildCover(context),
|
||||
SizedBox(
|
||||
width: sizeWidth - xl * 2,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(top: hasCover ? 64 : 0),
|
||||
child: buildPersonInfo(context),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Positioned(
|
||||
left: hasCover ? xl : 0,
|
||||
top: hasCover ? 38 : 0,
|
||||
child: buildAvatar(context),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildCover(BuildContext context) {
|
||||
final personState = context.read<PersonBloc>().state;
|
||||
final person = personState.person, url = person?.coverImageUrl ?? '';
|
||||
if (url.isEmpty) return VSpace(100);
|
||||
final theme = AppFlowyTheme.of(context), xl = theme.spacing.xl;
|
||||
final sizeWidth = MediaQuery.of(context).size.width;
|
||||
|
||||
return SizedBox(
|
||||
width: sizeWidth - xl * 2,
|
||||
height: 92,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(theme.spacing.m),
|
||||
child: CachedNetworkImage(imageUrl: url, fit: BoxFit.cover),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildAvatar(BuildContext context) {
|
||||
final personState = context.read<PersonBloc>().state;
|
||||
final person = personState.person, url = person?.avatarUrl ?? '';
|
||||
if (url.isEmpty) return const SizedBox.shrink();
|
||||
final hasCover = person?.coverImageUrl?.isNotEmpty ?? false;
|
||||
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
final avatar = SizedBox.square(
|
||||
dimension: 90,
|
||||
child: AFAvatar(
|
||||
url: url,
|
||||
radius: 41,
|
||||
name: person?.name,
|
||||
backgroundColor: Colors.transparent,
|
||||
),
|
||||
);
|
||||
return DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: theme.surfaceColorScheme.layer01,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: hasCover
|
||||
? SizedBox.square(
|
||||
dimension: 100,
|
||||
child: Center(child: avatar),
|
||||
)
|
||||
: avatar,
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildPersonInfo(BuildContext context) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
buildName(context),
|
||||
buildEmail(context),
|
||||
buildDescription(context),
|
||||
VSpace(theme.spacing.xxl),
|
||||
buildActions(context),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildName(BuildContext context) {
|
||||
final personState = context.read<PersonBloc>().state;
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
final suffixIcon = context.buildSuffixIcon();
|
||||
return Row(
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
personState.person?.name ?? '',
|
||||
style: theme.textStyle.title
|
||||
.prominent(color: theme.textColorScheme.primary),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
if (suffixIcon != null) suffixIcon,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildEmail(BuildContext context) {
|
||||
final person = context.read<PersonBloc>().state.person;
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
return Text(
|
||||
person?.email ?? '',
|
||||
style:
|
||||
theme.textStyle.body.standard(color: theme.textColorScheme.secondary),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildDescription(BuildContext context) {
|
||||
final person = context.read<PersonBloc>().state.person,
|
||||
description = person?.description;
|
||||
if (description?.isEmpty ?? true) return const SizedBox.shrink();
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
return Container(
|
||||
margin: EdgeInsets.only(top: theme.spacing.m),
|
||||
width: double.infinity,
|
||||
child: DecoratedBox(
|
||||
decoration: buildDescriptionDecoration(context),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(theme.spacing.l),
|
||||
child: Text(
|
||||
description!,
|
||||
style: theme.textStyle.caption
|
||||
.standard(color: theme.textColorScheme.primary),
|
||||
maxLines: 4,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildActions(BuildContext context) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
final person = context.read<PersonBloc>().state.person;
|
||||
if (person == null) return const SizedBox.shrink();
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
PersonRoleBadge(role: person.role),
|
||||
Spacer(),
|
||||
context.buildNotificationButton(),
|
||||
HSpace(theme.spacing.m),
|
||||
ProfileCardMoreButton(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Decoration buildCardDecoration(BuildContext context) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
return BoxDecoration(
|
||||
color: theme.surfaceColorScheme.layer01,
|
||||
borderRadius: BorderRadius.circular(theme.spacing.l),
|
||||
boxShadow: theme.shadow.small,
|
||||
);
|
||||
}
|
||||
|
||||
Decoration buildDescriptionDecoration(BuildContext context) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
return BoxDecoration(
|
||||
color: theme.fillColorScheme.contentVisible,
|
||||
borderRadius: BorderRadius.circular(theme.spacing.m),
|
||||
);
|
||||
}
|
||||
|
||||
void openEmailApp(Person person) {
|
||||
afLaunchUrlString('mailto:${person.email}');
|
||||
}
|
||||
}
|
||||
-50
@@ -1,50 +0,0 @@
|
||||
import 'package:appflowy/features/mension_person/logic/mention_bloc.dart';
|
||||
import 'package:appflowy/features/mension_person/presentation/mention_menu.dart';
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy_ui/appflowy_ui.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'item_visibility_detector.dart';
|
||||
|
||||
class MoreResultsItem extends StatelessWidget {
|
||||
const MoreResultsItem({
|
||||
super.key,
|
||||
required this.num,
|
||||
required this.onTap,
|
||||
required this.id,
|
||||
});
|
||||
|
||||
final int num;
|
||||
final VoidCallback onTap;
|
||||
final String id;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final state = context.read<MentionBloc>().state;
|
||||
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
return MentionMenuItenVisibilityDetector(
|
||||
id: id,
|
||||
child: AFTextMenuItem(
|
||||
selected: state.selectedId == id,
|
||||
title: LocaleKeys.document_mentionMenu_moreResults.tr(args: ['$num']),
|
||||
titleColor: theme.textColorScheme.tertiary,
|
||||
onTap: onTap,
|
||||
leading: SizedBox.square(
|
||||
dimension: 24,
|
||||
child: Center(
|
||||
child: FlowySvg(
|
||||
FlowySvgs.mention_more_results_m,
|
||||
color: theme.iconColorScheme.tertiary,
|
||||
size: const Size.square(20.0),
|
||||
),
|
||||
),
|
||||
),
|
||||
backgroundColor: context.mentionItemBGColor,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
-200
@@ -1,200 +0,0 @@
|
||||
import 'package:appflowy/features/mension_person/presentation/mention_menu.dart';
|
||||
import 'package:appflowy/features/mension_person/presentation/mention_menu_service.dart';
|
||||
import 'package:appflowy/plugins/document/application/prelude.dart';
|
||||
import 'package:appflowy/plugins/inline_actions/handlers/child_page.dart';
|
||||
import 'package:appflowy/plugins/inline_actions/handlers/inline_page_reference.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:appflowy/features/mension_person/data/models/mention_menu_item.dart';
|
||||
import 'package:appflowy/features/mension_person/logic/mention_bloc.dart';
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/workspace/application/recent/recent_views_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
||||
import 'package:appflowy/workspace/presentation/command_palette/widgets/search_icon.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:appflowy_ui/appflowy_ui.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import 'item_visibility_detector.dart';
|
||||
import 'more_results_item.dart';
|
||||
|
||||
class PageList extends StatelessWidget {
|
||||
const PageList({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final mentionState = context.read<MentionBloc>().state,
|
||||
itemMap = context.read<MentionItemMap>();
|
||||
final showMorePage = mentionState.showMorePage, query = mentionState.query;
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
|
||||
return BlocProvider(
|
||||
create: (context) =>
|
||||
RecentViewsBloc()..add(const RecentViewsEvent.initial()),
|
||||
child: BlocBuilder<RecentViewsBloc, RecentViewsState>(
|
||||
builder: (context, state) {
|
||||
final recentViews = state.views.map((e) => e.item).toSet().toList();
|
||||
List<ViewPB> filterViews = List.of(recentViews);
|
||||
if (query.isNotEmpty) {
|
||||
filterViews = filterViews
|
||||
.where(
|
||||
(view) => view.nameOrDefault
|
||||
.toLowerCase()
|
||||
.contains(query.toLowerCase()),
|
||||
)
|
||||
.toList();
|
||||
}
|
||||
final hasMorePage = filterViews.length > 4;
|
||||
List<ViewPB> displayedViews = List.of(filterViews);
|
||||
final showMoreResult = hasMorePage && !showMorePage;
|
||||
|
||||
if (showMoreResult) {
|
||||
displayedViews = displayedViews.sublist(0, 4);
|
||||
}
|
||||
|
||||
for (final view in displayedViews) {
|
||||
itemMap.addToPage(
|
||||
MentionMenuItem(
|
||||
id: view.id,
|
||||
onExecute: () => onPageSelected(view, context),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final createPageId =
|
||||
LocaleKeys.inlineActions_createPage.tr(args: ['addPage']);
|
||||
|
||||
if (query.isNotEmpty) {
|
||||
itemMap.addToPage(
|
||||
MentionMenuItem(
|
||||
id: createPageId,
|
||||
onExecute: () => onPageCreate(context),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final showMoreId = LocaleKeys.document_mentionMenu_moreResults
|
||||
.tr(args: ['show more page']);
|
||||
void onShowMore() {
|
||||
if (!showMoreResult) return;
|
||||
context
|
||||
.read<MentionBloc>()
|
||||
.add(MentionEvent.showMorePages(filterViews[4].id));
|
||||
}
|
||||
|
||||
if (showMoreResult) {
|
||||
itemMap.addToPage(
|
||||
MentionMenuItem(id: showMoreId, onExecute: onShowMore),
|
||||
);
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: EdgeInsets.all(theme.spacing.m),
|
||||
child: AFMenuSection(
|
||||
title: LocaleKeys.document_mentionMenu_pages.tr(),
|
||||
children: [
|
||||
...List.generate(displayedViews.length, (index) {
|
||||
final view = displayedViews[index];
|
||||
return MentionMenuItenVisibilityDetector(
|
||||
id: view.id,
|
||||
child: AFTextMenuItem(
|
||||
selected: mentionState.selectedId == view.id,
|
||||
leading: SizedBox(
|
||||
width: 24,
|
||||
child: Center(child: view.buildIcon(context)),
|
||||
),
|
||||
title: view.nameOrDefault,
|
||||
backgroundColor: context.mentionItemBGColor,
|
||||
onTap: () => onPageSelected(view, context),
|
||||
),
|
||||
);
|
||||
}),
|
||||
createPageItem(
|
||||
context: context,
|
||||
id: createPageId,
|
||||
onTap: () => onPageCreate(context),
|
||||
),
|
||||
if (showMoreResult)
|
||||
MoreResultsItem(
|
||||
num: recentViews.length - 4,
|
||||
onTap: onShowMore,
|
||||
id: showMoreId,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget createPageItem({
|
||||
required BuildContext context,
|
||||
required String id,
|
||||
required VoidCallback onTap,
|
||||
}) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
final state = context.read<MentionBloc>().state, query = state.query;
|
||||
if (query.isEmpty) return const SizedBox.shrink();
|
||||
|
||||
return MentionMenuItenVisibilityDetector(
|
||||
id: id,
|
||||
child: AFTextMenuItem(
|
||||
selected: state.selectedId == id,
|
||||
title: LocaleKeys.inlineActions_createPage.tr(args: [query]),
|
||||
leading: SizedBox.square(
|
||||
dimension: 24,
|
||||
child: Center(
|
||||
child: FlowySvg(
|
||||
FlowySvgs.mention_create_page_m,
|
||||
color: theme.iconColorScheme.primary,
|
||||
size: const Size.square(20.0),
|
||||
),
|
||||
),
|
||||
),
|
||||
backgroundColor: context.mentionItemBGColor,
|
||||
onTap: onTap,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> onPageSelected(ViewPB view, BuildContext context) async {
|
||||
final mentionInfo = context.read<MentionMenuServiceInfo>(),
|
||||
editorState = mentionInfo.editorState,
|
||||
query = context.read<MentionBloc>().state.query;
|
||||
final selection = editorState.selection;
|
||||
if (selection == null || !selection.isCollapsed) return;
|
||||
|
||||
final node = editorState.getNodeAtPath(selection.end.path);
|
||||
final delta = node?.delta;
|
||||
if (node == null || delta == null) return;
|
||||
|
||||
final range = mentionInfo.textRange(query);
|
||||
mentionInfo.onDismiss.call();
|
||||
await editorState.insertPageLinkRef(view, (range.start, range.end));
|
||||
}
|
||||
|
||||
Future<void> onPageCreate(BuildContext context) async {
|
||||
final mentionInfo = context.read<MentionMenuServiceInfo>(),
|
||||
editorState = mentionInfo.editorState,
|
||||
query = context.read<MentionBloc>().state.query,
|
||||
documentBloc = context.read<DocumentBloc?>();
|
||||
|
||||
if (query.isEmpty || documentBloc == null) return;
|
||||
final selection = editorState.selection;
|
||||
if (selection == null || !selection.isCollapsed) return;
|
||||
|
||||
final node = editorState.getNodeAtPath(selection.end.path);
|
||||
final delta = node?.delta;
|
||||
if (node == null || delta == null) return;
|
||||
|
||||
final range = mentionInfo.textRange(query);
|
||||
mentionInfo.onDismiss.call();
|
||||
await editorState.insertChildPage(
|
||||
documentBloc.documentId,
|
||||
(range.start, range.end),
|
||||
query,
|
||||
);
|
||||
}
|
||||
}
|
||||
-191
@@ -1,191 +0,0 @@
|
||||
import 'package:appflowy/features/mension_person/presentation/mention_menu.dart';
|
||||
import 'package:appflowy/features/mension_person/presentation/mention_menu_service.dart';
|
||||
import 'package:appflowy/features/workspace/logic/workspace_bloc.dart';
|
||||
import 'package:appflowy/plugins/document/application/document_bloc.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_block.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/workspace.pbenum.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:appflowy/features/mension_person/data/models/person.dart';
|
||||
import 'package:appflowy/features/mension_person/data/models/mention_menu_item.dart';
|
||||
import 'package:appflowy/features/mension_person/logic/mention_bloc.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';
|
||||
import 'package:appflowy_ui/appflowy_ui.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../invite/person_list_invite_item.dart';
|
||||
import '../item_visibility_detector.dart';
|
||||
import '../more_results_item.dart';
|
||||
|
||||
class PersonList extends StatelessWidget {
|
||||
const PersonList({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final state = context.read<MentionBloc>().state,
|
||||
itemMap = context.read<MentionItemMap>(),
|
||||
theme = AppFlowyTheme.of(context),
|
||||
spacing = theme.spacing;
|
||||
final workspaceType = context
|
||||
.read<UserWorkspaceBloc?>()
|
||||
?.state
|
||||
.currentWorkspace
|
||||
?.workspaceType;
|
||||
if (workspaceType == WorkspaceTypePB.LocalW) return const SizedBox.shrink();
|
||||
final persons = state.persons, showMorePersons = state.showMorePersons;
|
||||
final hasMorePersons = persons.length > 4;
|
||||
final showMoreResult = !showMorePersons && hasMorePersons;
|
||||
List<Person> displayPersons = List.of(persons);
|
||||
if (showMoreResult) {
|
||||
displayPersons = persons.sublist(0, 4);
|
||||
}
|
||||
|
||||
for (final person in displayPersons) {
|
||||
itemMap.addToPerson(
|
||||
MentionMenuItem(
|
||||
id: person.id,
|
||||
onExecute: () => onPersonSelected(person, context),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final id = LocaleKeys.document_mentionMenu_moreResults
|
||||
.tr(args: ['show more person']);
|
||||
void onShowMore() {
|
||||
if (!showMoreResult) return;
|
||||
context
|
||||
.read<MentionBloc>()
|
||||
.add(MentionEvent.showMorePersons(persons[4].id));
|
||||
}
|
||||
|
||||
if (showMoreResult) {
|
||||
itemMap.addToPerson(MentionMenuItem(id: id, onExecute: onShowMore));
|
||||
}
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Padding(
|
||||
padding: EdgeInsets.all(spacing.m),
|
||||
child: AFMenuSection(
|
||||
title: LocaleKeys.document_mentionMenu_people.tr(),
|
||||
titleTrailing: sendNotificationSwitch(context),
|
||||
children: [
|
||||
...List.generate(displayPersons.length, (index) {
|
||||
final person = displayPersons[index];
|
||||
return MentionMenuItenVisibilityDetector(
|
||||
id: person.id,
|
||||
child: AFTextMenuItem(
|
||||
leading:
|
||||
AFAvatar(url: person.avatarUrl, size: AFAvatarSize.s),
|
||||
selected: state.selectedId == person.id,
|
||||
title: person.name,
|
||||
subtitle: person.email,
|
||||
backgroundColor: context.mentionItemBGColor,
|
||||
onTap: () => onPersonSelected(person, context),
|
||||
),
|
||||
);
|
||||
}),
|
||||
if (showMoreResult)
|
||||
MoreResultsItem(
|
||||
num: persons.length - 4,
|
||||
onTap: onShowMore,
|
||||
id: id,
|
||||
),
|
||||
PersonListInviteItem(),
|
||||
],
|
||||
),
|
||||
),
|
||||
AFDivider(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget sendNotificationSwitch(BuildContext context) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
final bloc = context.read<MentionBloc>(), state = bloc.state;
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
Text(
|
||||
LocaleKeys.document_mentionMenu_sendNotification.tr(),
|
||||
style: theme.textStyle.caption
|
||||
.standard(color: theme.textColorScheme.secondary)
|
||||
.copyWith(letterSpacing: 0.1),
|
||||
),
|
||||
SizedBox(width: 4),
|
||||
Toggle(
|
||||
value: state.sendNotification,
|
||||
style: ToggleStyle(width: 34, height: 18, thumbRadius: 17),
|
||||
padding: EdgeInsets.zero,
|
||||
inactiveBackgroundColor: theme.fillColorScheme.secondary,
|
||||
onChanged: (v) {
|
||||
bloc.add(MentionEvent.toggleSendNotification());
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> onPersonSelected(
|
||||
Person person,
|
||||
BuildContext context,
|
||||
) async {
|
||||
final mentionInfo = context.read<MentionMenuServiceInfo>(),
|
||||
editorState = mentionInfo.editorState,
|
||||
mentionBloc = context.read<MentionBloc>(),
|
||||
documentBloc = context.read<DocumentBloc>(),
|
||||
mentionState = mentionBloc.state,
|
||||
query = mentionState.query;
|
||||
final selection = editorState.selection;
|
||||
if (selection == null || !selection.isCollapsed) return;
|
||||
|
||||
final node = editorState.getNodeAtPath(selection.end.path);
|
||||
final delta = node?.delta;
|
||||
if (node == null || delta == null) return;
|
||||
|
||||
final range = mentionInfo.textRange(query);
|
||||
mentionInfo.onDismiss.call();
|
||||
await editorState.insertPerson(
|
||||
person,
|
||||
documentBloc.documentId,
|
||||
range,
|
||||
mentionState.sendNotification,
|
||||
selection,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension PersonListEditorStateExtension on EditorState {
|
||||
Future<void> insertPerson(
|
||||
Person person,
|
||||
String pageId,
|
||||
TextRange range,
|
||||
bool sendNotification,
|
||||
Selection? selection,
|
||||
) async {
|
||||
final mSelection = selection ?? this.selection;
|
||||
if (mSelection == null || !mSelection.isCollapsed) return;
|
||||
|
||||
final node = getNodeAtPath(mSelection.start.path);
|
||||
final delta = node?.delta;
|
||||
if (node == null || delta == null) return;
|
||||
|
||||
final transaction = this.transaction
|
||||
..replaceText(
|
||||
node,
|
||||
range.start,
|
||||
range.end,
|
||||
MentionBlockKeys.mentionChar,
|
||||
attributes: MentionBlockKeys.buildMentionPersonAttributes(
|
||||
personId: person.id,
|
||||
pageId: pageId,
|
||||
),
|
||||
);
|
||||
|
||||
await apply(transaction);
|
||||
}
|
||||
}
|
||||
-307
@@ -1,307 +0,0 @@
|
||||
import 'package:appflowy/core/helpers/url_launcher.dart';
|
||||
import 'package:appflowy/features/mension_person/data/models/person.dart';
|
||||
import 'package:appflowy/features/mension_person/logic/person_bloc.dart';
|
||||
import 'package:appflowy/features/mension_person/presentation/widgets/person/person_role_badge.dart';
|
||||
import 'package:appflowy/features/mension_person/presentation/widgets/profile_card_more_button.dart';
|
||||
import 'package:appflowy/features/mension_person/presentation/widgets/profile_invite_button.dart';
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy_ui/appflowy_ui.dart';
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class PersonProfileCard extends StatelessWidget {
|
||||
const PersonProfileCard({
|
||||
super.key,
|
||||
required this.triggerSize,
|
||||
required this.showAtBottom,
|
||||
this.onEnter,
|
||||
this.onExit,
|
||||
});
|
||||
|
||||
final Size triggerSize;
|
||||
final bool showAtBottom;
|
||||
final PointerEnterEventListener? onEnter;
|
||||
final PointerExitEventListener? onExit;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final mouseRegionPlaceHolder = MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: Container(
|
||||
width: triggerSize.width,
|
||||
height: triggerSize.height,
|
||||
color: Colors.black.withAlpha(1),
|
||||
),
|
||||
);
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: showAtBottom
|
||||
? [mouseRegionPlaceHolder, buildDecoratedCard(context)]
|
||||
: [buildDecoratedCard(context), mouseRegionPlaceHolder],
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildDecoratedCard(BuildContext context) => DecoratedBox(
|
||||
decoration: buildCardDecoration(context),
|
||||
child: buildCard(context),
|
||||
);
|
||||
|
||||
Widget buildCard(BuildContext context) {
|
||||
final theme = AppFlowyTheme.of(context), xxl = theme.spacing.xxl;
|
||||
final personState = context.read<PersonBloc>().state,
|
||||
person = personState.person;
|
||||
if (person == null) return const SizedBox.shrink();
|
||||
|
||||
final hasCover = person.coverImageUrl?.isNotEmpty ?? false;
|
||||
return Stack(
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
buildCover(context),
|
||||
SizedBox(
|
||||
width: 280,
|
||||
child: Padding(
|
||||
padding:
|
||||
EdgeInsets.fromLTRB(xxl, hasCover ? 60 : xxl, xxl, xxl),
|
||||
child: buildPersonInfo(context),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Positioned(
|
||||
left: xxl,
|
||||
top: hasCover ? 38 : xxl,
|
||||
child: buildAvatar(context),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildCover(BuildContext context) {
|
||||
final personState = context.read<PersonBloc>().state;
|
||||
final person = personState.person, url = person?.coverImageUrl ?? '';
|
||||
if (url.isEmpty) return VSpace(100);
|
||||
final theme = AppFlowyTheme.of(context), spaceM = theme.spacing.m;
|
||||
return Container(
|
||||
width: 280,
|
||||
height: 88,
|
||||
padding: EdgeInsets.fromLTRB(spaceM, spaceM, spaceM, 0),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(theme.spacing.m),
|
||||
child: CachedNetworkImage(imageUrl: url, fit: BoxFit.cover),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildAvatar(BuildContext context) {
|
||||
final personState = context.read<PersonBloc>().state;
|
||||
final person = personState.person, url = person?.avatarUrl ?? '';
|
||||
if (url.isEmpty) return const SizedBox.shrink();
|
||||
final hasCover = person?.coverImageUrl?.isNotEmpty ?? false;
|
||||
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
final avatar = SizedBox.square(
|
||||
dimension: 90,
|
||||
child: AFAvatar(
|
||||
url: url,
|
||||
radius: 41,
|
||||
name: person?.name,
|
||||
backgroundColor: Colors.transparent,
|
||||
),
|
||||
);
|
||||
return DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: theme.surfaceColorScheme.layer01,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: hasCover
|
||||
? SizedBox.square(
|
||||
dimension: 100,
|
||||
child: Center(child: avatar),
|
||||
)
|
||||
: avatar,
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildPersonInfo(BuildContext context) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
buildName(context),
|
||||
buildEmail(context),
|
||||
buildDescription(context),
|
||||
VSpace(theme.spacing.xxl),
|
||||
buildActions(context),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildName(BuildContext context) {
|
||||
final personState = context.read<PersonBloc>().state;
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
final suffixIcon = context.buildSuffixIcon();
|
||||
return Row(
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
personState.person?.name ?? '',
|
||||
style: theme.textStyle.title
|
||||
.prominent(color: theme.textColorScheme.primary),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
if (suffixIcon != null) suffixIcon,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildEmail(BuildContext context) {
|
||||
final person = context.read<PersonBloc>().state.person;
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
return Text(
|
||||
person?.email ?? '',
|
||||
style:
|
||||
theme.textStyle.body.standard(color: theme.textColorScheme.secondary),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildDescription(BuildContext context) {
|
||||
final person = context.read<PersonBloc>().state.person,
|
||||
description = person?.description;
|
||||
if (description?.isEmpty ?? true) return const SizedBox.shrink();
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
return Container(
|
||||
margin: EdgeInsets.only(top: theme.spacing.m),
|
||||
width: double.infinity,
|
||||
child: DecoratedBox(
|
||||
decoration: buildDescriptionDecoration(context),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(theme.spacing.l),
|
||||
child: Text(
|
||||
description!,
|
||||
style: theme.textStyle.caption
|
||||
.standard(color: theme.textColorScheme.primary),
|
||||
maxLines: 4,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildActions(BuildContext context) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
final person = context.read<PersonBloc>().state.person;
|
||||
if (person == null) return const SizedBox.shrink();
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
PersonRoleBadge(role: person.role),
|
||||
Spacer(),
|
||||
context.buildNotificationButton(),
|
||||
HSpace(theme.spacing.m),
|
||||
ProfileCardMoreButton(
|
||||
onEnter: onEnter,
|
||||
onExit: onExit,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Decoration buildCardDecoration(BuildContext context) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
return BoxDecoration(
|
||||
color: theme.surfaceColorScheme.layer01,
|
||||
borderRadius: BorderRadius.circular(theme.spacing.l),
|
||||
boxShadow: theme.shadow.small,
|
||||
);
|
||||
}
|
||||
|
||||
Decoration buildDescriptionDecoration(BuildContext context) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
return BoxDecoration(
|
||||
color: theme.fillColorScheme.contentVisible,
|
||||
borderRadius: BorderRadius.circular(theme.spacing.m),
|
||||
);
|
||||
}
|
||||
|
||||
void openEmailApp(Person person) {
|
||||
afLaunchUrlString('mailto:${person.email}');
|
||||
}
|
||||
}
|
||||
|
||||
extension PersonProfileCardWidgetExtension on BuildContext {
|
||||
Widget? buildSuffixIcon() {
|
||||
final personState = read<PersonBloc>().state,
|
||||
person = personState.person,
|
||||
access = personState.access,
|
||||
theme = AppFlowyTheme.of(this);
|
||||
if (person == null) return null;
|
||||
if (!access) {
|
||||
return FlowySvg(
|
||||
FlowySvgs.no_access_suffix_icon_m,
|
||||
color: theme.iconColorScheme.tertiary,
|
||||
blendMode: null,
|
||||
);
|
||||
}
|
||||
if (person.role == PersonRole.contact) {
|
||||
return FlowySvg(
|
||||
FlowySvgs.contact_suffix_icon_m,
|
||||
color: theme.iconColorScheme.tertiary,
|
||||
blendMode: null,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Widget buildNotificationButton() {
|
||||
final theme = AppFlowyTheme.of(this);
|
||||
final personBloc = read<PersonBloc>(),
|
||||
personState = personBloc.state,
|
||||
person = personState.person;
|
||||
if (person == null) return const SizedBox.shrink();
|
||||
final hasAccess = personState.access,
|
||||
isContact = person.role == PersonRole.contact;
|
||||
if (isContact) {
|
||||
return AFOutlinedButton.normal(
|
||||
padding: EdgeInsets.all(theme.spacing.s),
|
||||
builder: (context, hovering, disabled) {
|
||||
return FlowySvg(
|
||||
FlowySvgs.mention_send_email_m,
|
||||
size: Size.square(20),
|
||||
color: theme.iconColorScheme.primary,
|
||||
);
|
||||
},
|
||||
onTap: () => afLaunchUrlString('mailto:${person.email}'),
|
||||
);
|
||||
}
|
||||
if (!hasAccess) {
|
||||
return ProfileInviteButton(
|
||||
onTap: () {},
|
||||
);
|
||||
}
|
||||
return AFOutlinedButton.normal(
|
||||
padding: EdgeInsets.all(theme.spacing.s),
|
||||
builder: (context, hovering, disabled) {
|
||||
return FlowySvg(
|
||||
FlowySvgs.mention_send_notification_m,
|
||||
size: Size.square(20),
|
||||
color: theme.iconColorScheme.primary,
|
||||
);
|
||||
},
|
||||
onTap: () {},
|
||||
);
|
||||
}
|
||||
}
|
||||
-79
@@ -1,79 +0,0 @@
|
||||
import 'package:appflowy/features/mension_person/data/models/person.dart';
|
||||
import 'package:appflowy/util/theme_extension.dart';
|
||||
import 'package:appflowy_ui/appflowy_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class PersonRoleBadge extends StatelessWidget {
|
||||
const PersonRoleBadge({super.key, required this.role});
|
||||
|
||||
final PersonRole role;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = AppFlowyTheme.of(context), spacing = theme.spacing;
|
||||
final paddingLeft = role == PersonRole.contact ? spacing.m : spacing.xs;
|
||||
return DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(theme.spacing.s),
|
||||
border: Border.all(color: theme.borderColorScheme.primary),
|
||||
),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.fromLTRB(paddingLeft, 2, spacing.m, 2),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
buildDot(context),
|
||||
Text(
|
||||
role.name.capitalize(),
|
||||
style: theme.textStyle.body.standard(
|
||||
color: color(context),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildDot(BuildContext context) {
|
||||
if (role == PersonRole.contact) return const SizedBox.shrink();
|
||||
final theme = AppFlowyTheme.of(context), spacing = theme.spacing;
|
||||
|
||||
return SizedBox.square(
|
||||
dimension: 20,
|
||||
child: Center(
|
||||
child: Container(
|
||||
width: spacing.m,
|
||||
height: spacing.m,
|
||||
decoration: BoxDecoration(
|
||||
color: color(context),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Color color(BuildContext context) {
|
||||
final theme = AppFlowyTheme.of(context),
|
||||
isLight = Theme.of(context).isLightMode;
|
||||
switch (role) {
|
||||
case PersonRole.member:
|
||||
return isLight
|
||||
? theme.badgeColorScheme.color15Thick2
|
||||
: theme.badgeColorScheme.color15Thick1;
|
||||
case PersonRole.guest:
|
||||
return isLight
|
||||
? theme.badgeColorScheme.color3Thick2
|
||||
: theme.badgeColorScheme.color3Thick1;
|
||||
case PersonRole.contact:
|
||||
return theme.textColorScheme.tertiary;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension StringExtension on String {
|
||||
String capitalize() {
|
||||
return "${this[0].toUpperCase()}${substring(1).toLowerCase()}";
|
||||
}
|
||||
}
|
||||
-235
@@ -1,235 +0,0 @@
|
||||
import 'package:appflowy/features/mension_person/data/models/person.dart';
|
||||
import 'package:appflowy/features/mension_person/logic/person_bloc.dart';
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_ui/appflowy_ui.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:universal_platform/universal_platform.dart';
|
||||
|
||||
class ProfileCardMoreButton extends StatefulWidget {
|
||||
const ProfileCardMoreButton({super.key, this.onEnter, this.onExit});
|
||||
|
||||
final PointerEnterEventListener? onEnter;
|
||||
final PointerExitEventListener? onExit;
|
||||
|
||||
@override
|
||||
State<ProfileCardMoreButton> createState() => _ProfileCardMoreButtonState();
|
||||
}
|
||||
|
||||
class _ProfileCardMoreButtonState extends State<ProfileCardMoreButton> {
|
||||
PointerEnterEventListener? get onEnter => widget.onEnter;
|
||||
PointerExitEventListener? get onExit => widget.onExit;
|
||||
|
||||
final popoverController = PopoverController();
|
||||
final menuShowingNotifier = ValueNotifier<bool>(false);
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
menuShowingNotifier.dispose();
|
||||
hide();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final personBloc = context.read<PersonBloc>(),
|
||||
person = personBloc.state.person;
|
||||
if (person == null) return const SizedBox.shrink();
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
|
||||
return AppFlowyPopover(
|
||||
offset: Offset(0, -2),
|
||||
direction: UniversalPlatform.isMobile
|
||||
? PopoverDirection.topWithRightAligned
|
||||
: PopoverDirection.topWithLeftAligned,
|
||||
margin: EdgeInsets.zero,
|
||||
controller: popoverController,
|
||||
onOpen: () => keepEditorFocusNotifier.increase(),
|
||||
onClose: () => keepEditorFocusNotifier.decrease(),
|
||||
popoverDecoration: BoxDecoration(),
|
||||
popupBuilder: (context) => BlocProvider.value(
|
||||
value: personBloc,
|
||||
child: BlocBuilder<PersonBloc, PersonState>(
|
||||
builder: (ctx, state) => MouseRegion(
|
||||
onEnter: onEnter,
|
||||
onExit: onExit,
|
||||
child: _Menu(),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: ValueListenableBuilder(
|
||||
valueListenable: menuShowingNotifier,
|
||||
builder: (context, showing, child) {
|
||||
return AFOutlinedButton.normal(
|
||||
backgroundColor: (context, isHovering, disabled) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
if (isHovering || showing) {
|
||||
return theme.fillColorScheme.contentHover;
|
||||
}
|
||||
return theme.fillColorScheme.content;
|
||||
},
|
||||
padding: EdgeInsets.all(theme.spacing.s),
|
||||
builder: (context, hovering, disabled) {
|
||||
return FlowySvg(
|
||||
FlowySvgs.mention_more_results_m,
|
||||
size: Size.square(20),
|
||||
color: theme.iconColorScheme.primary,
|
||||
);
|
||||
},
|
||||
onTap: show,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void show() {
|
||||
menuShowingNotifier.value = true;
|
||||
popoverController.show();
|
||||
}
|
||||
|
||||
void hide() {
|
||||
popoverController.close();
|
||||
}
|
||||
}
|
||||
|
||||
class _Menu extends StatelessWidget {
|
||||
const _Menu();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final personBloc = context.read<PersonBloc>(),
|
||||
person = personBloc.state.person;
|
||||
if (person == null) return const SizedBox.shrink();
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
final role = person.role;
|
||||
List<Widget> children = [];
|
||||
if (role == PersonRole.member) {
|
||||
children = buildMemberItems(person, context);
|
||||
} else if (role == PersonRole.guest) {
|
||||
children = buildGuestItems(person, context);
|
||||
} else {
|
||||
children = buildContactItems(person, context);
|
||||
}
|
||||
|
||||
return DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: theme.surfaceColorScheme.layer04,
|
||||
borderRadius: BorderRadius.circular(theme.borderRadius.l),
|
||||
boxShadow: theme.shadow.small,
|
||||
),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(theme.spacing.s),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: children,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> buildMemberItems(Person person, BuildContext context) {
|
||||
return [
|
||||
AFMenuItem(
|
||||
title:
|
||||
context._title(LocaleKeys.document_mentionMenu_shareProfile.tr()),
|
||||
onTap: () {},
|
||||
),
|
||||
AFMenuItem(
|
||||
title: context._title(LocaleKeys.document_mentionMenu_sendEmail.tr()),
|
||||
onTap: () {},
|
||||
),
|
||||
AFMenuItem(
|
||||
title:
|
||||
context._title(LocaleKeys.document_mentionMenu_editInfomation.tr()),
|
||||
onTap: () {},
|
||||
),
|
||||
AFMenuItem(
|
||||
title: context
|
||||
._title(LocaleKeys.document_mentionMenu_changeCoverImage.tr()),
|
||||
onTap: () {},
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
List<Widget> buildGuestItems(Person person, BuildContext context) {
|
||||
return [
|
||||
AFMenuItem(
|
||||
title:
|
||||
context._title(LocaleKeys.document_mentionMenu_shareProfile.tr()),
|
||||
onTap: () {},
|
||||
),
|
||||
AFMenuItem(
|
||||
title: context._title(LocaleKeys.document_mentionMenu_sendEmail.tr()),
|
||||
onTap: () {},
|
||||
),
|
||||
AFMenuItem(
|
||||
title: context
|
||||
._title(LocaleKeys.document_mentionMenu_convertToAMenber.tr()),
|
||||
onTap: () {},
|
||||
),
|
||||
AFMenuItem(
|
||||
title:
|
||||
context._title(LocaleKeys.document_mentionMenu_editInfomation.tr()),
|
||||
onTap: () {},
|
||||
),
|
||||
AFMenuItem(
|
||||
title: context
|
||||
._title(LocaleKeys.document_mentionMenu_changeCoverImage.tr()),
|
||||
onTap: () {},
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
List<Widget> buildContactItems(Person person, BuildContext context) {
|
||||
final invited = person.invited == true;
|
||||
return [
|
||||
AFMenuItem(
|
||||
title:
|
||||
context._title(LocaleKeys.document_mentionMenu_shareProfile.tr()),
|
||||
onTap: () {},
|
||||
),
|
||||
if (!invited)
|
||||
AFMenuItem(
|
||||
title: context
|
||||
._title(LocaleKeys.document_mentionMenu_inviteAsMember.tr()),
|
||||
onTap: () {},
|
||||
),
|
||||
if (!invited)
|
||||
AFMenuItem(
|
||||
title: context
|
||||
._title(LocaleKeys.document_mentionMenu_inviteAsGuest.tr()),
|
||||
onTap: () {},
|
||||
),
|
||||
AFMenuItem(
|
||||
title: context
|
||||
._title(LocaleKeys.document_mentionMenu_editContactInfomation.tr()),
|
||||
onTap: () {},
|
||||
),
|
||||
if (invited)
|
||||
AFMenuItem(
|
||||
title: context
|
||||
._title(LocaleKeys.document_mentionMenu_changeCoverImage.tr()),
|
||||
onTap: () {},
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
extension ProfileCardMenuContextExtension on BuildContext {
|
||||
Widget _title(String title) {
|
||||
final theme = AppFlowyTheme.of(this);
|
||||
return Text(
|
||||
title,
|
||||
style: theme.textStyle.body.standard(
|
||||
color: theme.textColorScheme.primary,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
-41
@@ -1,41 +0,0 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy_ui/appflowy_ui.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ProfileInviteButton extends StatelessWidget {
|
||||
const ProfileInviteButton({
|
||||
super.key,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = AppFlowyTheme.of(context), spacing = theme.spacing;
|
||||
return AFOutlinedButton.normal(
|
||||
onTap: onTap,
|
||||
builder: (context, isHovering, disabled) => Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
FlowySvg(
|
||||
FlowySvgs.mention_invite_user_m,
|
||||
size: Size.square(20),
|
||||
color: theme.iconColorScheme.primary,
|
||||
blendMode: null,
|
||||
),
|
||||
HSpace(spacing.s),
|
||||
Text(
|
||||
LocaleKeys.document_mentionMenu_invite.tr(),
|
||||
style: TextStyle(
|
||||
color: AppFlowyTheme.of(context).textColorScheme.primary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
-33
@@ -1,33 +0,0 @@
|
||||
import 'package:appflowy/features/share_tab/data/models/models.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
|
||||
/// Abstract repository for managing view lock status.
|
||||
///
|
||||
/// For example, we're using rust events now, but we can still use the http api
|
||||
/// for the future.
|
||||
abstract class PageAccessLevelRepository {
|
||||
/// Gets the current view from the backend.
|
||||
Future<FlowyResult<ViewPB, FlowyError>> getView(String pageId);
|
||||
|
||||
/// Locks the view.
|
||||
Future<FlowyResult<void, FlowyError>> lockView(String pageId);
|
||||
|
||||
/// Unlocks the view.
|
||||
Future<FlowyResult<void, FlowyError>> unlockView(String pageId);
|
||||
|
||||
/// Gets the access level of the current user.
|
||||
Future<FlowyResult<ShareAccessLevel, FlowyError>> getAccessLevel(
|
||||
String pageId,
|
||||
);
|
||||
|
||||
/// Gets the section type of the shared section.
|
||||
Future<FlowyResult<SharedSectionType, FlowyError>> getSectionType(
|
||||
String pageId,
|
||||
);
|
||||
|
||||
/// Get current workspace
|
||||
Future<FlowyResult<UserWorkspacePB, FlowyError>> getCurrentWorkspace();
|
||||
}
|
||||
-206
@@ -1,206 +0,0 @@
|
||||
import 'package:appflowy/features/page_access_level/data/repositories/page_access_level_repository.dart';
|
||||
import 'package:appflowy/features/share_tab/data/models/models.dart';
|
||||
import 'package:appflowy/features/util/extensions.dart';
|
||||
import 'package:appflowy/user/application/user_service.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_service.dart';
|
||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/code.pbenum.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'
|
||||
hide AFRolePB;
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
|
||||
class RustPageAccessLevelRepositoryImpl implements PageAccessLevelRepository {
|
||||
@override
|
||||
Future<FlowyResult<ViewPB, FlowyError>> getView(String pageId) async {
|
||||
final result = await ViewBackendService.getView(pageId);
|
||||
return result.fold(
|
||||
(view) {
|
||||
Log.debug('get view(${view.id}) success');
|
||||
return FlowyResult.success(view);
|
||||
},
|
||||
(error) {
|
||||
Log.error('failed to get view, error: $error');
|
||||
return FlowyResult.failure(error);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<FlowyResult<void, FlowyError>> lockView(String pageId) async {
|
||||
final result = await ViewBackendService.lockView(pageId);
|
||||
return result.fold(
|
||||
(_) {
|
||||
Log.debug('lock view($pageId) success');
|
||||
return FlowyResult.success(null);
|
||||
},
|
||||
(error) {
|
||||
Log.error('failed to lock view, error: $error');
|
||||
return FlowyResult.failure(error);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<FlowyResult<void, FlowyError>> unlockView(String pageId) async {
|
||||
final result = await ViewBackendService.unlockView(pageId);
|
||||
return result.fold(
|
||||
(_) {
|
||||
Log.debug('unlock view($pageId) success');
|
||||
return FlowyResult.success(null);
|
||||
},
|
||||
(error) {
|
||||
Log.error('failed to unlock view, error: $error');
|
||||
return FlowyResult.failure(error);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// 1. local users have full access
|
||||
/// 2. local workspace users have full access
|
||||
/// 3. page creator has full access
|
||||
/// 4. owner and members in public page have full access
|
||||
/// 5. check the shared users list
|
||||
@override
|
||||
Future<FlowyResult<ShareAccessLevel, FlowyError>> getAccessLevel(
|
||||
String pageId,
|
||||
) async {
|
||||
final userResult = await UserBackendService.getCurrentUserProfile();
|
||||
final user = userResult.fold(
|
||||
(s) => s,
|
||||
(_) => null,
|
||||
);
|
||||
|
||||
if (user == null) {
|
||||
return FlowyResult.failure(
|
||||
FlowyError(
|
||||
code: ErrorCode.Internal,
|
||||
msg: 'User not found',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (user.userAuthType == AuthTypePB.Local) {
|
||||
// Local user can always have full access.
|
||||
return FlowyResult.success(ShareAccessLevel.fullAccess);
|
||||
}
|
||||
|
||||
if (user.workspaceType == WorkspaceTypePB.LocalW) {
|
||||
// Local workspace can always have full access.
|
||||
return FlowyResult.success(ShareAccessLevel.fullAccess);
|
||||
}
|
||||
|
||||
// If the user is the creator of the page, they can always have full access.
|
||||
final viewResult = await getView(pageId);
|
||||
final view = viewResult.fold(
|
||||
(s) => s,
|
||||
(_) => null,
|
||||
);
|
||||
if (view?.createdBy == user.id) {
|
||||
return FlowyResult.success(ShareAccessLevel.fullAccess);
|
||||
}
|
||||
|
||||
// If the page is public, the user can always have full access.
|
||||
final workspaceResult = await getCurrentWorkspace();
|
||||
final workspace = workspaceResult.fold(
|
||||
(s) => s,
|
||||
(_) => null,
|
||||
);
|
||||
if (workspace == null) {
|
||||
return FlowyResult.failure(
|
||||
FlowyError(
|
||||
code: ErrorCode.Internal,
|
||||
msg: 'Current workspace not found',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final sectionTypeResult = await getSectionType(pageId);
|
||||
final sectionType = sectionTypeResult.fold(
|
||||
(s) => s,
|
||||
(_) => null,
|
||||
);
|
||||
if (sectionType == SharedSectionType.public &&
|
||||
workspace.role != AFRolePB.Guest) {
|
||||
return FlowyResult.success(ShareAccessLevel.fullAccess);
|
||||
}
|
||||
|
||||
final email = user.email;
|
||||
|
||||
final request = GetSharedUsersPayloadPB(
|
||||
viewId: pageId,
|
||||
);
|
||||
final result = await FolderEventGetSharedUsers(request).send();
|
||||
return result.fold(
|
||||
(success) {
|
||||
final accessLevel = success.items
|
||||
.firstWhereOrNull(
|
||||
(item) => item.email == email,
|
||||
)
|
||||
?.accessLevel
|
||||
.shareAccessLevel ??
|
||||
ShareAccessLevel.readOnly;
|
||||
|
||||
Log.debug('current user access level: $accessLevel, in page: $pageId');
|
||||
|
||||
return FlowyResult.success(accessLevel);
|
||||
},
|
||||
(failure) {
|
||||
Log.error(
|
||||
'failed to get user access level: $failure, in page: $pageId',
|
||||
);
|
||||
|
||||
// return the read access level if the user is not found
|
||||
return FlowyResult.success(ShareAccessLevel.readOnly);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<FlowyResult<SharedSectionType, FlowyError>> getSectionType(
|
||||
String pageId,
|
||||
) async {
|
||||
final request = ViewIdPB(value: pageId);
|
||||
final result = await FolderEventGetSharedViewSection(request).send();
|
||||
return result.fold(
|
||||
(success) {
|
||||
final sectionType = success.section.sharedSectionType;
|
||||
Log.debug('shared section type: $sectionType, in page: $pageId');
|
||||
return FlowyResult.success(sectionType);
|
||||
},
|
||||
(failure) {
|
||||
Log.error(
|
||||
'failed to get shared section type: $failure, in page: $pageId',
|
||||
);
|
||||
|
||||
return FlowyResult.failure(failure);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<FlowyResult<UserWorkspacePB, FlowyError>> getCurrentWorkspace() async {
|
||||
final result = await UserBackendService.getCurrentWorkspace();
|
||||
final currentWorkspaceId = result.fold(
|
||||
(s) => s.id,
|
||||
(_) => null,
|
||||
);
|
||||
|
||||
if (currentWorkspaceId == null) {
|
||||
return FlowyResult.failure(
|
||||
FlowyError(
|
||||
code: ErrorCode.Internal,
|
||||
msg: 'Current workspace not found',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final workspaceResult = await UserBackendService.getWorkspaceById(
|
||||
currentWorkspaceId,
|
||||
);
|
||||
return workspaceResult;
|
||||
}
|
||||
}
|
||||
-163
@@ -1,163 +0,0 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:appflowy/features/page_access_level/data/repositories/page_access_level_repository.dart';
|
||||
import 'package:appflowy/features/page_access_level/data/repositories/rust_page_access_level_repository_impl.dart';
|
||||
import 'package:appflowy/features/page_access_level/logic/page_access_level_event.dart';
|
||||
import 'package:appflowy/features/page_access_level/logic/page_access_level_state.dart';
|
||||
import 'package:appflowy/features/share_tab/data/models/models.dart';
|
||||
import 'package:appflowy/shared/feature_flags.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_listener.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:protobuf/protobuf.dart';
|
||||
|
||||
export 'page_access_level_event.dart';
|
||||
export 'page_access_level_state.dart';
|
||||
|
||||
class PageAccessLevelBloc
|
||||
extends Bloc<PageAccessLevelEvent, PageAccessLevelState> {
|
||||
PageAccessLevelBloc({
|
||||
required this.view,
|
||||
this.ignorePageAccessLevel = false,
|
||||
PageAccessLevelRepository? repository,
|
||||
}) : repository = repository ?? RustPageAccessLevelRepositoryImpl(),
|
||||
listener = ViewListener(viewId: view.id),
|
||||
super(PageAccessLevelState.initial(view)) {
|
||||
on<PageAccessLevelInitialEvent>(_onInitial);
|
||||
on<PageAccessLevelLockEvent>(_onLock);
|
||||
on<PageAccessLevelUnlockEvent>(_onUnlock);
|
||||
on<PageAccessLevelUpdateLockStatusEvent>(_onUpdateLockStatus);
|
||||
on<PageAccessLevelUpdateSectionTypeEvent>(_onUpdateSectionType);
|
||||
}
|
||||
|
||||
final ViewPB view;
|
||||
|
||||
// The repository to manage view lock status.
|
||||
// If you need to test this bloc, you can add your own repository implementation.
|
||||
final PageAccessLevelRepository repository;
|
||||
|
||||
// Used to listen for view updates.
|
||||
late final ViewListener listener;
|
||||
|
||||
// should ignore the page access level
|
||||
// in the row details page, we don't need to check the page access level
|
||||
final bool ignorePageAccessLevel;
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
await listener.stop();
|
||||
return super.close();
|
||||
}
|
||||
|
||||
Future<void> _onInitial(
|
||||
PageAccessLevelInitialEvent event,
|
||||
Emitter<PageAccessLevelState> emit,
|
||||
) async {
|
||||
// lock status
|
||||
listener.start(
|
||||
onViewUpdated: (view) async {
|
||||
add(PageAccessLevelEvent.updateLockStatus(view.isLocked));
|
||||
},
|
||||
);
|
||||
|
||||
// section type
|
||||
final sectionTypeResult = await repository.getSectionType(view.id);
|
||||
final sectionType = sectionTypeResult.fold(
|
||||
(sectionType) => sectionType,
|
||||
(_) => SharedSectionType.public,
|
||||
);
|
||||
|
||||
if (!FeatureFlag.sharedSection.isOn || ignorePageAccessLevel) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
view: view,
|
||||
isLocked: view.isLocked,
|
||||
isLoadingLockStatus: false,
|
||||
accessLevel: ShareAccessLevel.fullAccess,
|
||||
sectionType: sectionType,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final result = await repository.getView(view.id);
|
||||
final accessLevel = await repository.getAccessLevel(view.id);
|
||||
final latestView = result.fold(
|
||||
(view) => view,
|
||||
(_) => view,
|
||||
);
|
||||
emit(
|
||||
state.copyWith(
|
||||
view: latestView,
|
||||
isLocked: latestView.isLocked,
|
||||
isLoadingLockStatus: false,
|
||||
accessLevel: accessLevel.fold(
|
||||
(accessLevel) => accessLevel,
|
||||
(_) => ShareAccessLevel.readOnly,
|
||||
),
|
||||
sectionType: sectionType,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onLock(
|
||||
PageAccessLevelLockEvent event,
|
||||
Emitter<PageAccessLevelState> emit,
|
||||
) async {
|
||||
final result = await repository.lockView(view.id);
|
||||
final isLocked = result.fold(
|
||||
(_) => true,
|
||||
(_) => false,
|
||||
);
|
||||
add(
|
||||
PageAccessLevelEvent.updateLockStatus(
|
||||
isLocked,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onUnlock(
|
||||
PageAccessLevelUnlockEvent event,
|
||||
Emitter<PageAccessLevelState> emit,
|
||||
) async {
|
||||
final result = await repository.unlockView(view.id);
|
||||
final isLocked = result.fold(
|
||||
(_) => false,
|
||||
(_) => true,
|
||||
);
|
||||
add(
|
||||
PageAccessLevelEvent.updateLockStatus(
|
||||
isLocked,
|
||||
lockCounter: state.lockCounter + 1,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onUpdateLockStatus(
|
||||
PageAccessLevelUpdateLockStatusEvent event,
|
||||
Emitter<PageAccessLevelState> emit,
|
||||
) {
|
||||
state.view.freeze();
|
||||
final updatedView = state.view.rebuild(
|
||||
(update) => update.isLocked = event.isLocked,
|
||||
);
|
||||
emit(
|
||||
state.copyWith(
|
||||
view: updatedView,
|
||||
isLocked: event.isLocked,
|
||||
lockCounter: event.lockCounter ?? state.lockCounter,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onUpdateSectionType(
|
||||
PageAccessLevelUpdateSectionTypeEvent event,
|
||||
Emitter<PageAccessLevelState> emit,
|
||||
) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
sectionType: event.sectionType,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
-55
@@ -1,55 +0,0 @@
|
||||
import 'package:appflowy/features/share_tab/data/models/models.dart';
|
||||
|
||||
/// Base class for all PageAccessLevel events
|
||||
sealed class PageAccessLevelEvent {
|
||||
const PageAccessLevelEvent();
|
||||
|
||||
/// Initialize the view lock status, it will create a view listener to listen for view updates.
|
||||
/// Also, it will fetch the current view lock status from the repository.
|
||||
const factory PageAccessLevelEvent.initial() = PageAccessLevelInitialEvent;
|
||||
|
||||
/// Lock the view.
|
||||
const factory PageAccessLevelEvent.lock() = PageAccessLevelLockEvent;
|
||||
|
||||
/// Unlock the view.
|
||||
const factory PageAccessLevelEvent.unlock() = PageAccessLevelUnlockEvent;
|
||||
|
||||
/// Update the lock status in the state.
|
||||
const factory PageAccessLevelEvent.updateLockStatus(
|
||||
bool isLocked, {
|
||||
int? lockCounter,
|
||||
}) = PageAccessLevelUpdateLockStatusEvent;
|
||||
|
||||
/// Update the section type in the state.
|
||||
const factory PageAccessLevelEvent.updateSectionType(
|
||||
SharedSectionType sectionType,
|
||||
) = PageAccessLevelUpdateSectionTypeEvent;
|
||||
}
|
||||
|
||||
class PageAccessLevelInitialEvent extends PageAccessLevelEvent {
|
||||
const PageAccessLevelInitialEvent();
|
||||
}
|
||||
|
||||
class PageAccessLevelLockEvent extends PageAccessLevelEvent {
|
||||
const PageAccessLevelLockEvent();
|
||||
}
|
||||
|
||||
class PageAccessLevelUnlockEvent extends PageAccessLevelEvent {
|
||||
const PageAccessLevelUnlockEvent();
|
||||
}
|
||||
|
||||
class PageAccessLevelUpdateLockStatusEvent extends PageAccessLevelEvent {
|
||||
const PageAccessLevelUpdateLockStatusEvent(
|
||||
this.isLocked, {
|
||||
this.lockCounter,
|
||||
});
|
||||
|
||||
final bool isLocked;
|
||||
final int? lockCounter;
|
||||
}
|
||||
|
||||
class PageAccessLevelUpdateSectionTypeEvent extends PageAccessLevelEvent {
|
||||
const PageAccessLevelUpdateSectionTypeEvent(this.sectionType);
|
||||
|
||||
final SharedSectionType sectionType;
|
||||
}
|
||||
-97
@@ -1,97 +0,0 @@
|
||||
import 'package:appflowy/features/share_tab/data/models/models.dart';
|
||||
import 'package:appflowy/shared/feature_flags.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
|
||||
class PageAccessLevelState {
|
||||
factory PageAccessLevelState.initial(ViewPB view) => PageAccessLevelState(
|
||||
view: view,
|
||||
isLocked: false,
|
||||
lockCounter: 0,
|
||||
sectionType: SharedSectionType.public,
|
||||
accessLevel: ShareAccessLevel.readOnly,
|
||||
);
|
||||
|
||||
const PageAccessLevelState({
|
||||
required this.view,
|
||||
required this.isLocked,
|
||||
required this.lockCounter,
|
||||
required this.accessLevel,
|
||||
required this.sectionType,
|
||||
this.myRole,
|
||||
this.isLoadingLockStatus = true,
|
||||
});
|
||||
|
||||
final ViewPB view;
|
||||
final bool isLocked;
|
||||
final int lockCounter;
|
||||
final bool isLoadingLockStatus;
|
||||
final ShareAccessLevel accessLevel;
|
||||
final SharedSectionType sectionType;
|
||||
final ShareRole? myRole;
|
||||
|
||||
bool get isPublic => sectionType == SharedSectionType.public;
|
||||
bool get isPrivate => sectionType == SharedSectionType.private;
|
||||
bool get isShared => sectionType == SharedSectionType.shared;
|
||||
bool get shouldHideSpace => myRole == ShareRole.guest;
|
||||
|
||||
bool get isEditable {
|
||||
if (!FeatureFlag.sharedSection.isOn) {
|
||||
return !isLocked;
|
||||
}
|
||||
|
||||
return accessLevel != ShareAccessLevel.readOnly && !isLocked;
|
||||
}
|
||||
|
||||
bool get isReadOnly => accessLevel == ShareAccessLevel.readOnly;
|
||||
|
||||
PageAccessLevelState copyWith({
|
||||
ViewPB? view,
|
||||
bool? isLocked,
|
||||
int? lockCounter,
|
||||
bool? isLoadingLockStatus,
|
||||
ShareAccessLevel? accessLevel,
|
||||
SharedSectionType? sectionType,
|
||||
ShareRole? myRole,
|
||||
}) {
|
||||
return PageAccessLevelState(
|
||||
view: view ?? this.view,
|
||||
isLocked: isLocked ?? this.isLocked,
|
||||
lockCounter: lockCounter ?? this.lockCounter,
|
||||
isLoadingLockStatus: isLoadingLockStatus ?? this.isLoadingLockStatus,
|
||||
accessLevel: accessLevel ?? this.accessLevel,
|
||||
sectionType: sectionType ?? this.sectionType,
|
||||
myRole: myRole ?? this.myRole,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
return other is PageAccessLevelState &&
|
||||
other.view == view &&
|
||||
other.isLocked == isLocked &&
|
||||
other.lockCounter == lockCounter &&
|
||||
other.isLoadingLockStatus == isLoadingLockStatus &&
|
||||
other.accessLevel == accessLevel &&
|
||||
other.sectionType == sectionType &&
|
||||
other.myRole == myRole;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return Object.hash(
|
||||
view,
|
||||
isLocked,
|
||||
lockCounter,
|
||||
isLoadingLockStatus,
|
||||
accessLevel,
|
||||
sectionType,
|
||||
myRole,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'PageAccessLevelState(view: $view, isLocked: $isLocked, lockCounter: $lockCounter, isLoadingLockStatus: $isLoadingLockStatus, accessLevel: $accessLevel, sectionType: $sectionType, myRole: $myRole)';
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
class UserDataLocation extends Equatable {
|
||||
const UserDataLocation({
|
||||
required this.path,
|
||||
required this.isCustom,
|
||||
});
|
||||
|
||||
final String path;
|
||||
final bool isCustom;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [path, isCustom];
|
||||
}
|
||||
-61
@@ -1,61 +0,0 @@
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/startup/tasks/rust_sdk.dart';
|
||||
import 'package:appflowy/user/application/user_settings_service.dart';
|
||||
import 'package:appflowy/workspace/application/settings/prelude.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/protobuf.dart';
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
|
||||
import '../models/user_data_location.dart';
|
||||
import 'settings_repository.dart';
|
||||
|
||||
class RustSettingsRepositoryImpl implements SettingsRepository {
|
||||
const RustSettingsRepositoryImpl();
|
||||
|
||||
final _userBackendService = const UserSettingsBackendService();
|
||||
|
||||
@override
|
||||
Future<FlowyResult<UserDataLocation, FlowyError>>
|
||||
getUserDataLocation() async {
|
||||
final defaultDirectory = (await appFlowyApplicationDataDirectory()).path;
|
||||
final result = await _userBackendService.getUserSetting();
|
||||
|
||||
return result.map(
|
||||
(settings) {
|
||||
final userDirectory = settings.userFolder;
|
||||
return UserDataLocation(
|
||||
path: userDirectory,
|
||||
isCustom: userDirectory.contains(defaultDirectory),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<FlowyResult<UserDataLocation, FlowyError>>
|
||||
resetUserDataLocation() async {
|
||||
final directory = await appFlowyApplicationDataDirectory();
|
||||
await getIt<ApplicationDataStorage>().setPath(directory.path);
|
||||
|
||||
return FlowyResult.success(
|
||||
UserDataLocation(
|
||||
path: directory.path,
|
||||
isCustom: false,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<FlowyResult<UserDataLocation, FlowyError>> setCustomLocation(
|
||||
String path,
|
||||
) async {
|
||||
final defaultDirectory = (await appFlowyApplicationDataDirectory()).path;
|
||||
await getIt<ApplicationDataStorage>().setCustomPath(path);
|
||||
|
||||
return FlowyResult.success(
|
||||
UserDataLocation(
|
||||
path: path,
|
||||
isCustom: path.contains(defaultDirectory),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
-14
@@ -1,14 +0,0 @@
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/protobuf.dart';
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
|
||||
import '../models/user_data_location.dart';
|
||||
|
||||
abstract class SettingsRepository {
|
||||
Future<FlowyResult<UserDataLocation, FlowyError>> getUserDataLocation();
|
||||
|
||||
Future<FlowyResult<UserDataLocation, FlowyError>> resetUserDataLocation();
|
||||
|
||||
Future<FlowyResult<UserDataLocation, FlowyError>> setCustomLocation(
|
||||
String path,
|
||||
);
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../data/repositories/settings_repository.dart';
|
||||
import 'data_location_event.dart';
|
||||
import 'data_location_state.dart';
|
||||
|
||||
class DataLocationBloc extends Bloc<DataLocationEvent, DataLocationState> {
|
||||
DataLocationBloc({
|
||||
required SettingsRepository repository,
|
||||
}) : _repository = repository,
|
||||
super(DataLocationState.initial()) {
|
||||
on<DataLocationInitial>(_onStarted);
|
||||
on<DataLocationResetToDefault>(_onResetToDefault);
|
||||
on<DataLocationSetCustomPath>(_onSetCustomPath);
|
||||
on<DataLocationClearState>(_onClearState);
|
||||
}
|
||||
|
||||
final SettingsRepository _repository;
|
||||
|
||||
Future<void> _onStarted(
|
||||
DataLocationInitial event,
|
||||
Emitter<DataLocationState> emit,
|
||||
) async {
|
||||
final userDataLocation =
|
||||
await _repository.getUserDataLocation().toNullable();
|
||||
|
||||
emit(
|
||||
DataLocationState(
|
||||
userDataLocation: userDataLocation,
|
||||
didResetToDefault: false,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onResetToDefault(
|
||||
DataLocationResetToDefault event,
|
||||
Emitter<DataLocationState> emit,
|
||||
) async {
|
||||
final defaultLocation =
|
||||
await _repository.resetUserDataLocation().toNullable();
|
||||
|
||||
emit(
|
||||
DataLocationState(
|
||||
userDataLocation: defaultLocation,
|
||||
didResetToDefault: true,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onClearState(
|
||||
DataLocationClearState event,
|
||||
Emitter<DataLocationState> emit,
|
||||
) async {
|
||||
emit(
|
||||
state.copyWith(
|
||||
didResetToDefault: false,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onSetCustomPath(
|
||||
DataLocationSetCustomPath event,
|
||||
Emitter<DataLocationState> emit,
|
||||
) async {
|
||||
final userDataLocation =
|
||||
await _repository.setCustomLocation(event.path).toNullable();
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
userDataLocation: userDataLocation,
|
||||
didResetToDefault: false,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
sealed class DataLocationEvent {
|
||||
const DataLocationEvent();
|
||||
|
||||
factory DataLocationEvent.initial() = DataLocationInitial;
|
||||
|
||||
factory DataLocationEvent.resetToDefault() = DataLocationResetToDefault;
|
||||
|
||||
factory DataLocationEvent.setCustomPath(String path) =
|
||||
DataLocationSetCustomPath;
|
||||
|
||||
factory DataLocationEvent.clearState() = DataLocationClearState;
|
||||
}
|
||||
|
||||
class DataLocationInitial extends DataLocationEvent {}
|
||||
|
||||
class DataLocationResetToDefault extends DataLocationEvent {}
|
||||
|
||||
class DataLocationSetCustomPath extends DataLocationEvent {
|
||||
const DataLocationSetCustomPath(this.path);
|
||||
|
||||
final String path;
|
||||
}
|
||||
|
||||
class DataLocationClearState extends DataLocationEvent {}
|
||||
@@ -1,29 +0,0 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
import '../data/models/user_data_location.dart';
|
||||
|
||||
class DataLocationState extends Equatable {
|
||||
const DataLocationState({
|
||||
required this.userDataLocation,
|
||||
required this.didResetToDefault,
|
||||
});
|
||||
|
||||
factory DataLocationState.initial() =>
|
||||
const DataLocationState(userDataLocation: null, didResetToDefault: false);
|
||||
|
||||
final UserDataLocation? userDataLocation;
|
||||
final bool didResetToDefault;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [userDataLocation, didResetToDefault];
|
||||
|
||||
DataLocationState copyWith({
|
||||
UserDataLocation? userDataLocation,
|
||||
bool? didResetToDefault,
|
||||
}) {
|
||||
return DataLocationState(
|
||||
userDataLocation: userDataLocation ?? this.userDataLocation,
|
||||
didResetToDefault: didResetToDefault ?? this.didResetToDefault,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
export 'logic/data_location_bloc.dart';
|
||||
export 'logic/data_location_event.dart';
|
||||
export 'logic/data_location_state.dart';
|
||||
export 'data/models/user_data_location.dart';
|
||||
export 'data/repositories/settings_repository.dart';
|
||||
export 'data/repositories/rust_settings_repository_impl.dart';
|
||||
@@ -1,5 +0,0 @@
|
||||
export 'share_access_level.dart';
|
||||
export 'share_popover_group_id.dart';
|
||||
export 'share_role.dart';
|
||||
export 'shared_user.dart';
|
||||
export 'share_section_type.dart';
|
||||
@@ -1,57 +0,0 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
|
||||
/// The access level a user can have on a shared page.
|
||||
enum ShareAccessLevel {
|
||||
/// Can view the page only.
|
||||
readOnly,
|
||||
|
||||
/// Can read and comment on the page.
|
||||
readAndComment,
|
||||
|
||||
/// Can read and write to the page.
|
||||
readAndWrite,
|
||||
|
||||
/// Full access (edit, share, remove, etc.) and can add new users.
|
||||
fullAccess;
|
||||
|
||||
String get title {
|
||||
switch (this) {
|
||||
case ShareAccessLevel.readOnly:
|
||||
return LocaleKeys.shareTab_accessLevel_view.tr();
|
||||
case ShareAccessLevel.readAndComment:
|
||||
return LocaleKeys.shareTab_accessLevel_comment.tr();
|
||||
case ShareAccessLevel.readAndWrite:
|
||||
return LocaleKeys.shareTab_accessLevel_edit.tr();
|
||||
case ShareAccessLevel.fullAccess:
|
||||
return LocaleKeys.shareTab_accessLevel_fullAccess.tr();
|
||||
}
|
||||
}
|
||||
|
||||
String get subtitle {
|
||||
switch (this) {
|
||||
case ShareAccessLevel.readOnly:
|
||||
return LocaleKeys.shareTab_cantMakeChanges.tr();
|
||||
case ShareAccessLevel.readAndComment:
|
||||
return LocaleKeys.shareTab_canMakeAnyChanges.tr();
|
||||
case ShareAccessLevel.readAndWrite:
|
||||
return LocaleKeys.shareTab_canMakeAnyChanges.tr();
|
||||
case ShareAccessLevel.fullAccess:
|
||||
return LocaleKeys.shareTab_canMakeAnyChanges.tr();
|
||||
}
|
||||
}
|
||||
|
||||
FlowySvgData get icon {
|
||||
switch (this) {
|
||||
case ShareAccessLevel.readOnly:
|
||||
return FlowySvgs.access_level_view_m;
|
||||
case ShareAccessLevel.readAndComment:
|
||||
return FlowySvgs.access_level_edit_m;
|
||||
case ShareAccessLevel.readAndWrite:
|
||||
return FlowySvgs.access_level_edit_m;
|
||||
case ShareAccessLevel.fullAccess:
|
||||
return FlowySvgs.access_level_edit_m;
|
||||
}
|
||||
}
|
||||
}
|
||||
-1
@@ -1 +0,0 @@
|
||||
class SharePopoverGroupId {}
|
||||
@@ -1,10 +0,0 @@
|
||||
enum ShareRole {
|
||||
/// The owner of the shared page.
|
||||
owner,
|
||||
|
||||
/// A guest on the shared page.
|
||||
guest,
|
||||
|
||||
/// A member of the shared page.
|
||||
member,
|
||||
}
|
||||
Loaded 100 of 823 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user