Compare commits

..
Author SHA1 Message Date
Nathan 58e04291c7 chore: run local ai on windows 2025-03-11 21:30:07 +08:00
Nathan.fooo 57a5b38509 Merge pull request #7488 from AppFlowy-IO/ollama
feat: support Ollama
2025-03-11 13:53:35 +08:00
Nathan 83c53188e3 chore: clippy 2025-03-11 13:22:59 +08:00
Nathan 6ba7f93f69 chore: find plugin load 2025-03-11 13:14:47 +08:00
Nathan 702a486cce chore: find windows exe 2025-03-11 12:55:19 +08:00
Richard Shiue eb0ed1ad86 fix: don't allow selection of text in related questions or loading (#7500) 2025-03-11 11:38:20 +08:00
Lucas e264b3a5b8 Merge pull request #7490 from richardshiue/fix/callout-icon-in-markdown
fix: don't include icon while exporting callout to md
2025-03-11 10:38:46 +08:00
Richard Shiue 667d15c627 fix: ai writer gestures (#7499) 2025-03-11 10:31:08 +08:00
Nathan bd06e1d559 chore: clippy 2025-03-11 09:32:20 +08:00
Lucas 459aca5291 Merge pull request #7496 from LucasXu0/ai_writer_position_improvement
feat: ensure the ai writer block visible when generating result
2025-03-11 09:31:24 +08:00
Nathan e7cd90b6ab chore: update commit 2025-03-11 09:14:11 +08:00
Nathan 940db70447 chore: fix build 2025-03-11 00:27:55 +08:00
Nathan 59139ff323 chore: catch panic 2025-03-10 23:40:28 +08:00
Nathan 22fed1bfbc chore: load from env command 2025-03-10 22:48:09 +08:00
Lucas.Xu 5e593bd36e chore: update appflowy editor version 2025-03-10 21:08:28 +08:00
Lucas.Xu ba1dfc6de4 feat: ensure the ai writer block visible when generating result 2025-03-10 19:18:06 +08:00
Lucas c81f87dcdc feat: make the columns block same width width the editor (#7493)
* feat: make the columns block same width width the editor

* chore: turn off column debug mode

* feat: add block selection container in outline block

* feat: use ratio instead of width in simple columns

* fix: document rules

* fix: turn off debug mode

* fix: update the existing columns block data
2025-03-10 18:13:15 +08:00
Nathan a8b55ca3f0 chore: update prompt 2025-03-10 15:56:09 +08:00
Richard Shiue 0cefaf633c fix: fix test 2025-03-10 14:56:01 +08:00
Richard Shiue ba4aebd005 chore: merge remote-tracking branch 'main' into this one 2025-03-10 13:36:08 +08:00
Lucas 7b32a92290 fix: callout block build error (#7491) 2025-03-10 13:04:29 +08:00
Richard Shiue 41b99209f1 fix: retain if is emoji 2025-03-10 12:10:59 +08:00
Morn c1612fe298 feat: add custom icons for callout (#7449) 2025-03-10 11:46:55 +08:00
Lucas e69a09d332 feat: support nested list in callout block and quote block (#7479)
* feat: support nested list in callout block

* chore: update pubspec.yml

* feat: add new quote block

* feat: support nested list in quote block

* feat: refacotr quote block

* feat: optimize quote block align

* feat: support nested list in quote block

* fix: icon and drag menu overlap

* chore: update appflowy editor version

* feat: support trailing action builder for plugin blocks

* chore: update appflowy editor version
2025-03-10 11:46:17 +08:00
Richard Shiue 4e0d9fdb0b fix: don't include icon while exporting callout to md 2025-03-10 11:08:22 +08:00
Nathan 8b2e769fca chore: ai setting ui 2025-03-10 10:24:55 +08:00
Nathan d29a90a472 chore: init ai plugin on separate thread 2025-03-10 08:54:32 +08:00
Nathan 2e4beb0652 chore: enable windows 2025-03-10 00:35:52 +08:00
Nathan addb041816 chore: update exe path 2025-03-10 00:25:19 +08:00
Nathan 4ff71b5dce chore: implement ollama 2025-03-09 23:32:42 +08:00
Richard Shiue a0ae62d6f5 fix: consider simple table in exclude table types (#7478) 2025-03-07 12:32:27 +08:00
Lucas ea18aa7551 chore: bump collab version 45239d2 (#7477) 2025-03-07 11:17:56 +08:00
Morn 68e7069e92 chore: bump version 0.8.6 & update changelog (#7473)
* chore: bump version 0.8.6

* chore: update changelog
2025-03-06 22:03:30 +08:00
Morn 556d929b67 fix: error caused by ScrollablePositionedList(#7460) (#7469)
* fix: error caused by ScrollablePositionedList

* chore: update appflowy_editor version
2025-03-06 18:45:27 +08:00
Lucas 7f3469a0f2 feat: use undo to revert the autotypograph (#7472) 2025-03-06 18:22:10 +08:00
Lucas a062c4aadb fix: bulleted list icon does not center in the columns (#7471) 2025-03-06 16:59:58 +08:00
Morn 3d3f81ad52 fix: complete the missing icons in the database (#7464)
* fix: complete the missing icons in the database

* fix: the toggle is slower than the actual change taken into effect
2025-03-06 16:04:54 +08:00
Lucas fc0fb0b3d3 fix: update locked page button background color (#7470) 2025-03-06 16:02:56 +08:00
Richard Shiue 884586f0af fix: ai writer issues (#7467)
* fix: disable ai writer in table

* fix: enable header row by default when converting from md

* chore: add title when continue writing

* chore: rewrite using predefined format

* fix: mouse & keyboard event still propagate

* chore: bump editor ref
2025-03-06 15:09:33 +08:00
Morn f8c18afbcf fix: improve the experience of using icon color picker (#7468) 2025-03-06 12:39:18 +08:00
Lucas 8046177d84 fix: simple columns issues (#7466)
* Revert "feat: use flutter_distrubutor to build linux and macos packages (#7392)"

This reverts commit 6dc45c9830.

* fix: linux link issue

* fix: outline doesn't work well in columns

* fix: cannot drag a block under a table that’s in the second column
2025-03-06 12:33:53 +08:00
Morn 8d8fc91391 fix: title position working incorrectly with document width setting (#7465) 2025-03-06 12:33:12 +08:00
Richard Shiue 796fda159e chore: bump client api (#7463) 2025-03-06 09:59:53 +08:00
Nathan.fooo 4b2389dafd chore: bump client api (#7455) 2025-03-05 10:23:28 +08:00
144 changed files with 3408 additions and 3886 deletions

No files matched your search

+109 -121
View File
@@ -128,9 +128,9 @@ jobs:
runs-on: ${{ matrix.job.os }}
needs: create-release
env:
MACOS_APP_RELEASE_PATH: frontend/appflowy_flutter/build/${{ github.ref_name }}
MACOS_APP_RELEASE_PATH: frontend/appflowy_flutter/product/${{ github.ref_name }}/macos/Release
MACOS_X86_ZIP_NAME: AppFlowy-${{ github.ref_name }}-macos-x86_64.zip
MACOS_DMG_NAME: AppFlowy-${{ github.ref_name }}-macos-x86_64.dmg
MACOS_DMG_NAME: AppFlowy-${{ github.ref_name }}-macos-x86_64
strategy:
fail-fast: false
matrix:
@@ -158,22 +158,46 @@ jobs:
- name: Install prerequisites
working-directory: frontend
run: |
brew install p7zip
cargo install --force --locked cargo-make
cargo install --force --locked duckscript_cli
- name: Import codesign certificate
uses: apple-actions/import-codesign-certs@v3
with:
p12-file-base64: ${{ secrets.MACOS_CERTIFICATE }}
p12-password: ${{ secrets.MACOS_CERTIFICATE_PWD }}
- name: Build AppFlowy
working-directory: frontend
run: |
flutter config --enable-macos-desktop
flutter pub global activate flutter_distributor
sh scripts/flutter_release_build/build_macos.sh --build_type dmg --build_arch x86_64 --version ${{ github.ref_name }} --apple-id ${{ secrets.MACOS_NOTARY_USER }} --team-id ${{ secrets.MACOS_TEAM_ID }} --password ${{ secrets.MACOS_NOTARY_PWD }}
dart ./scripts/flutter_release_build/build_flowy.dart run . ${{ github.ref_name }}
- name: Codesign AppFlowy
run: |
echo ${{ secrets.MACOS_CERTIFICATE }} | base64 --decode > certificate.p12
security create-keychain -p action build.keychain
security default-keychain -s build.keychain
security unlock-keychain -p action build.keychain
security import certificate.p12 -k build.keychain -P ${{ secrets.MACOS_CERTIFICATE_PWD }} -T /usr/bin/codesign
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k action build.keychain
/usr/bin/codesign --force --options runtime --deep --sign "${{ secrets.MACOS_CODESIGN_ID }}" "${{ env.MACOS_APP_RELEASE_PATH }}/AppFlowy.app" -v
- name: Create macOS dmg
run: |
brew install create-dmg
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
- name: Archive Asset
working-directory: ${{ env.MACOS_APP_RELEASE_PATH }}
run: zip --symlinks -qr ${{ env.MACOS_X86_ZIP_NAME }} AppFlowy.app
- name: Upload Asset
uses: actions/upload-release-asset@v1
@@ -191,82 +215,8 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ needs.create-release.outputs.upload_url }}
asset_path: ${{ env.MACOS_APP_RELEASE_PATH }}/${{ env.MACOS_DMG_NAME }}
asset_name: ${{ env.MACOS_DMG_NAME }}
asset_content_type: application/octet-stream
build-for-macOS-arm64:
name: ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-args }}]
runs-on: ${{ matrix.job.os }}
needs: create-release
env:
MACOS_APP_RELEASE_PATH: frontend/appflowy_flutter/build/${{ github.ref_name }}
MACOS_AARCH64_ZIP_NAME: AppFlowy-${{ github.ref_name }}-macos-arm64.zip
MACOS_DMG_NAME: AppFlowy-${{ github.ref_name }}-macos-arm64.dmg
strategy:
fail-fast: false
matrix:
job:
- {
targets: "aarch64-apple-darwin",
os: macos-latest,
extra-build-args: "",
}
steps:
- name: Checkout source code
uses: actions/checkout@v4
- name: Install flutter
uses: subosito/flutter-action@v2
with:
channel: "stable"
flutter-version: ${{ env.FLUTTER_VERSION }}
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ env.RUST_TOOLCHAIN }}
targets: ${{ matrix.job.targets }}
components: rustfmt
- name: Install prerequisites
working-directory: frontend
run: |
brew install p7zip
cargo install --force --locked cargo-make
cargo install --force --locked duckscript_cli
- name: Import codesign certificate
uses: apple-actions/import-codesign-certs@v3
with:
p12-file-base64: ${{ secrets.MACOS_CERTIFICATE }}
p12-password: ${{ secrets.MACOS_CERTIFICATE_PWD }}
- name: Build AppFlowy
working-directory: frontend
run: |
flutter config --enable-macos-desktop
dart pub global activate flutter_distributor
sh scripts/flutter_release_build/build_macos.sh --build_type dmg --build_arch arm64 --version ${{ github.ref_name }} --apple-id ${{ secrets.MACOS_NOTARY_USER }} --team-id ${{ secrets.MACOS_TEAM_ID }} --password ${{ secrets.MACOS_NOTARY_PWD }}
- name: Upload Asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ needs.create-release.outputs.upload_url }}
asset_path: ${{ env.MACOS_APP_RELEASE_PATH }}/${{ env.MACOS_AARCH64_ZIP_NAME }}
asset_name: ${{ env.MACOS_AARCH64_ZIP_NAME }}
asset_content_type: application/octet-stream
- name: Upload DMG Asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ needs.create-release.outputs.upload_url }}
asset_path: ${{ env.MACOS_APP_RELEASE_PATH }}/${{ env.MACOS_DMG_NAME }}
asset_name: ${{ env.MACOS_DMG_NAME }}
asset_path: ${{ env.MACOS_APP_RELEASE_PATH }}/${{ env.MACOS_DMG_NAME }}.dmg
asset_name: ${{ env.MACOS_DMG_NAME }}.dmg
asset_content_type: application/octet-stream
build-for-macOS-universal:
@@ -274,9 +224,9 @@ jobs:
runs-on: ${{ matrix.job.os }}
needs: create-release
env:
MACOS_APP_RELEASE_PATH: frontend/appflowy_flutter/build/${{ github.ref_name }}
MACOS_APP_RELEASE_PATH: frontend/appflowy_flutter/product/${{ github.ref_name }}/macos/Release
MACOS_AARCH64_ZIP_NAME: AppFlowy-${{ github.ref_name }}-macos-universal.zip
MACOS_DMG_NAME: AppFlowy-${{ github.ref_name }}-macos-universal.dmg
MACOS_DMG_NAME: AppFlowy-${{ github.ref_name }}-macos-universal
strategy:
fail-fast: false
matrix:
@@ -306,22 +256,46 @@ jobs:
- name: Install prerequisites
working-directory: frontend
run: |
brew install p7zip
cargo install --force --locked cargo-make
cargo install --force --locked duckscript_cli
- name: Import codesign certificate
uses: apple-actions/import-codesign-certs@v3
with:
p12-file-base64: ${{ secrets.MACOS_CERTIFICATE }}
p12-password: ${{ secrets.MACOS_CERTIFICATE_PWD }}
- name: Build AppFlowy
working-directory: frontend
run: |
flutter config --enable-macos-desktop
dart pub global activate flutter_distributor
sh scripts/flutter_release_build/build_macos.sh --build_type dmg --build_arch universal --version ${{ github.ref_name }} --apple-id ${{ secrets.MACOS_NOTARY_USER }} --team-id ${{ secrets.MACOS_TEAM_ID }} --password ${{ secrets.MACOS_NOTARY_PWD }}
sh scripts/flutter_release_build/build_universal_package_for_macos.sh ${{ github.ref_name }}
- name: Codesign AppFlowy
run: |
echo ${{ secrets.MACOS_CERTIFICATE }} | base64 --decode > certificate.p12
security create-keychain -p action build.keychain
security default-keychain -s build.keychain
security unlock-keychain -p action build.keychain
security import certificate.p12 -k build.keychain -P ${{ secrets.MACOS_CERTIFICATE_PWD }} -T /usr/bin/codesign
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k action build.keychain
/usr/bin/codesign --force --options runtime --deep --sign "${{ secrets.MACOS_CODESIGN_ID }}" "${{ env.MACOS_APP_RELEASE_PATH }}/AppFlowy.app" -v
- name: Create macOS dmg
run: |
brew install create-dmg
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
- name: Archive Asset
working-directory: ${{ env.MACOS_APP_RELEASE_PATH }}
run: zip --symlinks -qr ${{ env.MACOS_AARCH64_ZIP_NAME }} AppFlowy.app
- name: Upload Asset
uses: actions/upload-release-asset@v1
@@ -339,8 +313,8 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ needs.create-release.outputs.upload_url }}
asset_path: ${{ env.MACOS_APP_RELEASE_PATH }}/${{ env.MACOS_DMG_NAME }}
asset_name: ${{ env.MACOS_DMG_NAME }}
asset_path: ${{ env.MACOS_APP_RELEASE_PATH }}/${{ env.MACOS_DMG_NAME }}.dmg
asset_name: ${{ env.MACOS_DMG_NAME }}.dmg
asset_content_type: application/octet-stream
build-for-linux:
@@ -348,12 +322,14 @@ jobs:
runs-on: ${{ matrix.job.os }}
needs: create-release
env:
LINUX_APP_RELEASE_PATH: frontend/appflowy_flutter/build/${{ github.ref_name }}
LINUX_APP_RELEASE_PATH: frontend/appflowy_flutter/product/${{ github.ref_name }}/linux/Release
LINUX_ZIP_NAME: AppFlowy-${{ matrix.job.target }}-x86_64.tar.gz
LINUX_PACKAGE_DEB_NAME: AppFlowy-${{ github.ref_name }}-linux-x86_64.deb
LINUX_PACKAGE_RPM_NAME: AppFlowy-${{ github.ref_name }}-linux-x86_64.rpm
LINUX_PACKAGE_TMP_RPM_NAME: AppFlowy-${{ github.ref_name }}-2.x86_64.rpm
LINUX_PACKAGE_TMP_APPIMAGE_NAME: AppFlowy-${{ github.ref_name }}-x86_64.AppImage
LINUX_PACKAGE_APPIMAGE_NAME: AppFlowy-${{ github.ref_name }}-linux-x86_64.AppImage
LINUX_PACKAGE_ZIP_NAME: AppFlowy-${{ github.ref_name }}-linux-x86_64.zip
LINUX_PACKAGE_TAR_NAME: AppFlowy-${{ github.ref_name }}-linux-x86_64.tar.gz
LINUX_PACKAGE_ZIP_NAME: AppFlowy-${{ github.ref_name }}-linux-x86_64.tar.gz
strategy:
fail-fast: false
@@ -362,7 +338,7 @@ jobs:
- {
arch: x86_64,
target: x86_64-unknown-linux-gnu,
os: ubuntu-22.04,
os: ubuntu-20.04,
extra-build-args: "",
flutter_profile: production-linux-x86_64,
}
@@ -388,24 +364,14 @@ jobs:
- name: Install prerequisites
working-directory: frontend
run: |
# Install dependencies
sudo wget -qO /etc/apt/trusted.gpg.d/dart_linux_signing_key.asc https://dl-ssl.google.com/linux/linux_signing_key.pub
sudo apt-get update
sudo apt-get install -y build-essential libsqlite3-dev libssl-dev clang cmake ninja-build pkg-config libgtk-3-dev keybinder-3.0 libnotify-dev
sudo apt install rpm patchelf locate
sudo add-apt-repository universe
sudo apt install libfuse2
# Install appimagetool
wget -O appimagetool "https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage"
chmod +x appimagetool
mv appimagetool /usr/local/bin/
# Install cargo-make and duckscript
sudo apt-get install -y build-essential libsqlite3-dev libssl-dev clang cmake ninja-build pkg-config libgtk-3-dev
sudo apt-get install keybinder-3.0
sudo apt-get install -y alien libnotify-dev
source $HOME/.cargo/env
cargo install --force --locked cargo-make
cargo install --force --locked duckscript_cli
rustup target add ${{ matrix.job.target }}
- name: Install gcc-aarch64-linux-gnu
@@ -418,8 +384,30 @@ jobs:
working-directory: frontend
run: |
flutter config --enable-linux-desktop
dart pub global activate flutter_distributor
./scripts/flutter_release_build/build_linux.sh --build_type all --build_arch x86_64 --version ${{ github.ref_name }}
dart ./scripts/flutter_release_build/build_flowy.dart run . ${{ github.ref_name }}
- name: Archive Asset
working-directory: ${{ env.LINUX_APP_RELEASE_PATH }}
run: tar -czf ${{ env.LINUX_ZIP_NAME }} *
- name: Build Linux package (.deb)
working-directory: frontend
run: |
sh scripts/linux_distribution/deb/build_deb.sh appflowy_flutter/product/${{ github.ref_name }}/linux/Release ${{ github.ref_name }} ${{ env.LINUX_PACKAGE_DEB_NAME }}
- name: Build Linux package (.rpm)
working-directory: ${{ env.LINUX_APP_RELEASE_PATH }}
run: |
sudo alien -r ${{ env.LINUX_PACKAGE_DEB_NAME }}
cp -r ${{ env.LINUX_PACKAGE_TMP_RPM_NAME }} ${{ env.LINUX_PACKAGE_RPM_NAME }}
- name: Build Linux package (.AppImage)
working-directory: frontend
continue-on-error: true
run: |
sh scripts/linux_distribution/appimage/build_appimage.sh ${{ github.ref_name }}
cd ..
cp -r frontend/${{ env.LINUX_PACKAGE_TMP_APPIMAGE_NAME }} ${{ env.LINUX_APP_RELEASE_PATH }}/${{ env.LINUX_PACKAGE_APPIMAGE_NAME }}
- name: Upload Asset
id: upload-release-asset
@@ -428,11 +416,11 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ needs.create-release.outputs.upload_url }}
asset_path: ${{ env.LINUX_APP_RELEASE_PATH }}/${{ env.LINUX_PACKAGE_ZIP_NAME }}
asset_path: ${{ env.LINUX_APP_RELEASE_PATH }}/${{ env.LINUX_ZIP_NAME }}
asset_name: ${{ env.LINUX_PACKAGE_ZIP_NAME }}
asset_content_type: application/octet-stream
- name: Upload DEB package
- name: Upload Debian package
id: upload-release-asset-install-package-deb
uses: actions/upload-release-asset@v1
env:
+13
View File
@@ -1,4 +1,17 @@
# Release Notes
## Version 0.8.6 - 06/03/2025
### Bug Fixes
- Fix the incorrect title positioning when adjusting the document width setting
- Enhance the user experience of the icon color picker for smoother interactions
- Add missing icons to the database to ensure completeness and consistency
- Resolve the issue with links not functioning correctly on Linux systems
- Improve the outline feature to work seamlessly within columns
- Center the bulleted list icon within columns for better visual alignment
- Enable dragging blocks under tables in the second column to enhance flexibility
- Disable the AI writer feature within tables to prevent conflicts and improve usability
- Automatically enable the header row when converting content from Markdown to ensure proper formatting
- Use the "Undo" function to revert the auto-formatting
## Version 0.8.5 - 04/03/2025
### New Features
- Columns in Documents: Arrange content side by side using drag-and-drop or the slash menu
+1 -1
View File
@@ -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.8.5"
APPFLOWY_VERSION = "0.8.6"
FLUTTER_DESKTOP_FEATURES = "dart"
PRODUCT_NAME = "AppFlowy"
MACOSX_DEPLOYMENT_TARGET = "11.0"
@@ -1,35 +1,12 @@
output: build/
output: dist/
releases:
- name: prod
- name: dev
jobs:
- name: release-prod-linux-deb
- name: release-dev-linux-deb
package:
platform: linux
target: deb
- name: release-prod-linux-rpm
- name: release-dev-linux-rpm
package:
platform: linux
target: rpm
- name: release-prod-linux-appimage
package:
platform: linux
target: appimage
- name: release-prod-linux-zip
package:
platform: linux
target: zip
# Because the flutter_distribute plugin does not support the deep code-signing for macos, we need to manually sign the app.
# So we don't use the flutter_distribute plugin to distribute the macos app.
# - name: release-prod-macos-dmg
# package:
# platform: macos
# target: dmg
# - name: release-prod-macos-pkg
# package:
# platform: macos
# target: pkg
- name: release-prod-macos-zip
package:
platform: macos
target: zip
@@ -36,36 +36,29 @@ class AIPromptInputBloc extends Bloc<AIPromptInputEvent, AIPromptInputState> {
on<AIPromptInputEvent>(
(event, emit) {
event.when(
updateChatState: (LocalAIChatPB chatState) {
updateAIState: (LocalAIPB localAIState) {
if (localAIState.hasLackOfResource()) {
emit(
state.copyWith(
aiType: AIType.appflowyAI,
supportChatWithFile: false,
localAIState: localAIState,
),
);
return;
}
// Only user enable chat with file and the plugin is already running
final supportChatWithFile = chatState.fileEnabled &&
chatState.pluginState.state == RunningStatePB.Running;
final supportChatWithFile = localAIState.enabled &&
localAIState.state == RunningStatePB.Running;
final aiType = chatState.pluginState.state == RunningStatePB.Running
? AIType.localAI
: AIType.appflowyAI;
final aiType =
localAIState.enabled ? AIType.localAI : AIType.appflowyAI;
emit(
state.copyWith(
aiType: aiType,
supportChatWithFile: supportChatWithFile,
chatState: chatState,
),
);
},
updatePluginState: (LocalAIPluginStatePB chatState) {
final fileEnabled = state.chatState?.fileEnabled ?? false;
final supportChatWithFile =
fileEnabled && chatState.state == RunningStatePB.Running;
final aiType = chatState.state == RunningStatePB.Running
? AIType.localAI
: AIType.appflowyAI;
emit(
state.copyWith(
supportChatWithFile: supportChatWithFile,
aiType: aiType,
localAIState: localAIState,
),
);
},
@@ -130,22 +123,17 @@ class AIPromptInputBloc extends Bloc<AIPromptInputEvent, AIPromptInputState> {
_listener.start(
stateCallback: (pluginState) {
if (!isClosed) {
add(AIPromptInputEvent.updatePluginState(pluginState));
}
},
chatStateCallback: (chatState) {
if (!isClosed) {
add(AIPromptInputEvent.updateChatState(chatState));
add(AIPromptInputEvent.updateAIState(pluginState));
}
},
);
}
void _init() {
AIEventGetLocalAIChatState().send().fold(
(chatState) {
AIEventGetLocalAIState().send().fold(
(localAIState) {
if (!isClosed) {
add(AIPromptInputEvent.updateChatState(chatState));
add(AIPromptInputEvent.updateAIState(localAIState));
}
},
Log.error,
@@ -168,12 +156,8 @@ class AIPromptInputBloc extends Bloc<AIPromptInputEvent, AIPromptInputState> {
@freezed
class AIPromptInputEvent with _$AIPromptInputEvent {
const factory AIPromptInputEvent.updateChatState(
LocalAIChatPB chatState,
) = _UpdateChatState;
const factory AIPromptInputEvent.updatePluginState(
LocalAIPluginStatePB chatState,
) = _UpdatePluginState;
const factory AIPromptInputEvent.updateAIState(LocalAIPB localAIState) =
_UpdateAIState;
const factory AIPromptInputEvent.toggleShowPredefinedFormat() =
_ToggleShowPredefinedFormat;
const factory AIPromptInputEvent.updatePredefinedFormat(
@@ -196,7 +180,7 @@ class AIPromptInputState with _$AIPromptInputState {
required bool supportChatWithFile,
required bool showPredefinedFormats,
required PredefinedFormat? predefinedFormat,
required LocalAIChatPB? chatState,
required LocalAIPB? localAIState,
required List<ChatFile> attachedFiles,
required List<ViewPB> mentionedPages,
}) = _AIPromptInputState;
@@ -207,7 +191,7 @@ class AIPromptInputState with _$AIPromptInputState {
supportChatWithFile: false,
showPredefinedFormats: format != null,
predefinedFormat: format,
chatState: null,
localAIState: null,
attachedFiles: [],
mentionedPages: [],
);
@@ -16,48 +16,50 @@ class AILoadingIndicator extends StatelessWidget {
@override
Widget build(BuildContext context) {
final slice = Duration(milliseconds: duration.inMilliseconds ~/ 5);
return SizedBox(
height: 20,
child: SeparatedRow(
separatorBuilder: () => const HSpace(4),
children: [
Padding(
padding: const EdgeInsetsDirectional.only(end: 4.0),
child: FlowyText(
text,
color: Theme.of(context).hintColor,
return SelectionContainer.disabled(
child: SizedBox(
height: 20,
child: SeparatedRow(
separatorBuilder: () => const HSpace(4),
children: [
Padding(
padding: const EdgeInsetsDirectional.only(end: 4.0),
child: FlowyText(
text,
color: Theme.of(context).hintColor,
),
),
),
buildDot(const Color(0xFF9327FF))
.animate(onPlay: (controller) => controller.repeat())
.slideY(duration: slice, begin: 0, end: -1)
.then()
.slideY(begin: -1, end: 1)
.then()
.slideY(begin: 1, end: 0)
.then()
.slideY(duration: slice * 2, begin: 0, end: 0),
buildDot(const Color(0xFFFB006D))
.animate(onPlay: (controller) => controller.repeat())
.slideY(duration: slice, begin: 0, end: 0)
.then()
.slideY(begin: 0, end: -1)
.then()
.slideY(begin: -1, end: 1)
.then()
.slideY(begin: 1, end: 0)
.then()
.slideY(begin: 0, end: 0),
buildDot(const Color(0xFFFFCE00))
.animate(onPlay: (controller) => controller.repeat())
.slideY(duration: slice * 2, begin: 0, end: 0)
.then()
.slideY(duration: slice, begin: 0, end: -1)
.then()
.slideY(begin: -1, end: 1)
.then()
.slideY(begin: 1, end: 0),
],
buildDot(const Color(0xFF9327FF))
.animate(onPlay: (controller) => controller.repeat())
.slideY(duration: slice, begin: 0, end: -1)
.then()
.slideY(begin: -1, end: 1)
.then()
.slideY(begin: 1, end: 0)
.then()
.slideY(duration: slice * 2, begin: 0, end: 0),
buildDot(const Color(0xFFFB006D))
.animate(onPlay: (controller) => controller.repeat())
.slideY(duration: slice, begin: 0, end: 0)
.then()
.slideY(begin: 0, end: -1)
.then()
.slideY(begin: -1, end: 1)
.then()
.slideY(begin: 1, end: 0)
.then()
.slideY(begin: 0, end: 0),
buildDot(const Color(0xFFFFCE00))
.animate(onPlay: (controller) => controller.repeat())
.slideY(duration: slice * 2, begin: 0, end: 0)
.then()
.slideY(duration: slice, begin: 0, end: -1)
.then()
.slideY(begin: -1, end: 1)
.then()
.slideY(begin: 1, end: 0),
],
),
),
);
}
@@ -572,7 +572,7 @@ class _PromptBottomActions extends StatelessWidget {
margin: DesktopAIChatSizes.inputActionBarMargin,
child: BlocBuilder<AIPromptInputBloc, AIPromptInputState>(
builder: (context, state) {
if (state.chatState == null) {
if (state.localAIState == null) {
return Align(
alignment: AlignmentDirectional.centerEnd,
child: _sendButton(),
@@ -10,6 +10,7 @@ import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:open_filex/open_filex.dart';
import 'package:string_validator/string_validator.dart';
import 'package:universal_platform/universal_platform.dart';
import 'package:url_launcher/url_launcher.dart' as launcher;
typedef OnFailureCallback = void Function(Uri uri);
@@ -38,6 +39,11 @@ Future<bool> afLaunchUri(
);
}
// on Linux, add http scheme to the url if it is not present
if (UniversalPlatform.isLinux && !isURL(url, {'require_protocol': true})) {
uri = Uri.parse('https://$url');
}
// try to launch the uri directly
bool result;
try {
@@ -21,33 +21,35 @@ class RelatedQuestionList extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ListView.separated(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: relatedQuestions.length + 1,
padding:
const EdgeInsets.only(bottom: 8.0) + AIChatUILayout.messageMargin,
separatorBuilder: (context, index) => const VSpace(4.0),
itemBuilder: (context, index) {
if (index == 0) {
return Padding(
padding: const EdgeInsets.only(bottom: 4.0),
child: FlowyText(
LocaleKeys.chat_relatedQuestion.tr(),
color: Theme.of(context).hintColor,
fontWeight: FontWeight.w600,
),
);
} else {
return Align(
alignment: AlignmentDirectional.centerStart,
child: RelatedQuestionItem(
question: relatedQuestions[index - 1],
onQuestionSelected: onQuestionSelected,
),
);
}
},
return SelectionContainer.disabled(
child: ListView.separated(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: relatedQuestions.length + 1,
padding:
const EdgeInsets.only(bottom: 8.0) + AIChatUILayout.messageMargin,
separatorBuilder: (context, index) => const VSpace(4.0),
itemBuilder: (context, index) {
if (index == 0) {
return Padding(
padding: const EdgeInsets.only(bottom: 4.0),
child: FlowyText(
LocaleKeys.chat_relatedQuestion.tr(),
color: Theme.of(context).hintColor,
fontWeight: FontWeight.w600,
),
);
} else {
return Align(
alignment: AlignmentDirectional.centerStart,
child: RelatedQuestionItem(
question: relatedQuestions[index - 1],
onQuestionSelected: onQuestionSelected,
),
);
}
},
),
);
}
}
@@ -2,10 +2,13 @@ import 'package:appflowy/plugins/database/application/database_controller.dart';
import 'package:appflowy/plugins/database/grid/application/filter/filter_editor_bloc.dart';
import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/toolbar/filter_button.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/toolbar/view_database_button.dart';
import 'package:appflowy/plugins/database/widgets/setting/setting_button.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:provider/provider.dart';
class BoardSettingBar extends StatelessWidget {
const BoardSettingBar({
@@ -30,6 +33,8 @@ class BoardSettingBar extends StatelessWidget {
if (value) {
return const SizedBox.shrink();
}
final isReference =
Provider.of<ReferenceState?>(context)?.isReference ?? false;
return SizedBox(
height: 20,
child: Row(
@@ -38,6 +43,10 @@ class BoardSettingBar extends StatelessWidget {
FilterButton(
toggleExtension: toggleExtension,
),
if (isReference) ...[
const HSpace(2),
ViewDatabaseButton(view: databaseController.view),
],
const HSpace(2),
SettingButton(
databaseController: databaseController,
@@ -2,10 +2,13 @@ import 'package:appflowy/plugins/database/application/database_controller.dart';
import 'package:appflowy/plugins/database/grid/application/filter/filter_editor_bloc.dart';
import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/toolbar/filter_button.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/toolbar/view_database_button.dart';
import 'package:appflowy/plugins/database/widgets/setting/setting_button.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:provider/provider.dart';
class CalendarSettingBar extends StatelessWidget {
const CalendarSettingBar({
@@ -30,6 +33,8 @@ class CalendarSettingBar extends StatelessWidget {
if (value) {
return const SizedBox.shrink();
}
final isReference =
Provider.of<ReferenceState?>(context)?.isReference ?? false;
return SizedBox(
height: 20,
child: Row(
@@ -38,6 +43,10 @@ class CalendarSettingBar extends StatelessWidget {
FilterButton(
toggleExtension: toggleExtension,
),
if (isReference) ...[
const HSpace(2),
ViewDatabaseButton(view: databaseController.view),
],
const HSpace(2),
SettingButton(
databaseController: databaseController,
@@ -54,12 +54,14 @@ class GridSettingBar extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.end,
children: [
FilterButton(toggleExtension: toggleExtension),
const HSpace(6),
const HSpace(2),
SortButton(toggleExtension: toggleExtension),
const HSpace(6),
if (isReference) ...[
const HSpace(2),
ViewDatabaseButton(view: controller.view),
],
const HSpace(2),
SettingButton(databaseController: controller),
if (isReference) const HSpace(6),
if (isReference) ViewDatabaseButton(view: controller.view),
],
),
);
@@ -81,6 +81,7 @@ class DatabaseLayoutSelector extends StatelessWidget {
builder: (context, compactMode, child) {
return Toggle(
value: compactMode,
duration: Duration.zero,
onChanged: (value) =>
databaseController.setCompactMode(value),
padding: EdgeInsets.zero,
@@ -13,7 +13,6 @@ import 'package:appflowy_editor/appflowy_editor.dart'
NodeIterator,
NodeExternalValues,
HeadingBlockKeys,
QuoteBlockKeys,
NumberedListBlockKeys,
BulletedListBlockKeys,
blockComponentDelta;
@@ -104,6 +104,28 @@ class DocumentRules {
} else {
// otherwise, delete the column
deleteColumnsTransaction.deleteNode(column);
final deletedColumnRatio =
column.attributes[SimpleColumnBlockKeys.ratio];
if (deletedColumnRatio != null) {
// update the ratio of the columns
final columnsNode = column.columnsParent;
if (columnsNode != null) {
final length = columnsNode.children.length;
for (final columnNode in columnsNode.children) {
final ratio =
columnNode.attributes[SimpleColumnBlockKeys.ratio] ??
1.0 / length;
if (ratio != null) {
deleteColumnsTransaction.updateNode(columnNode, {
...columnNode.attributes,
SimpleColumnBlockKeys.ratio:
ratio + deletedColumnRatio / (length - 1),
});
}
}
}
}
}
}
}
@@ -6,7 +6,8 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/mo
import 'package:appflowy/plugins/document/presentation/editor_plugins/code_block/code_block_copy_button.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy/plugins/document/presentation/editor_style.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor/appflowy_editor.dart'
hide QuoteBlockComponentBuilder, quoteNode, QuoteBlockKeys;
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
import 'package:easy_localization/easy_localization.dart' hide TextDirection;
import 'package:flowy_infra/theme_extension.dart';
@@ -121,10 +122,18 @@ BlockComponentConfiguration _buildDefaultConfiguration(BuildContext context) {
},
indentPadding: (node, textDirection) {
double padding = 26.0;
// only add indent padding for the top level node to align the children
if (UniversalPlatform.isMobile && node.path.length == 1) {
padding += EditorStyleCustomizer.nodeHorizontalPadding;
}
// in the quote block, we reduce the indent padding for the first level block.
// So we have to add more padding for the second level to avoid the drag menu overlay the quote icon.
if (node.isInQuote && node.level == 2) {
padding += 24;
}
return textDirection == TextDirection.ltr
? EdgeInsets.only(left: padding)
: EdgeInsets.only(right: padding);
@@ -203,6 +212,16 @@ void _customBlockOptionActions(
),
);
builder.actionTrailingBuilder = (context, state) {
if (context.node.parent?.type == QuoteBlockKeys.type) {
return const SizedBox(
width: 24,
height: 24,
);
}
return const SizedBox.shrink();
};
builder.actionBuilder = (context, state) {
double top = builder.configuration.padding(context.node).top;
final type = context.node.type;
@@ -592,6 +611,7 @@ QuoteBlockComponentBuilder _buildQuoteBlockComponentBuilder(
node: node,
configuration: configuration,
),
indentPadding: (node, _) => EdgeInsets.zero,
),
);
}
@@ -798,8 +818,14 @@ CalloutBlockComponentBuilder _buildCalloutBlockComponentBuilder(
configuration: configuration,
textSpan: textSpan,
),
indentPadding: (node, _) => EdgeInsets.only(left: 42),
),
inlinePadding: const EdgeInsets.symmetric(vertical: 8.0),
inlinePadding: (node) {
if (node.children.isEmpty) {
return const EdgeInsets.symmetric(vertical: 8.0);
}
return EdgeInsets.only(top: 8.0, bottom: 2.0);
},
defaultColor: calloutBGColor,
);
}
@@ -1065,3 +1091,16 @@ TextAlign _buildTextAlignInTableCell(
return node.tableAlign.textAlign;
}
extension on Node {
bool get isInQuote {
Node? parent = this.parent;
while (parent != null) {
if (parent.type == QuoteBlockKeys.type) {
return true;
}
parent = parent.parent;
}
return false;
}
}
@@ -81,8 +81,8 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage>
];
final List<ToolbarItem> toolbarItems = [
improveWritingItem..isActive = onlyShowInTextType,
aiWriterItem..isActive = onlyShowInTextType,
improveWritingItem..isActive = onlyShowInTextTypeAndExcludeTable,
aiWriterItem..isActive = onlyShowInTextTypeAndExcludeTable,
paragraphItem..isActive = onlyShowInSingleTextTypeSelectionAndExcludeTable,
headingsToolbarItem
..isActive = onlyShowInSingleTextTypeSelectionAndExcludeTable,
@@ -154,7 +154,10 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage>
InlineMathEquationKeys.formula,
]);
indentableBlockTypes.add(ToggleListBlockKeys.type);
indentableBlockTypes.addAll([
ToggleListBlockKeys.type,
CalloutBlockKeys.type,
]);
convertibleBlockTypes.addAll([
ToggleListBlockKeys.type,
CalloutBlockKeys.type,
@@ -363,7 +366,9 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage>
contextMenuItems: customContextMenuItems,
// customize the header and footer.
header: widget.header,
autoScrollEdgeOffset: UniversalPlatform.isDesktopOrWeb
? 250
: appFlowyEditorAutoScrollEdgeOffset,
footer: GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () async {
@@ -372,7 +377,7 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage>
},
child: SizedBox(
width: double.infinity,
height: UniversalPlatform.isDesktopOrWeb ? 200 : 400,
height: UniversalPlatform.isDesktopOrWeb ? 300 : 400,
),
),
dropTargetStyle: AppFlowyDropTargetStyle(
@@ -10,7 +10,8 @@ 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/view.pb.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor/appflowy_editor.dart'
hide QuoteBlockKeys, quoteNode;
import 'package:flutter_bloc/flutter_bloc.dart';
class BlockActionOptionState {}
@@ -89,7 +89,7 @@ class _DraggableOptionButtonState extends State<DraggableOptionButton> {
interceptor: (context, targetNode) {
// if the cursor node is in a columns block or a column block,
// we will return the node's parent instead to support dragging a node to the inside of a columns block or a column block.
final parentColumnNode = targetNode.parentColumn;
final parentColumnNode = targetNode.columnParent;
if (parentColumnNode != null) {
final position = getDragAreaPosition(
context,
@@ -108,6 +108,12 @@ class _DraggableOptionButtonState extends State<DraggableOptionButton> {
}
}
// return simple table block if the target node is in a simple table block
final parentSimpleTableNode = targetNode.parentTableNode;
if (parentSimpleTableNode != null) {
return parentSimpleTableNode;
}
return targetNode;
},
builder: (context, data) {
@@ -141,7 +147,7 @@ class _DraggableOptionButtonState extends State<DraggableOptionButton> {
interceptor: (context, targetNode) {
// if the cursor node is in a columns block or a column block,
// we will return the node's parent instead to support dragging a node to the inside of a columns block or a column block.
final parentColumnNode = targetNode.parentColumn;
final parentColumnNode = targetNode.columnParent;
if (parentColumnNode != null) {
final position = getDragAreaPosition(
context,
@@ -160,9 +166,16 @@ class _DraggableOptionButtonState extends State<DraggableOptionButton> {
}
}
// return simple table block if the target node is in a simple table block
final parentSimpleTableNode = targetNode.parentTableNode;
if (parentSimpleTableNode != null) {
return parentSimpleTableNode;
}
return targetNode;
},
);
dragToMoveNode(
context,
node: widget.blockComponentContext.node,
@@ -1,7 +1,7 @@
import 'package:appflowy/plugins/document/presentation/editor_plugins/columns/simple_columns_block_constant.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor/appflowy_editor.dart'
hide QuoteBlockKeys, quoteNode;
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
@@ -36,6 +36,15 @@ Future<void> dragToMoveNode(
return;
}
if (shouldIgnoreDragTarget(
editorState: editorState,
dragNode: node,
targetPath: acceptedPath,
)) {
Log.info('Drop ignored: node($node, ${node.path}), path($acceptedPath)');
return;
}
final position = getDragAreaPosition(context, targetNode, dragOffset);
if (position == null) {
Log.info('position is null');
@@ -50,38 +59,37 @@ Future<void> dragToMoveNode(
// 1. if the targetNode is a column block, it means we should create a column block to contain the node and insert the column node to the target node's parent
// 2. if the targetNode is not a column block, it means we should create a columns block to contain the target node and the drag node
final transaction = editorState.transaction;
final targetNodeParent = targetNode.parentColumnsBlock;
final targetNodeParent = targetNode.columnsParent;
if (targetNodeParent != null) {
final length = targetNodeParent.children.length;
final ratios = targetNodeParent.children
.map(
(e) =>
e.attributes[SimpleColumnBlockKeys.ratio]?.toDouble() ??
1.0 / length,
)
.map((e) => e * length / (length + 1))
.toList();
final columnNode = simpleColumnNode(
children: [node.deepCopy()],
width: (node.rect.width * 1 / (length + 1)).clamp(
SimpleColumnsBlockConstants.minimumColumnWidth,
double.infinity,
),
ratio: 1.0 / (length + 1),
);
for (final column in targetNodeParent.children) {
final width =
column.attributes[SimpleColumnBlockKeys.width]?.toDouble() ??
SimpleColumnsBlockConstants.minimumColumnWidth;
for (final (index, column) in targetNodeParent.children.indexed) {
transaction.updateNode(column, {
...column.attributes,
SimpleColumnBlockKeys.width: (width * length / (length + 1)).clamp(
SimpleColumnsBlockConstants.minimumColumnWidth,
double.infinity,
),
SimpleColumnBlockKeys.ratio: ratios[index],
});
}
transaction.insertNode(targetNode.path.next, columnNode);
transaction.deleteNode(node);
} else {
final width = targetNode.rect.width / 2 - 16;
final columnsNode = simpleColumnsNode(
children: [
simpleColumnNode(children: [targetNode.deepCopy()], width: width),
simpleColumnNode(children: [node.deepCopy()], width: width),
simpleColumnNode(children: [targetNode.deepCopy()], ratio: 0.5),
simpleColumnNode(children: [node.deepCopy()], ratio: 0.5),
],
);
@@ -99,39 +107,37 @@ Future<void> dragToMoveNode(
// 1. if the target node is a column block, we should create a column block to contain the node and insert the column node to the target node's parent
// 2. if the target node is not a column block, we should create a columns block to contain the target node and the drag node
final transaction = editorState.transaction;
final targetNodeParent = targetNode.parentColumnsBlock;
final targetNodeParent = targetNode.columnsParent;
if (targetNodeParent != null) {
// find the previous sibling node of the target node
final length = targetNodeParent.children.length;
final ratios = targetNodeParent.children
.map(
(e) =>
e.attributes[SimpleColumnBlockKeys.ratio]?.toDouble() ??
1.0 / length,
)
.map((e) => e * length / (length + 1))
.toList();
final columnNode = simpleColumnNode(
children: [node.deepCopy()],
width: (node.rect.width * 1 / (length + 1)).clamp(
SimpleColumnsBlockConstants.minimumColumnWidth,
double.infinity,
),
ratio: 1.0 / (length + 1),
);
for (final column in targetNodeParent.children) {
final width =
column.attributes[SimpleColumnBlockKeys.width]?.toDouble() ??
SimpleColumnsBlockConstants.minimumColumnWidth;
for (final (index, column) in targetNodeParent.children.indexed) {
transaction.updateNode(column, {
...column.attributes,
SimpleColumnBlockKeys.width: (width * length / (length + 1)).clamp(
SimpleColumnsBlockConstants.minimumColumnWidth,
double.infinity,
),
SimpleColumnBlockKeys.ratio: ratios[index],
});
}
transaction.insertNode(targetNode.path.previous, columnNode);
transaction.deleteNode(node);
} else {
final width = targetNode.rect.width / 2 - 16;
final columnsNode = simpleColumnsNode(
children: [
simpleColumnNode(children: [node.deepCopy()], width: width),
simpleColumnNode(children: [targetNode.deepCopy()], width: width),
simpleColumnNode(children: [node.deepCopy()], ratio: 0.5),
simpleColumnNode(children: [targetNode.deepCopy()], ratio: 0.5),
],
);
@@ -182,6 +188,7 @@ Future<void> dragToMoveNode(
Node dragTargetNode,
Offset dragOffset,
) {
debugPrint('getDragAreaPosition - dragTargetNode: ${dragTargetNode.type}');
final selectable = dragTargetNode.selectable;
final renderBox = selectable?.context.findRenderObject() as RenderBox?;
if (selectable == null || renderBox == null) {
@@ -1,7 +1,8 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor/appflowy_editor.dart'
hide QuoteBlockKeys, quoteNode;
import 'package:easy_localization/easy_localization.dart' hide TextDirection;
import 'package:easy_localization/easy_localization.dart';
@@ -5,7 +5,8 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.da
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/presentation/home/menu/menu_shared_state.dart';
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor/appflowy_editor.dart'
hide QuoteBlockKeys, quoteNode;
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
@@ -18,6 +18,7 @@ import 'operations/ai_writer_cubit.dart';
import 'operations/ai_writer_entities.dart';
import 'operations/ai_writer_node_extension.dart';
import 'suggestion_action_bar.dart';
import 'widgets/ai_writer_gesture_detector.dart';
class AiWriterBlockKeys {
const AiWriterBlockKeys._();
@@ -66,6 +67,10 @@ class AIWriterBlockComponentBuilder extends BlockComponentBuilder {
blockComponentContext,
state,
),
actionTrailingBuilder: (context, state) => actionTrailingBuilder(
blockComponentContext,
state,
),
);
}
@@ -83,6 +88,7 @@ class AiWriterBlockComponent extends BlockComponentStatefulWidget {
required super.node,
super.showActions,
super.actionBuilder,
super.actionTrailingBuilder,
super.configuration = const BlockComponentConfiguration(),
});
@@ -163,13 +169,11 @@ class _AIWriterBlockComponentState extends State<AiWriterBlockComponent> {
children: [
BlocBuilder<AiWriterCubit, AiWriterState>(
builder: (context, state) {
final hitTestBehavior = state is GeneratingAiWriterState
? HitTestBehavior.opaque
: HitTestBehavior.translucent;
return GestureDetector(
behavior: hitTestBehavior,
onTap: () => onTapOutside(),
onTapDown: (_) => onTapOutside(),
return AiWriterGestureDetector(
behavior: state is GeneratingAiWriterState
? HitTestBehavior.opaque
: HitTestBehavior.translucent,
onPointerEvent: () => onTapOutside(),
);
},
),
@@ -280,7 +284,7 @@ class OverlayContent extends StatelessWidget {
hasSelection: hasSelection,
),
onTap: (action) {
context.read<AiWriterCubit>().runResponseAction(action);
_onSelectSuggestionAction(context, action);
},
),
),
@@ -346,9 +350,7 @@ class OverlayContent extends StatelessWidget {
hasSelection: hasSelection,
),
onTap: (action) {
context
.read<AiWriterCubit>()
.runResponseAction(action);
_onSelectSuggestionAction(context, action);
},
),
],
@@ -539,6 +541,18 @@ class OverlayContent extends StatelessWidget {
};
}
}
void _onSelectSuggestionAction(
BuildContext context,
SuggestionAction action,
) {
final predefinedFormat =
context.read<AIPromptInputBloc>().state.predefinedFormat;
context.read<AiWriterCubit>().runResponseAction(
action,
predefinedFormat,
);
}
}
class MainContentArea extends StatelessWidget {
@@ -1,6 +1,7 @@
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/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
@@ -8,7 +9,6 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'ai_writer_block_component.dart';
import 'operations/ai_writer_entities.dart';
const _improveWritingToolbarItemId = 'appflowy.editor.ai_improve_writing';
@@ -223,7 +223,6 @@ void _insertAiNode(EditorState editorState, AiWriterCommand command) async {
command: command,
),
)
..afterSelection = selection
..selectionExtraInfo = {selectionExtraInfoDisableToolbar: true};
await editorState.apply(
@@ -232,6 +231,7 @@ void _insertAiNode(EditorState editorState, AiWriterCommand command) async {
recordUndo: false,
inMemoryUpdate: true,
),
withUpdateSelection: false,
);
}
@@ -240,3 +240,9 @@ bool _isAIEnabled(EditorState editorState) {
return documentContext == null ||
!documentContext.read<DocumentBloc>().isLocalMode;
}
bool onlyShowInTextTypeAndExcludeTable(
EditorState editorState,
) {
return onlyShowInTextType(editorState) && notShowInTable(editorState);
}
@@ -2,9 +2,11 @@ import 'dart:async';
import 'package:appflowy/ai/ai.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/application/view/view_service.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/protobuf/flowy-ai/protobuf.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_result/appflowy_result.dart';
import 'package:bloc/bloc.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/foundation.dart';
@@ -30,7 +32,7 @@ class AiWriterCubit extends Cubit<AiWriterState> {
isFirstRun: true,
),
) {
editorState.service.keyboardService?.disable();
editorState.service.keyboardService?.disableShortcuts();
}
final String documentId;
@@ -47,7 +49,7 @@ class AiWriterCubit extends Cubit<AiWriterState> {
@override
Future<void> close() async {
selectedSourcesNotifier.dispose();
editorState.service.keyboardService?.enable();
editorState.service.keyboardService?.enableShortcuts();
await super.close();
}
@@ -184,11 +186,14 @@ class AiWriterCubit extends Cubit<AiWriterState> {
await removeAiWriterNode(editorState, getAiWriterNode());
}
void runResponseAction(SuggestionAction action) async {
void runResponseAction(
SuggestionAction action, [
PredefinedFormat? predefinedFormat,
]) async {
if (action case SuggestionAction.rewrite || SuggestionAction.tryAgain) {
await _textRobot.discard();
_textRobot.reset();
runCommand(state.command, null, isRetry: true);
runCommand(state.command, predefinedFormat, isRetry: true);
return;
}
@@ -295,32 +300,39 @@ class AiWriterCubit extends Cubit<AiWriterState> {
end: cursorPosition,
).normalized;
final text = await editorState.getMarkdownInSelection(selection);
String text = await editorState.getMarkdownInSelection(selection);
if (text.isEmpty) {
if (state is! ReadyAiWriterState) {
return;
}
final readyState = state as ReadyAiWriterState;
emit(
SingleShotAiWriterState(
command,
title: LocaleKeys.ai_continueWritingEmptyDocumentTitle.tr(),
description:
LocaleKeys.ai_continueWritingEmptyDocumentDescription.tr(),
onDismiss: () {
if (isImmediateRun) {
removeAiWriterNode(editorState, node);
}
},
),
);
emit(readyState);
return;
final view = await ViewBackendService.getView(documentId).toNullable();
if (view == null ||
view.name.isEmpty ||
view.name == LocaleKeys.menuAppHeader_defaultNewPageName.tr()) {
final readyState = state as ReadyAiWriterState;
emit(
SingleShotAiWriterState(
command,
title: LocaleKeys.ai_continueWritingEmptyDocumentTitle.tr(),
description:
LocaleKeys.ai_continueWritingEmptyDocumentDescription.tr(),
onDismiss: () {
if (isImmediateRun) {
removeAiWriterNode(editorState, node);
}
},
),
);
emit(readyState);
return;
} else {
text += view.name;
}
}
final stream = await _aiService.streamCompletion(
objectId: documentId,
text: await editorState.getMarkdownInSelection(selection),
text: text,
completionType: command.toCompletionType(),
onStart: () async {
final transaction = editorState.transaction;
@@ -0,0 +1,40 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
class AiWriterGestureDetector extends StatelessWidget {
const AiWriterGestureDetector({
super.key,
required this.behavior,
required this.onPointerEvent,
this.child,
});
final HitTestBehavior behavior;
final void Function() onPointerEvent;
final Widget? child;
@override
Widget build(BuildContext context) {
return RawGestureDetector(
behavior: behavior,
gestures: <Type, GestureRecognizerFactory>{
TapGestureRecognizer:
GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(
() => TapGestureRecognizer(),
(instance) {
instance
..onTap = onPointerEvent
..onTapDown = (_) => onPointerEvent();
},
),
ImmediateMultiDragGestureRecognizer:
GestureRecognizerFactoryWithHandlers<
ImmediateMultiDragGestureRecognizer>(
() => ImmediateMultiDragGestureRecognizer(), (instance) {
instance.onStart = (offset) => null;
}),
},
child: child,
);
}
}
@@ -208,7 +208,7 @@ class _MobileEmojiPickerButton extends StatelessWidget {
MobileEmojiPickerScreen.iconSelectedType: emoji.type.name,
MobileEmojiPickerScreen.uploadDocumentId: documentId,
MobileEmojiPickerScreen.selectTabs:
tabs.map((e) => e.name).toList(),
tabs.map((e) => e.name).toList().join('-'),
},
).toString(),
);
@@ -7,6 +7,9 @@ const _equals = '=';
const _equalGreater = '⇒';
const _dashGreater = '→';
const _hyphen = '-';
const _emDash = '—'; // This is an em dash — not a single dash - !!
/// format '=' + '>' into an ⇒
///
/// - support
@@ -43,6 +46,24 @@ final CharacterShortcutEvent customFormatDashGreater = CharacterShortcutEvent(
),
);
/// format two hyphens into an em dash
///
/// - support
/// - desktop
/// - mobile
/// - web
///
final CharacterShortcutEvent customFormatDoubleHyphenEmDash =
CharacterShortcutEvent(
key: 'format double hyphen into an em dash',
character: _hyphen,
handler: (editorState) async => _handleDoubleCharacterReplacement(
editorState: editorState,
character: _hyphen,
replacement: _emDash,
),
);
/// If [prefixCharacter] is null or empty, [character] is used
Future<bool> _handleDoubleCharacterReplacement({
required EditorState editorState,
@@ -81,11 +102,29 @@ Future<bool> _handleDoubleCharacterReplacement({
return false;
}
// insert the greater character first and convert it to the replacement character to support undo
final insert = editorState.transaction
..insertText(
node,
selection.end.offset,
character,
);
await editorState.apply(
insert,
skipHistoryDebounce: true,
);
final afterSelection = editorState.selection;
if (afterSelection == null) {
return false;
}
final replace = editorState.transaction
..replaceText(
node,
selection.end.offset - 1,
1,
afterSelection.end.offset - 2,
2,
replacement,
);
@@ -1,9 +1,9 @@
import 'dart:math';
import 'package:flutter/material.dart';
// ignore: implementation_imports
import 'package:appflowy_editor/src/flutter/scrollable_positioned_list/scrollable_positioned_list.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
import 'package:flutter/material.dart';
class SelectableItemListMenu extends StatelessWidget {
const SelectableItemListMenu({
@@ -1,5 +1,10 @@
import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
bool _isTableType(String type) {
return [TableBlockKeys.type, SimpleTableBlockKeys.type].contains(type);
}
bool notShowInTable(EditorState editorState) {
final selection = editorState.selection;
if (selection == null) {
@@ -7,12 +12,12 @@ bool notShowInTable(EditorState editorState) {
}
final nodes = editorState.getNodesInSelection(selection);
return nodes.every((element) {
if (element.type == TableBlockKeys.type) {
if (_isTableType(element.type)) {
return false;
}
var parent = element.parent;
while (parent != null) {
if (parent.type == TableBlockKeys.type) {
if (_isTableType(parent.type)) {
return false;
}
parent = parent.parent;
@@ -42,10 +42,8 @@ class BulletedListIcon extends StatelessWidget {
size: Size.square(size * 0.8),
);
return Container(
constraints: BoxConstraints(
minWidth: size,
minHeight: size,
),
width: size,
height: size,
margin: const EdgeInsets.only(right: 8.0),
alignment: Alignment.center,
child: icon,
@@ -1,6 +1,8 @@
import 'package:appflowy/generated/locale_keys.g.dart' show LocaleKeys;
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/application/document_bloc.dart';
import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';
import 'package:appflowy/shared/icon_emoji_picker/tab.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart'
@@ -80,7 +82,7 @@ class CalloutBlockComponentBuilder extends BlockComponentBuilder {
});
final Color defaultColor;
final EdgeInsets inlinePadding;
final EdgeInsets Function(Node node) inlinePadding;
@override
BlockComponentWidget build(BlockComponentContext blockComponentContext) {
@@ -96,12 +98,15 @@ class CalloutBlockComponentBuilder extends BlockComponentBuilder {
blockComponentContext,
state,
),
actionTrailingBuilder: (context, state) => actionTrailingBuilder(
blockComponentContext,
state,
),
);
}
@override
BlockComponentValidate get validate =>
(node) => node.delta != null && node.children.isEmpty;
BlockComponentValidate get validate => (node) => node.delta != null;
}
// the main widget for rendering the callout block
@@ -111,13 +116,14 @@ class CalloutBlockComponentWidget extends BlockComponentStatefulWidget {
required super.node,
super.showActions,
super.actionBuilder,
super.actionTrailingBuilder,
super.configuration = const BlockComponentConfiguration(),
required this.defaultColor,
required this.inlinePadding,
});
final Color defaultColor;
final EdgeInsets inlinePadding;
final EdgeInsets Function(Node node) inlinePadding;
@override
State<CalloutBlockComponentWidget> createState() =>
@@ -132,7 +138,8 @@ class _CalloutBlockComponentWidgetState
BlockComponentConfigurable,
BlockComponentTextDirectionMixin,
BlockComponentAlignMixin,
BlockComponentBackgroundColorMixin {
BlockComponentBackgroundColorMixin,
NestedBlockComponentStatefulWidgetMixin {
// the key used to forward focus to the richtext child
@override
final forwardKey = GlobalKey(debugLabel: 'flowy_rich_text');
@@ -170,40 +177,63 @@ class _CalloutBlockComponentWidgetState
try {
result = EmojiIconData(FlowyIconType.values.byName(type), icon);
} catch (e) {
Log.error(
'get emoji error with icon:[$icon], type:[$type] within alloutBlockComponentWidget',
Log.info(
'get emoji error with icon:[$icon], type:[$type] within calloutBlockComponentWidget',
e,
);
}
return result;
}
// get access to the editor state via provider
@override
late final editorState = Provider.of<EditorState>(context, listen: false);
Widget buildComponentWithChildren(BuildContext context) {
return Stack(
children: [
Positioned.fill(
left: cachedLeft,
child: Container(
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(6.0)),
color: backgroundColor,
),
),
),
NestedListWidget(
indentPadding: indentPadding.copyWith(bottom: 8),
child: buildComponent(context, withBackgroundColor: false),
children: editorState.renderer.buildList(
context,
widget.node.children,
),
),
],
);
}
// build the callout block widget
@override
Widget build(BuildContext context) {
Widget buildComponent(
BuildContext context, {
bool withBackgroundColor = true,
}) {
final textDirection = calculateTextDirection(
layoutDirection: Directionality.maybeOf(context),
);
final (emojiSize, emojiButtonSize) = calculateEmojiSize();
final documentId = context.read<DocumentBloc?>()?.documentId;
Widget child = Container(
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(8.0)),
borderRadius: const BorderRadius.all(Radius.circular(6.0)),
color: backgroundColor,
),
padding: widget.inlinePadding,
padding: widget.inlinePadding(widget.node),
width: double.infinity,
alignment: alignment,
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
textDirection: textDirection,
children: [
if (UniversalPlatform.isDesktopOrWeb) const HSpace(4.0),
if (UniversalPlatform.isDesktopOrWeb) const HSpace(6.0),
// the emoji picker button for the note
EmojiPickerButton(
// force to refresh the popover state
@@ -217,12 +247,18 @@ class _CalloutBlockComponentWidgetState
emojiSize: emojiSize,
showBorder: false,
buttonSize: emojiButtonSize,
documentId: documentId,
tabs: const [
PickerTabType.emoji,
PickerTabType.icon,
PickerTabType.custom,
],
onSubmitted: (r, controller) {
setEmojiIconData(r.data);
if (!r.keepOpen) controller?.close();
},
),
if (UniversalPlatform.isDesktopOrWeb) const HSpace(4.0),
if (UniversalPlatform.isDesktopOrWeb) const HSpace(6.0),
Flexible(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
@@ -236,7 +272,7 @@ class _CalloutBlockComponentWidgetState
child = Padding(
key: blockComponentKey,
padding: padding,
padding: EdgeInsets.zero,
child: child,
);
@@ -245,6 +281,7 @@ class _CalloutBlockComponentWidgetState
delegate: this,
listenable: editorState.selectionNotifier,
blockColor: editorState.editorStyle.selectionColor,
selectionAboveBlock: true,
supportTypes: const [
BlockSelectionType.block,
],
@@ -255,6 +292,7 @@ class _CalloutBlockComponentWidgetState
child = BlockComponentActionWrapper(
node: widget.node,
actionBuilder: widget.actionBuilder!,
actionTrailingBuilder: widget.actionTrailingBuilder,
child: child,
);
}
@@ -33,8 +33,20 @@ CharacterShortcutEventHandler _insertNewLineHandler = (editorState) async {
if (HardwareKeyboard.instance.isShiftPressed) {
await editorState.insertNewLine();
} else {
await editorState.insertTextAtCurrentSelection('\n');
} else if (node.children.isEmpty) {
// insert a new paragraph within the callout block
final path = node.path.child(0);
final transaction = editorState.transaction;
transaction.insertNode(
path,
paragraphNode(),
);
transaction.afterSelection = Selection.collapsed(
Position(
path: path,
),
);
await editorState.apply(transaction);
}
return true;
@@ -3,6 +3,8 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/base/selec
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/string_extension.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/code_block/code_language_screen.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
// ignore: implementation_imports
import 'package:appflowy_editor/src/flutter/scrollable_positioned_list/scrollable_positioned_list.dart';
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme_extension.dart';
@@ -10,7 +12,6 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:go_router/go_router.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
import 'package:universal_platform/universal_platform.dart';
CodeBlockLanguagePickerBuilder codeBlockLanguagePickerBuilder = (
@@ -7,27 +7,62 @@ import 'package:provider/provider.dart';
Node simpleColumnNode({
List<Node>? children,
double? width,
double? ratio,
}) {
return Node(
type: SimpleColumnBlockKeys.type,
children: children ?? [paragraphNode()],
attributes: {
SimpleColumnBlockKeys.width: width,
SimpleColumnBlockKeys.ratio: ratio,
},
);
}
extension SimpleColumnBlockAttributes on Node {
// get the next column node of the current column node
// if the current column node is the last column node, return null
Node? get nextColumn {
final index = path.last;
final parent = this.parent;
if (parent == null || index == parent.children.length - 1) {
return null;
}
return parent.children[index + 1];
}
// get the previous column node of the current column node
// if the current column node is the first column node, return null
Node? get previousColumn {
final index = path.last;
final parent = this.parent;
if (parent == null || index == 0) {
return null;
}
return parent.children[index - 1];
}
}
class SimpleColumnBlockKeys {
const SimpleColumnBlockKeys._();
static const String type = 'simple_column';
/// @Deprecated Use [SimpleColumnBlockKeys.ratio] instead.
///
/// This field is no longer used since v0.6.9
@Deprecated('Use [SimpleColumnBlockKeys.ratio] instead.')
static const String width = 'width';
/// The ratio of the column width.
///
/// The value is a double number between 0 and 1.
static const String ratio = 'ratio';
}
class SimpleColumnBlockComponentBuilder extends BlockComponentBuilder {
SimpleColumnBlockComponentBuilder({super.configuration});
SimpleColumnBlockComponentBuilder({
super.configuration,
});
@override
BlockComponentWidget build(BlockComponentContext blockComponentContext) {
@@ -51,6 +86,7 @@ class SimpleColumnBlockComponent extends BlockComponentStatefulWidget {
required super.node,
super.showActions,
super.actionBuilder,
super.actionTrailingBuilder,
super.configuration = const BlockComponentConfiguration(),
});
@@ -73,16 +109,6 @@ class SimpleColumnBlockComponentState extends State<SimpleColumnBlockComponent>
late final EditorState editorState = context.read<EditorState>();
@override
void initState() {
super.initState();
}
@override
void dispose() {
super.dispose();
}
@override
Widget build(BuildContext context) {
Widget child = Column(
@@ -120,7 +146,7 @@ class SimpleColumnBlockComponentState extends State<SimpleColumnBlockComponent>
if (SimpleColumnsBlockConstants.enableDebugBorder) {
child = Container(
color: Colors.green.withValues(
alpha: 0.2,
alpha: 0.3,
),
child: child,
);
@@ -1,6 +1,5 @@
import 'package:appflowy/plugins/document/presentation/editor_configuration.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/draggable_option_button.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/columns/simple_columns_block_constant.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';
@@ -26,6 +25,13 @@ class _SimpleColumnBlockWidthResizerState
ValueNotifier<bool> isHovering = ValueNotifier(false);
@override
void dispose() {
isHovering.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return MouseRegion(
@@ -78,25 +84,47 @@ class _SimpleColumnBlockWidthResizerState
// update the column width in memory
final columnNode = widget.columnNode;
final columnsNode = columnNode.columnsParent;
if (columnsNode == null) {
return;
}
final editorWidth = columnsNode.rect.width;
final rect = columnNode.rect;
final width =
columnNode.attributes[SimpleColumnBlockKeys.width] ?? rect.width;
final width = rect.width;
final originalRatio = columnNode.attributes[SimpleColumnBlockKeys.ratio];
final newWidth = width + details.delta.dx;
final transaction = widget.editorState.transaction;
final newRatio = newWidth / editorWidth;
transaction.updateNode(columnNode, {
...columnNode.attributes,
SimpleColumnBlockKeys.width: newWidth.clamp(
SimpleColumnsBlockConstants.minimumColumnWidth,
double.infinity,
),
SimpleColumnBlockKeys.ratio: newRatio,
});
final columnsNode = columnNode.parent;
if (columnsNode != null) {
transaction.updateNode(columnsNode, {
...columnsNode.attributes,
ColumnsBlockKeys.columnCount: columnsNode.children.length,
if (newRatio < 0.1 && newRatio < originalRatio) {
return;
}
final nextColumn = columnNode.nextColumn;
if (nextColumn != null) {
final nextColumnRect = nextColumn.rect;
final nextColumnWidth = nextColumnRect.width;
final newNextColumnWidth = nextColumnWidth - details.delta.dx;
final newNextColumnRatio = newNextColumnWidth / editorWidth;
if (newNextColumnRatio < 0.1) {
return;
}
transaction.updateNode(nextColumn, {
...nextColumn.attributes,
SimpleColumnBlockKeys.ratio: newNextColumnRatio,
});
}
transaction.updateNode(columnsNode, {
...columnsNode.attributes,
ColumnsBlockKeys.columnCount: columnsNode.children.length,
});
widget.editorState.apply(
transaction,
options: ApplyOptions(inMemoryUpdate: true),
@@ -113,9 +141,15 @@ class _SimpleColumnBlockWidthResizerState
// apply the transaction again to make sure the width is updated
final transaction = widget.editorState.transaction;
transaction.updateNode(widget.columnNode, {
...widget.columnNode.attributes,
});
final columnsNode = widget.columnNode.columnsParent;
if (columnsNode == null) {
return;
}
for (final columnNode in columnsNode.children) {
transaction.updateNode(columnNode, {
...columnNode.attributes,
});
}
widget.editorState.apply(transaction);
isDragging = false;
@@ -3,7 +3,7 @@ import 'package:appflowy_editor/appflowy_editor.dart';
extension SimpleColumnNodeExtension on Node {
/// Returns the parent [Node] of the current node if it is a [SimpleColumnsBlock].
Node? get parentColumnsBlock {
Node? get columnsParent {
Node? currentNode = parent;
while (currentNode != null) {
if (currentNode.type == SimpleColumnsBlockKeys.type) {
@@ -15,7 +15,7 @@ extension SimpleColumnNodeExtension on Node {
}
/// Returns the parent [Node] of the current node if it is a [SimpleColumnBlock].
Node? get parentColumn {
Node? get columnParent {
Node? currentNode = parent;
while (currentNode != null) {
if (currentNode.type == SimpleColumnBlockKeys.type) {
@@ -27,8 +27,8 @@ extension SimpleColumnNodeExtension on Node {
}
/// Returns whether the current node is in a [SimpleColumnsBlock].
bool get isInColumnsBlock => parentColumnsBlock != null;
bool get isInColumnsBlock => columnsParent != null;
/// Returns whether the current node is in a [SimpleColumnBlock].
bool get isInColumnBlock => parentColumn != null;
bool get isInColumnBlock => columnParent != null;
}
@@ -1,3 +1,5 @@
import 'dart:math';
import 'package:appflowy/plugins/document/presentation/editor_plugins/columns/simple_columns_block_constant.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy_backend/log.dart';
@@ -5,20 +7,19 @@ import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:universal_platform/universal_platform.dart';
// if the children is not provided, it will create two columns by default.
// if the columnCount is provided, it will create the specified number of columns.
Node simpleColumnsNode({
List<Node>? children,
int? columnCount,
double? width,
double? ratio,
}) {
columnCount ??= 2;
children ??= List.generate(
columnCount,
(index) => simpleColumnNode(
width: width,
ratio: ratio,
children: [paragraphNode()],
),
);
@@ -67,6 +68,7 @@ class ColumnsBlockComponent extends BlockComponentStatefulWidget {
required super.node,
super.showActions,
super.actionBuilder,
super.actionTrailingBuilder,
super.configuration = const BlockComponentConfiguration(),
});
@@ -90,6 +92,13 @@ class ColumnsBlockComponentState extends State<ColumnsBlockComponent>
final ScrollController scrollController = ScrollController();
@override
void initState() {
super.initState();
_updateColumnsBlock();
}
@override
void dispose() {
scrollController.dispose();
@@ -99,23 +108,12 @@ class ColumnsBlockComponentState extends State<ColumnsBlockComponent>
@override
Widget build(BuildContext context) {
Widget child = SingleChildScrollView(
scrollDirection: Axis.horizontal,
controller: scrollController,
child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: _buildChildren(),
),
Widget child = Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: _buildChildren(),
);
if (UniversalPlatform.isDesktop) {
// only show the scrollbar on desktop
child = Scrollbar(
controller: scrollController,
child: child,
);
}
child = Align(
alignment: Alignment.topLeft,
child: IntrinsicHeight(
@@ -147,19 +145,18 @@ class ColumnsBlockComponentState extends State<ColumnsBlockComponent>
}
List<Widget> _buildChildren() {
final length = node.children.length;
final children = <Widget>[];
for (var i = 0; i < node.children.length; i++) {
for (var i = 0; i < length; i++) {
final childNode = node.children[i];
final width =
childNode.attributes[SimpleColumnBlockKeys.width]?.toDouble() ??
SimpleColumnsBlockConstants.minimumColumnWidth;
final double ratio =
childNode.attributes[SimpleColumnBlockKeys.ratio]?.toDouble() ??
1.0 / length;
Widget child = editorState.renderer.build(context, childNode);
child = SizedBox(
width: width.clamp(
SimpleColumnsBlockConstants.minimumColumnWidth,
double.infinity,
),
child = Expanded(
flex: (max(ratio, 0.1) * 10000).toInt(),
child: child,
);
@@ -175,6 +172,26 @@ class ColumnsBlockComponentState extends State<ColumnsBlockComponent>
return children;
}
// Update the existing columns block data
// if the column ratio is not existing, it will be set to 1.0 / columnCount
void _updateColumnsBlock() {
final transaction = editorState.transaction;
final length = node.children.length;
for (int i = 0; i < length; i++) {
final childNode = node.children[i];
final ratio = childNode.attributes[SimpleColumnBlockKeys.ratio];
if (ratio == null) {
transaction.updateNode(childNode, {
...childNode.attributes,
SimpleColumnBlockKeys.ratio: 1.0 / length,
});
}
}
if (transaction.operations.isNotEmpty) {
editorState.apply(transaction);
}
}
@override
Position start() => Position(path: widget.node.path);
@@ -42,6 +42,10 @@ class DatabaseViewBlockComponentBuilder extends BlockComponentBuilder {
blockComponentContext,
state,
),
actionTrailingBuilder: (context, state) => actionTrailingBuilder(
blockComponentContext,
state,
),
);
}
@@ -58,6 +62,7 @@ class DatabaseBlockComponentWidget extends BlockComponentStatefulWidget {
required super.node,
super.showActions,
super.actionBuilder,
super.actionTrailingBuilder,
super.configuration = const BlockComponentConfiguration(),
});
@@ -30,6 +30,10 @@ class ErrorBlockComponentBuilder extends BlockComponentBuilder {
blockComponentContext,
state,
),
actionTrailingBuilder: (context, state) => actionTrailingBuilder(
blockComponentContext,
state,
),
);
}
@@ -43,6 +47,7 @@ class ErrorBlockComponentWidget extends BlockComponentStatefulWidget {
required super.node,
super.showActions,
super.actionBuilder,
super.actionTrailingBuilder,
super.configuration = const BlockComponentConfiguration(),
});
@@ -81,6 +86,7 @@ class _ErrorBlockComponentWidgetState extends State<ErrorBlockComponentWidget>
child = BlockComponentActionWrapper(
node: node,
actionBuilder: widget.actionBuilder!,
actionTrailingBuilder: widget.actionTrailingBuilder,
child: child,
);
}
@@ -141,6 +141,7 @@ class FileBlockComponent extends BlockComponentStatefulWidget {
required super.node,
super.showActions,
super.actionBuilder,
super.actionTrailingBuilder,
super.configuration = const BlockComponentConfiguration(),
});
@@ -303,6 +304,7 @@ class FileBlockComponentState extends State<FileBlockComponent>
child = BlockComponentActionWrapper(
node: node,
actionBuilder: widget.actionBuilder!,
actionTrailingBuilder: widget.actionTrailingBuilder,
child: child,
);
}
@@ -1,7 +1,6 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/shared_context/shared_context.dart';
import 'package:appflowy/plugins/document/presentation/editor_style.dart';
import 'package:appflowy/shared/text_field/text_filed_with_metric_lines.dart';
import 'package:appflowy/workspace/application/appearance_defaults.dart';
import 'package:appflowy/workspace/application/view/view_bloc.dart';
@@ -93,7 +92,6 @@ class _InnerCoverTitleState extends State<_InnerCoverTitle> {
builder: (context, state) {
final appearance = context.read<DocumentAppearanceCubit>().state;
return Container(
padding: EditorStyleCustomizer.documentPaddingWithOptionMenu,
constraints: BoxConstraints(maxWidth: width),
child: Theme(
data: Theme.of(context).copyWith(
@@ -196,7 +196,7 @@ class _DocumentCoverWidgetState extends State<DocumentCoverWidget> {
],
),
Padding(
padding: const EdgeInsets.only(bottom: 12.0),
padding: EdgeInsets.fromLTRB(offset, 0, offset, 12),
child: MouseRegion(
onEnter: (event) => isCoverTitleHovered.value = true,
onExit: (event) => isCoverTitleHovered.value = false,
@@ -123,6 +123,7 @@ class CustomImageBlockComponent extends BlockComponentStatefulWidget {
required super.node,
super.showActions,
super.actionBuilder,
super.actionTrailingBuilder,
super.configuration = const BlockComponentConfiguration(),
this.showMenu = false,
this.menuBuilder,
@@ -226,6 +227,7 @@ class CustomImageBlockComponentState extends State<CustomImageBlockComponent>
delegate: this,
listenable: editorState.selectionNotifier,
blockColor: editorState.editorStyle.selectionColor,
selectionAboveBlock: true,
supportTypes: const [BlockSelectionType.block],
child: child,
);
@@ -235,6 +237,7 @@ class CustomImageBlockComponentState extends State<CustomImageBlockComponent>
child = BlockComponentActionWrapper(
node: node,
actionBuilder: widget.actionBuilder!,
actionTrailingBuilder: widget.actionTrailingBuilder,
child: child,
);
}
@@ -311,7 +314,7 @@ class CustomImageBlockComponentState extends State<CustomImageBlockComponent>
}) {
final imageBox = imageKey.currentContext?.findRenderObject();
if (imageBox is RenderBox) {
return Offset.zero & imageBox.size;
return padding.topLeft & imageBox.size;
}
return Rect.zero;
}
@@ -82,6 +82,7 @@ class MultiImageBlockComponent extends BlockComponentStatefulWidget {
this.menuBuilder,
super.configuration = const BlockComponentConfiguration(),
super.actionBuilder,
super.actionTrailingBuilder,
});
final bool showMenu;
@@ -190,6 +191,7 @@ class MultiImageBlockComponentState extends State<MultiImageBlockComponent>
child = BlockComponentActionWrapper(
node: node,
actionBuilder: widget.actionBuilder!,
actionTrailingBuilder: widget.actionTrailingBuilder,
child: child,
);
}
@@ -79,6 +79,10 @@ class MathEquationBlockComponentBuilder extends BlockComponentBuilder {
blockComponentContext,
state,
),
actionTrailingBuilder: (context, state) => actionTrailingBuilder(
blockComponentContext,
state,
),
);
}
@@ -94,6 +98,7 @@ class MathEquationBlockComponentWidget extends BlockComponentStatefulWidget {
required super.node,
super.showActions,
super.actionBuilder,
super.actionTrailingBuilder,
super.configuration = const BlockComponentConfiguration(),
});
@@ -157,6 +162,7 @@ class MathEquationBlockComponentWidgetState
child = BlockComponentActionWrapper(
node: node,
actionBuilder: widget.actionBuilder!,
actionTrailingBuilder: widget.actionTrailingBuilder,
child: child,
);
}
@@ -7,7 +7,8 @@ import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy/workspace/application/view/view_service.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor/appflowy_editor.dart'
hide QuoteBlockComponentBuilder, quoteNode, QuoteBlockKeys;
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
import 'package:collection/collection.dart';
import 'package:string_validator/string_validator.dart';
@@ -6,7 +6,8 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_too
import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_popup_menu.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_toolbar_theme.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor/appflowy_editor.dart'
hide QuoteBlockKeys, quoteNode;
import 'package:collection/collection.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
@@ -12,7 +12,8 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.da
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/startup/tasks/app_widget.dart';
import 'package:appflowy/workspace/presentation/home/menu/menu_shared_state.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor/appflowy_editor.dart'
hide QuoteBlockComponentBuilder, quoteNode, QuoteBlockKeys;
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
@@ -29,7 +29,7 @@ class NumberedListIcon extends StatelessWidget {
);
return Padding(
padding: const EdgeInsets.only(right: 8.0),
padding: const EdgeInsets.only(left: 6.0, right: 10.0),
child: Text(
node.levelString,
style: adjustedTextStyle,
@@ -52,6 +52,10 @@ class OutlineBlockComponentBuilder extends BlockComponentBuilder {
blockComponentContext,
state,
),
actionTrailingBuilder: (context, state) => actionTrailingBuilder(
blockComponentContext,
state,
),
);
}
@@ -65,6 +69,7 @@ class OutlineBlockWidget extends BlockComponentStatefulWidget {
required super.node,
super.showActions,
super.actionBuilder,
super.actionTrailingBuilder,
super.configuration = const BlockComponentConfiguration(),
});
@@ -76,7 +81,9 @@ class _OutlineBlockWidgetState extends State<OutlineBlockWidget>
with
BlockComponentConfigurable,
BlockComponentTextDirectionMixin,
BlockComponentBackgroundColorMixin {
BlockComponentBackgroundColorMixin,
DefaultSelectableMixin,
SelectableMixin {
// Change the value if the heading block type supports heading levels greater than '3'
static const maxVisibleDepth = 6;
@@ -90,6 +97,17 @@ class _OutlineBlockWidgetState extends State<OutlineBlockWidget>
late EditorState editorState = context.read<EditorState>();
late Stream<EditorTransactionValue> stream = editorState.transactionStream;
@override
GlobalKey<State<StatefulWidget>> blockComponentKey = GlobalKey(
debugLabel: OutlineBlockKeys.type,
);
@override
GlobalKey<State<StatefulWidget>> get containerKey => widget.node.key;
@override
GlobalKey<State<StatefulWidget>> get forwardKey => widget.node.key;
@override
Widget build(BuildContext context) {
return StreamBuilder(
@@ -97,11 +115,25 @@ class _OutlineBlockWidgetState extends State<OutlineBlockWidget>
builder: (context, snapshot) {
Widget child = _buildOutlineBlock();
child = BlockSelectionContainer(
node: node,
delegate: this,
listenable: editorState.selectionNotifier,
remoteSelection: editorState.remoteSelections,
blockColor: editorState.editorStyle.selectionColor,
selectionAboveBlock: true,
supportTypes: const [
BlockSelectionType.block,
],
child: child,
);
if (UniversalPlatform.isDesktopOrWeb) {
if (widget.showActions && widget.actionBuilder != null) {
child = BlockComponentActionWrapper(
node: widget.node,
actionBuilder: widget.actionBuilder!,
actionTrailingBuilder: widget.actionTrailingBuilder,
child: child,
);
}
@@ -170,6 +202,7 @@ class _OutlineBlockWidgetState extends State<OutlineBlockWidget>
}
return Container(
key: blockComponentKey,
constraints: const BoxConstraints(
minHeight: 40.0,
),
@@ -263,10 +296,13 @@ class OutlineItemWidget extends StatelessWidget {
textDirection: textDirection,
children: [
HSpace(node.leftIndent),
Text(
node.outlineItemText,
textDirection: textDirection,
style: style,
Flexible(
child: Text(
node.outlineItemText,
textDirection: textDirection,
style: style,
overflow: TextOverflow.ellipsis,
),
),
],
),
@@ -25,6 +25,7 @@ class CustomPageBlockComponent extends BlockComponentStatelessWidget {
required super.node,
super.showActions,
super.actionBuilder,
super.actionTrailingBuilder,
super.configuration = const BlockComponentConfiguration(),
this.header,
this.footer,
@@ -1,4 +1,5 @@
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
class CalloutNodeParser extends NodeParser {
@@ -10,7 +11,6 @@ class CalloutNodeParser extends NodeParser {
@override
String transform(Node node, DocumentMarkdownEncoder? encoder) {
assert(node.children.isEmpty);
final icon = node.attributes[CalloutBlockKeys.icon];
final delta = node.delta ?? Delta()
..insert('');
final String markdown = DeltaMarkdownEncoder()
@@ -18,9 +18,15 @@ class CalloutNodeParser extends NodeParser {
.split('\n')
.map((e) => '> $e')
.join('\n');
final type = node.attributes[CalloutBlockKeys.iconType];
final icon = type == FlowyIconType.emoji.name || type == null || type == ""
? node.attributes[CalloutBlockKeys.icon]
: null;
final content = icon == null ? markdown : "> $icon\n$markdown";
return '''
> $icon
$markdown
$content
''';
}
@@ -106,6 +106,7 @@ class MarkdownSimpleTableParser extends CustomMarkdownParser {
return [
simpleTableBlockNode(
children: rows,
enableHeaderRow: true,
columnWidths: UniversalPlatform.isMobile || tableWidth == null
? null
: {for (var i = 0; i < th.length; i++) i.toString(): tableWidth!},
@@ -63,6 +63,7 @@ export 'outline/outline_block_component.dart';
export 'parsers/document_markdown_parsers.dart';
export 'parsers/markdown_parsers.dart';
export 'parsers/markdown_simple_table_parser.dart';
export 'quote/quote_block_component.dart';
export 'quote/quote_block_shortcuts.dart';
export 'shortcuts/character_shortcuts.dart';
export 'shortcuts/command_shortcuts.dart';
@@ -0,0 +1,304 @@
import 'dart:async';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
typedef QuoteBlockIconBuilder = Widget Function(
BuildContext context,
Node node,
);
class QuoteBlockKeys {
const QuoteBlockKeys._();
static const String type = 'quote';
static const String delta = blockComponentDelta;
static const String backgroundColor = blockComponentBackgroundColor;
static const String textDirection = blockComponentTextDirection;
}
Node quoteNode({
Delta? delta,
String? textDirection,
Attributes? attributes,
Iterable<Node>? children,
}) {
attributes ??= {'delta': (delta ?? Delta()).toJson()};
return Node(
type: QuoteBlockKeys.type,
attributes: {
...attributes,
if (textDirection != null) QuoteBlockKeys.textDirection: textDirection,
},
children: children ?? [],
);
}
class QuoteBlockComponentBuilder extends BlockComponentBuilder {
QuoteBlockComponentBuilder({
super.configuration,
this.iconBuilder,
});
final QuoteBlockIconBuilder? iconBuilder;
@override
BlockComponentWidget build(BlockComponentContext blockComponentContext) {
final node = blockComponentContext.node;
return QuoteBlockComponentWidget(
key: node.key,
node: node,
configuration: configuration,
iconBuilder: iconBuilder,
showActions: showActions(node),
actionBuilder: (context, state) => actionBuilder(
blockComponentContext,
state,
),
actionTrailingBuilder: (context, state) => actionTrailingBuilder(
blockComponentContext,
state,
),
);
}
@override
BlockComponentValidate get validate => (node) => node.delta != null;
}
class QuoteBlockComponentWidget extends BlockComponentStatefulWidget {
const QuoteBlockComponentWidget({
super.key,
required super.node,
super.showActions,
super.actionBuilder,
super.actionTrailingBuilder,
super.configuration = const BlockComponentConfiguration(),
this.iconBuilder,
});
final QuoteBlockIconBuilder? iconBuilder;
@override
State<QuoteBlockComponentWidget> createState() =>
_QuoteBlockComponentWidgetState();
}
class _QuoteBlockComponentWidgetState extends State<QuoteBlockComponentWidget>
with
SelectableMixin,
DefaultSelectableMixin,
BlockComponentConfigurable,
BlockComponentBackgroundColorMixin,
BlockComponentTextDirectionMixin,
BlockComponentAlignMixin,
NestedBlockComponentStatefulWidgetMixin {
@override
final forwardKey = GlobalKey(debugLabel: 'flowy_rich_text');
@override
GlobalKey<State<StatefulWidget>> get containerKey => widget.node.key;
@override
GlobalKey<State<StatefulWidget>> blockComponentKey = GlobalKey(
debugLabel: QuoteBlockKeys.type,
);
@override
BlockComponentConfiguration get configuration => widget.configuration;
@override
Node get node => widget.node;
ValueNotifier<double> quoteBlockHeightNotifier = ValueNotifier(0);
StreamSubscription<EditorTransactionValue>? _transactionSubscription;
final GlobalKey layoutBuilderKey = GlobalKey();
@override
void initState() {
super.initState();
_observerQuoteBlockChanges();
}
@override
void dispose() {
_transactionSubscription?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
_updateQuoteBlockHeight();
return KeyedSubtree(
key: layoutBuilderKey,
child: node.children.isEmpty
? buildComponent(context)
: buildComponentWithChildren(context),
);
},
);
}
@override
Widget buildComponent(
BuildContext context, {
bool withBackgroundColor = true,
}) {
final textDirection = calculateTextDirection(
layoutDirection: Directionality.maybeOf(context),
);
Widget child = AppFlowyRichText(
key: forwardKey,
delegate: this,
node: widget.node,
editorState: editorState,
textAlign: alignment?.toTextAlign ?? textAlign,
placeholderText: placeholderText,
textSpanDecorator: (textSpan) => textSpan.updateTextStyle(
textStyleWithTextSpan(textSpan: textSpan),
),
placeholderTextSpanDecorator: (textSpan) => textSpan.updateTextStyle(
placeholderTextStyleWithTextSpan(textSpan: textSpan),
),
textDirection: textDirection,
cursorColor: editorState.editorStyle.cursorColor,
selectionColor: editorState.editorStyle.selectionColor,
cursorWidth: editorState.editorStyle.cursorWidth,
);
child = Container(
width: double.infinity,
alignment: alignment,
child: IntrinsicHeight(
child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
textDirection: textDirection,
children: [
widget.iconBuilder != null
? widget.iconBuilder!(context, node)
: ValueListenableBuilder<double>(
valueListenable: quoteBlockHeightNotifier,
builder: (context, height, child) {
return QuoteIcon(height: height);
},
),
Flexible(
child: child,
),
],
),
),
);
child = Container(
color: backgroundColor,
child: Padding(
key: blockComponentKey,
padding: padding,
child: child,
),
);
child = BlockSelectionContainer(
node: node,
delegate: this,
listenable: editorState.selectionNotifier,
remoteSelection: editorState.remoteSelections,
blockColor: editorState.editorStyle.selectionColor,
supportTypes: const [
BlockSelectionType.block,
],
child: child,
);
if (widget.showActions && widget.actionBuilder != null) {
child = BlockComponentActionWrapper(
node: node,
actionBuilder: widget.actionBuilder!,
actionTrailingBuilder: widget.actionTrailingBuilder,
child: child,
);
}
return child;
}
void _updateQuoteBlockHeight() {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
final renderObject = layoutBuilderKey.currentContext?.findRenderObject();
if (renderObject != null && renderObject is RenderBox) {
quoteBlockHeightNotifier.value =
renderObject.size.height - padding.top * 2;
} else {
quoteBlockHeightNotifier.value = 0;
}
});
}
void _observerQuoteBlockChanges() {
_transactionSubscription = editorState.transactionStream.listen((event) {
final time = event.$1;
if (time != TransactionTime.before) {
return;
}
final transaction = event.$2;
final operations = transaction.operations;
for (final operation in operations) {
if (node.path.isAncestorOf(operation.path)) {
_updateQuoteBlockHeight();
}
}
});
}
}
class QuoteIcon extends StatelessWidget {
const QuoteIcon({
super.key,
this.height = 0,
});
final double height;
@override
Widget build(BuildContext context) {
final textScaleFactor =
context.read<EditorState>().editorStyle.textScaleFactor;
return Container(
alignment: Alignment.center,
constraints:
const BoxConstraints(minWidth: 22, minHeight: 22, maxHeight: 22) *
textScaleFactor,
padding: const EdgeInsets.only(right: 6.0),
child: SizedBox(
width: 3 * textScaleFactor,
// use overflow box to ensure the container can overflow the height so that the children of the quote block can have the quote
child: OverflowBox(
alignment: Alignment.topCenter,
maxHeight: height,
child: Container(
width: 3 * textScaleFactor,
height: height,
color: Theme.of(context).colorScheme.primary,
),
),
),
);
}
}
@@ -31,8 +31,20 @@ CharacterShortcutEventHandler _insertNewLineHandler = (editorState) async {
if (HardwareKeyboard.instance.isShiftPressed) {
await editorState.insertNewLine();
} else {
await editorState.insertTextAtCurrentSelection('\n');
} else if (node.children.isEmpty) {
// insert a new paragraph within the callout block
final path = node.path.child(0);
final transaction = editorState.transaction;
transaction.insertNode(
path,
paragraphNode(),
);
transaction.afterSelection = Selection.collapsed(
Position(
path: path,
),
);
await editorState.apply(transaction);
}
return true;
@@ -44,6 +44,7 @@ List<CharacterShortcutEvent> buildCharacterShortcutEvents(
customFormatGreaterEqual,
customFormatDashGreater,
customFormatDoubleHyphenEmDash,
customFormatNumberToNumberedList,
customFormatSignToHeading,
@@ -55,6 +56,7 @@ List<CharacterShortcutEvent> buildCharacterShortcutEvents(
formatGreaterEqual, // Overridden by customFormatGreaterEqual
formatNumberToNumberedList, // Overridden by customFormatNumberToNumberedList
formatSignToHeading, // Overridden by customFormatSignToHeading
formatDoubleHyphenEmDash, // Overridden by customFormatDoubleHyphenEmDash
].contains(shortcut),
),
@@ -161,6 +161,10 @@ class SimpleTableBlockComponentBuilder extends BlockComponentBuilder {
blockComponentContext,
state,
),
actionTrailingBuilder: (context, state) => actionTrailingBuilder(
blockComponentContext,
state,
),
);
}
@@ -174,6 +178,7 @@ class SimpleTableBlockWidget extends BlockComponentStatefulWidget {
required super.node,
super.showActions,
super.actionBuilder,
super.actionTrailingBuilder,
super.configuration = const BlockComponentConfiguration(),
required this.alwaysDistributeColumnWidths,
});
@@ -262,6 +267,7 @@ class _SimpleTableBlockWidgetState extends State<SimpleTableBlockWidget>
child = BlockComponentActionWrapper(
node: node,
actionBuilder: widget.actionBuilder!,
actionTrailingBuilder: widget.actionTrailingBuilder,
child: child,
);
}
@@ -47,6 +47,10 @@ class SimpleTableCellBlockComponentBuilder extends BlockComponentBuilder {
blockComponentContext,
state,
),
actionTrailingBuilder: (context, state) => actionTrailingBuilder(
blockComponentContext,
state,
),
);
}
@@ -60,6 +64,7 @@ class SimpleTableCellBlockWidget extends BlockComponentStatefulWidget {
required super.node,
super.showActions,
super.actionBuilder,
super.actionTrailingBuilder,
super.configuration = const BlockComponentConfiguration(),
required this.alwaysDistributeColumnWidths,
});
@@ -39,6 +39,10 @@ class SimpleTableRowBlockComponentBuilder extends BlockComponentBuilder {
blockComponentContext,
state,
),
actionTrailingBuilder: (context, state) => actionTrailingBuilder(
blockComponentContext,
state,
),
);
}
@@ -52,6 +56,7 @@ class SimpleTableRowBlockWidget extends BlockComponentStatefulWidget {
required super.node,
super.showActions,
super.actionBuilder,
super.actionTrailingBuilder,
super.configuration = const BlockComponentConfiguration(),
required this.alwaysDistributeColumnWidths,
});
@@ -90,13 +90,8 @@ SelectionMenuItem fourColumnsSlashMenuItem = SelectionMenuItem.node(
);
Node _buildColumnsNode(EditorState editorState, int columnCount) {
final selection = editorState.selection;
double? width;
if (selection != null) {
final parentNode = editorState.getNodeAtPath(selection.start.path);
if (parentNode != null) {
width = parentNode.rect.width / columnCount - 16;
}
}
return simpleColumnsNode(columnCount: columnCount, width: width);
return simpleColumnsNode(
columnCount: columnCount,
ratio: 1.0 / columnCount,
);
}
@@ -73,6 +73,7 @@ class SubPageBlockComponent extends BlockComponentStatefulWidget {
required super.node,
super.showActions,
super.actionBuilder,
super.actionTrailingBuilder,
super.configuration = const BlockComponentConfiguration(),
});
@@ -295,6 +296,7 @@ class SubPageBlockComponentState extends State<SubPageBlockComponent>
child = BlockComponentActionWrapper(
node: node,
actionBuilder: widget.actionBuilder!,
actionTrailingBuilder: widget.actionTrailingBuilder,
child: child,
);
}
@@ -111,6 +111,10 @@ class ToggleListBlockComponentBuilder extends BlockComponentBuilder {
blockComponentContext,
state,
),
actionTrailingBuilder: (context, state) => actionTrailingBuilder(
blockComponentContext,
state,
),
);
}
@@ -124,6 +128,7 @@ class ToggleListBlockComponentWidget extends BlockComponentStatefulWidget {
required super.node,
super.showActions,
super.actionBuilder,
super.actionTrailingBuilder,
super.configuration = const BlockComponentConfiguration(),
this.padding = const EdgeInsets.all(0),
this.textStyleBuilder,
@@ -247,6 +252,7 @@ class _ToggleListBlockComponentWidgetState
child = BlockComponentActionWrapper(
node: node,
actionBuilder: widget.actionBuilder!,
actionTrailingBuilder: widget.actionTrailingBuilder,
child: child,
);
}
@@ -284,83 +284,110 @@ class IconPicker extends StatefulWidget {
class _IconPickerState extends State<IconPicker> {
final mutex = PopoverMutex();
PopoverController? childPopoverController;
@override
void dispose() {
super.dispose();
childPopoverController = null;
}
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: widget.iconGroups.length,
padding: const EdgeInsets.symmetric(horizontal: 16.0),
itemBuilder: (context, index) {
final iconGroup = widget.iconGroups[index];
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
FlowyText(
iconGroup.displayName.capitalize(),
fontSize: 12,
figmaLineHeight: 18.0,
color: context.pickerTextColor,
),
const VSpace(4.0),
GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: widget.iconPerLine,
),
itemCount: iconGroup.icons.length,
physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true,
itemBuilder: (context, index) {
final icon = iconGroup.icons[index];
return widget.enableBackgroundColorSelection
? _Icon(
icon: icon,
mutex: mutex,
onSelectedColor: (context, color) {
String groupName = iconGroup.name;
if (groupName == _kRecentIconGroupName) {
groupName = getGroupName(index);
}
widget.onSelectedIcon(
IconsData(
groupName,
icon.name,
color,
),
return GestureDetector(
onTap: hideColorSelector,
child: NotificationListener(
onNotification: (notificationInfo) {
if (notificationInfo is ScrollStartNotification) {
hideColorSelector();
}
return true;
},
child: ListView.builder(
itemCount: widget.iconGroups.length,
padding: const EdgeInsets.symmetric(horizontal: 16.0),
itemBuilder: (context, index) {
final iconGroup = widget.iconGroups[index];
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
FlowyText(
iconGroup.displayName.capitalize(),
fontSize: 12,
figmaLineHeight: 18.0,
color: context.pickerTextColor,
),
const VSpace(4.0),
GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: widget.iconPerLine,
),
itemCount: iconGroup.icons.length,
physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true,
itemBuilder: (context, index) {
final icon = iconGroup.icons[index];
return widget.enableBackgroundColorSelection
? _Icon(
icon: icon,
mutex: mutex,
onOpen: (childPopoverController) {
this.childPopoverController =
childPopoverController;
},
onSelectedColor: (context, color) {
String groupName = iconGroup.name;
if (groupName == _kRecentIconGroupName) {
groupName = getGroupName(index);
}
widget.onSelectedIcon(
IconsData(
groupName,
icon.name,
color,
),
);
RecentIcons.putIcon(RecentIcon(icon, groupName));
PopoverContainer.of(context).close();
},
)
: _IconNoBackground(
icon: icon,
onSelectedIcon: () {
String groupName = iconGroup.name;
if (groupName == _kRecentIconGroupName) {
groupName = getGroupName(index);
}
widget.onSelectedIcon(
IconsData(
groupName,
icon.name,
null,
),
);
RecentIcons.putIcon(RecentIcon(icon, groupName));
},
);
RecentIcons.putIcon(RecentIcon(icon, groupName));
PopoverContainer.of(context).close();
},
)
: _IconNoBackground(
icon: icon,
onSelectedIcon: () {
String groupName = iconGroup.name;
if (groupName == _kRecentIconGroupName) {
groupName = getGroupName(index);
}
widget.onSelectedIcon(
IconsData(
groupName,
icon.name,
null,
),
);
RecentIcons.putIcon(RecentIcon(icon, groupName));
},
);
},
),
const VSpace(12.0),
if (index == widget.iconGroups.length - 1) ...[
const StreamlinePermit(),
const VSpace(12.0),
],
],
);
},
},
),
const VSpace(12.0),
if (index == widget.iconGroups.length - 1) ...[
const StreamlinePermit(),
const VSpace(12.0),
],
],
);
},
),
),
);
}
void hideColorSelector() {
childPopoverController?.close();
childPopoverController = null;
}
String getGroupName(int index) {
final recentIcons = RecentIcons.getIconsSync();
try {
@@ -376,9 +403,11 @@ class _IconNoBackground extends StatelessWidget {
const _IconNoBackground({
required this.icon,
required this.onSelectedIcon,
this.isSelected = false,
});
final Icon icon;
final bool isSelected;
final VoidCallback onSelectedIcon;
@override
@@ -387,6 +416,7 @@ class _IconNoBackground extends StatelessWidget {
message: icon.displayName,
preferBelow: false,
child: FlowyButton(
isSelected: isSelected,
useIntrinsicWidth: true,
onTap: () => onSelectedIcon(),
margin: const EdgeInsets.all(8.0),
@@ -408,11 +438,13 @@ class _Icon extends StatefulWidget {
required this.icon,
required this.mutex,
required this.onSelectedColor,
this.onOpen,
});
final Icon icon;
final PopoverMutex mutex;
final void Function(BuildContext context, String color) onSelectedColor;
final ValueChanged<PopoverController>? onOpen;
@override
State<_Icon> createState() => _IconState();
@@ -420,6 +452,7 @@ class _Icon extends StatefulWidget {
class _IconState extends State<_Icon> {
final PopoverController _popoverController = PopoverController();
bool isSelected = false;
@override
void dispose() {
@@ -434,10 +467,18 @@ class _IconState extends State<_Icon> {
controller: _popoverController,
offset: const Offset(0, 6),
mutex: widget.mutex,
onClose: () {
updateIsSelected(false);
},
clickHandler: PopoverClickHandler.gestureDetector,
child: _IconNoBackground(
icon: widget.icon,
onSelectedIcon: () => _popoverController.show(),
isSelected: isSelected,
onSelectedIcon: () {
updateIsSelected(true);
_popoverController.show();
widget.onOpen?.call(_popoverController);
},
),
popupBuilder: (context) {
return Container(
@@ -449,6 +490,12 @@ class _IconState extends State<_Icon> {
},
);
}
void updateIsSelected(bool isSelected) {
setState(() {
this.isSelected = isSelected;
});
}
}
class StreamlinePermit extends StatelessWidget {
@@ -15,7 +15,7 @@ void handleOpenWorkspaceError(BuildContext context, FlowyError error) {
getIt<AuthRouter>().pushWorkspaceErrorScreen(context, userFolder, error);
break;
case ErrorCode.InvalidEncryptSecret:
case ErrorCode.HttpError:
case ErrorCode.NetworkError:
showToastNotification(
context,
message: error.msg,
@@ -1,167 +0,0 @@
import 'dart:async';
import 'dart:ffi';
import 'dart:isolate';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/ai_chat/application/chat_entity.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart';
import 'package:bloc/bloc.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:fixnum/fixnum.dart';
part 'download_model_bloc.freezed.dart';
class DownloadModelBloc extends Bloc<DownloadModelEvent, DownloadModelState> {
DownloadModelBloc(LLMModelPB model)
: super(DownloadModelState.initial(model)) {
on<DownloadModelEvent>(_handleEvent);
}
Future<void> _handleEvent(
DownloadModelEvent event,
Emitter<DownloadModelState> emit,
) async {
await event.when(
started: () async {
final downloadStream = DownloadingStream();
downloadStream.listen(
onModelPercentage: (name, percent) {
if (!isClosed) {
add(
DownloadModelEvent.updatePercent(name, percent),
);
}
},
onPluginPercentage: (percent) {
if (!isClosed) {
add(DownloadModelEvent.updatePercent("AppFlowy Plugin", percent));
}
},
onFinish: () {
add(const DownloadModelEvent.downloadFinish());
},
onError: (err) {
Log.error(err);
},
);
final payload =
DownloadLLMPB(progressStream: Int64(downloadStream.nativePort));
final result = await AIEventDownloadLLMResource(payload).send();
result.fold((_) {
emit(
state.copyWith(
downloadStream: downloadStream,
loadingState: const ChatLoadingState.finish(),
downloadError: null,
),
);
}, (err) {
emit(
state.copyWith(
loadingState: ChatLoadingState.finish(error: err),
),
);
});
},
updatePercent: (String object, double percent) {
emit(state.copyWith(object: object, percent: percent));
},
downloadFinish: () {
emit(state.copyWith(isFinish: true));
},
);
}
@override
Future<void> close() async {
await state.downloadStream?.dispose();
return super.close();
}
}
@freezed
class DownloadModelEvent with _$DownloadModelEvent {
const factory DownloadModelEvent.started() = _Started;
const factory DownloadModelEvent.updatePercent(
String object,
double percent,
) = _UpdatePercent;
const factory DownloadModelEvent.downloadFinish() = _DownloadFinish;
}
@freezed
class DownloadModelState with _$DownloadModelState {
const factory DownloadModelState({
required LLMModelPB model,
DownloadingStream? downloadStream,
String? downloadError,
@Default("") String object,
@Default(0) double percent,
@Default(false) bool isFinish,
String? bigFileDownloadPrompt,
@Default(ChatLoadingState.loading()) ChatLoadingState loadingState,
}) = _DownloadModelState;
factory DownloadModelState.initial(LLMModelPB model) {
// bigger than 1 GB then show download big file prompt
String? bigFileDownloadPrompt;
if (model.fileSize > 1 * 1024 * 1024 * 1024) {
bigFileDownloadPrompt =
LocaleKeys.settings_aiPage_keys_downloadBigFilePrompt.tr();
}
return DownloadModelState(
model: model,
bigFileDownloadPrompt: bigFileDownloadPrompt,
);
}
}
class DownloadingStream {
DownloadingStream() {
_port.handler = _controller.add;
}
final RawReceivePort _port = RawReceivePort();
StreamSubscription<String>? _sub;
final StreamController<String> _controller = StreamController.broadcast();
int get nativePort => _port.sendPort.nativePort;
Future<void> dispose() async {
await _sub?.cancel();
await _controller.close();
_port.close();
}
void listen({
void Function(String modelName, double percent)? onModelPercentage,
void Function(double percent)? onPluginPercentage,
void Function(String data)? onError,
void Function()? onFinish,
}) {
_sub = _controller.stream.listen((text) {
if (text.contains(':progress:')) {
final progressIndex = text.indexOf(':progress:');
final modelName = text.substring(0, progressIndex);
final progressValue = text
.substring(progressIndex + 10); // 10 is the length of ":progress:"
final percent = double.tryParse(progressValue);
if (percent != null) {
onModelPercentage?.call(modelName, percent);
}
} else if (text.startsWith('plugin:progress:')) {
final percent = double.tryParse(text.substring(16));
if (percent != null) {
onPluginPercentage?.call(percent);
}
} else if (text.startsWith('finish')) {
onFinish?.call();
} else if (text.startsWith('error:')) {
// substring 6 to remove "error:"
onError?.call(text.substring(6));
}
});
}
}
@@ -18,7 +18,7 @@ class DownloadOfflineAIBloc
) async {
await event.when(
started: () async {
final result = await AIEventGetOfflineAIAppLink().send();
final result = await AIEventGetLocalAIDownloadLink().send();
await result.fold(
(app) async {
await launchUrl(Uri.parse(app.link));
@@ -52,7 +52,8 @@ class LocalAIToggleBloc extends Bloc<LocalAIToggleEvent, LocalAIToggleState> {
(localAI) {
emit(
state.copyWith(
pageIndicator: LocalAIToggleStateIndicator.ready(localAI.enabled),
pageIndicator:
LocalAIToggleStateIndicator.isEnabled(localAI.enabled),
),
);
},
@@ -88,6 +89,6 @@ class LocalAIToggleState with _$LocalAIToggleState {
class LocalAIToggleStateIndicator with _$LocalAIToggleStateIndicator {
// when start downloading the model
const factory LocalAIToggleStateIndicator.error(FlowyError error) = _OnError;
const factory LocalAIToggleStateIndicator.ready(bool isEnabled) = _Ready;
const factory LocalAIToggleStateIndicator.isEnabled(bool isEnabled) = _Ready;
const factory LocalAIToggleStateIndicator.loading() = _Loading;
}
@@ -1,185 +1,61 @@
import 'dart:async';
import 'package:appflowy/plugins/ai_chat/application/chat_entity.dart';
import 'package:appflowy/workspace/application/settings/ai/local_llm_listener.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_result/appflowy_result.dart';
import 'package:bloc/bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'local_ai_chat_bloc.freezed.dart';
class LocalAIChatSettingBloc
extends Bloc<LocalAIChatSettingEvent, LocalAIChatSettingState> {
LocalAIChatSettingBloc()
class LocalAISettingPanelBloc
extends Bloc<LocalAISettingPanelEvent, LocalAISettingPanelState> {
LocalAISettingPanelBloc()
: listener = LocalLLMListener(),
super(const LocalAIChatSettingState()) {
super(const LocalAISettingPanelState()) {
listener.start(
stateCallback: (newState) {
if (!isClosed) {
add(LocalAIChatSettingEvent.updatePluginState(newState));
add(LocalAISettingPanelEvent.updateAIState(newState));
}
},
);
on<LocalAIChatSettingEvent>(_handleEvent);
on<LocalAISettingPanelEvent>(_handleEvent);
}
final LocalLLMListener listener;
/// Handles incoming events and dispatches them to the appropriate handler.
Future<void> _handleEvent(
LocalAIChatSettingEvent event,
Emitter<LocalAIChatSettingState> emit,
LocalAISettingPanelEvent event,
Emitter<LocalAISettingPanelState> emit,
) async {
await event.when(
refreshAISetting: _handleStarted,
didLoadModelInfo: (FlowyResult<LLMModelInfoPB, FlowyError> result) {
result.fold(
(modelInfo) {
_fetchCurremtLLMState();
emit(
state.copyWith(
modelInfo: modelInfo,
models: modelInfo.models,
selectedLLMModel: modelInfo.selectedModel,
aiModelProgress: const AIModelProgress.finish(),
),
);
},
(err) {
emit(
state.copyWith(
aiModelProgress: AIModelProgress.finish(error: err),
),
);
},
);
},
selectLLMConfig: (LLMModelPB llmModel) async {
final result = await AIEventUpdateLocalLLM(llmModel).send();
result.fold(
(llmResource) {
// If all resources are downloaded, show reload plugin
if (llmResource.pendingResources.isNotEmpty) {
emit(
state.copyWith(
selectedLLMModel: llmModel,
progressIndicator: LocalAIProgress.showDownload(
llmResource,
llmModel,
),
selectLLMState: const ChatLoadingState.finish(),
),
);
} else {
emit(
state.copyWith(
selectedLLMModel: llmModel,
selectLLMState: const ChatLoadingState.finish(),
progressIndicator: const LocalAIProgress.checkPluginState(),
),
);
event.when(
started: () {
AIEventGetLocalAIState().send().fold(
(localAIState) {
if (!isClosed) {
add(LocalAISettingPanelEvent.updateAIState(localAIState));
}
},
(err) {
emit(
state.copyWith(
selectLLMState: ChatLoadingState.finish(error: err),
),
);
},
Log.error,
);
},
refreshLLMState: (LocalModelResourcePB llmResource) {
if (state.selectedLLMModel == null) {
Log.error(
'Unexpected null selected config. It should be set already',
);
return;
}
// reload plugin if all resources are downloaded
if (llmResource.pendingResources.isEmpty) {
updateAIState: (LocalAIPB pluginState) {
if (pluginState.isPluginExecutableReady) {
emit(
state.copyWith(
runningState: pluginState.state,
progressIndicator: const LocalAIProgress.checkPluginState(),
),
);
} else {
if (state.selectedLLMModel != null) {
// Go to download page if the selected model is downloading
if (llmResource.isDownloading) {
emit(
state.copyWith(
progressIndicator:
LocalAIProgress.startDownloading(state.selectedLLMModel!),
selectLLMState: const ChatLoadingState.finish(),
),
);
return;
} else {
emit(
state.copyWith(
progressIndicator: LocalAIProgress.showDownload(
llmResource,
state.selectedLLMModel!,
),
selectLLMState: const ChatLoadingState.finish(),
),
);
}
}
}
},
startDownloadModel: (LLMModelPB llmModel) {
emit(
state.copyWith(
progressIndicator: LocalAIProgress.startDownloading(llmModel),
selectLLMState: const ChatLoadingState.finish(),
),
);
},
cancelDownload: () async {
final _ = await AIEventCancelDownloadLLMResource().send();
_fetchCurremtLLMState();
},
finishDownload: () async {
emit(
state.copyWith(
progressIndicator: const LocalAIProgress.finishDownload(),
),
);
},
updatePluginState: (LocalAIPluginStatePB pluginState) {
if (pluginState.offlineAiReady) {
AIEventRefreshLocalAIModelInfo().send().then((result) {
if (!isClosed) {
add(LocalAIChatSettingEvent.didLoadModelInfo(result));
}
});
if (pluginState.state == RunningStatePB.Stopped) {
emit(
state.copyWith(
runningState: pluginState.state,
progressIndicator: const LocalAIProgress.checkPluginState(),
),
);
} else {
emit(
state.copyWith(
runningState: pluginState.state,
),
);
}
} else {
emit(
state.copyWith(
progressIndicator: const LocalAIProgress.startOfflineAIApp(),
progressIndicator: const LocalAIProgress.downloadLocalAIApp(),
),
);
}
@@ -187,39 +63,6 @@ class LocalAIChatSettingBloc
);
}
void _fetchCurremtLLMState() async {
final result = await AIEventGetLocalLLMState().send();
result.fold(
(llmResource) {
if (!isClosed) {
add(LocalAIChatSettingEvent.refreshLLMState(llmResource));
}
},
(err) {
Log.error(err);
},
);
}
/// Handles the event to fetch local AI settings when the application starts.
Future<void> _handleStarted() async {
final result = await AIEventGetLocalAIPluginState().send();
result.fold(
(pluginState) async {
if (!isClosed) {
add(LocalAIChatSettingEvent.updatePluginState(pluginState));
if (pluginState.offlineAiReady) {
final result = await AIEventRefreshLocalAIModelInfo().send();
if (!isClosed) {
add(LocalAIChatSettingEvent.didLoadModelInfo(result));
}
}
}
},
(err) => Log.error(err.toString()),
);
}
@override
Future<void> close() async {
await listener.stop();
@@ -228,60 +71,24 @@ class LocalAIChatSettingBloc
}
@freezed
class LocalAIChatSettingEvent with _$LocalAIChatSettingEvent {
const factory LocalAIChatSettingEvent.refreshAISetting() = _RefreshAISetting;
const factory LocalAIChatSettingEvent.didLoadModelInfo(
FlowyResult<LLMModelInfoPB, FlowyError> result,
) = _ModelInfo;
const factory LocalAIChatSettingEvent.selectLLMConfig(LLMModelPB config) =
_SelectLLMConfig;
const factory LocalAIChatSettingEvent.refreshLLMState(
LocalModelResourcePB llmResource,
) = _RefreshLLMResource;
const factory LocalAIChatSettingEvent.startDownloadModel(
LLMModelPB llmModel,
) = _StartDownloadModel;
const factory LocalAIChatSettingEvent.cancelDownload() = _CancelDownload;
const factory LocalAIChatSettingEvent.finishDownload() = _FinishDownload;
const factory LocalAIChatSettingEvent.updatePluginState(
LocalAIPluginStatePB pluginState,
) = _PluginState;
class LocalAISettingPanelEvent with _$LocalAISettingPanelEvent {
const factory LocalAISettingPanelEvent.started() = _Started;
const factory LocalAISettingPanelEvent.updateAIState(
LocalAIPB aiState,
) = _UpdateAIState;
}
@freezed
class LocalAIChatSettingState with _$LocalAIChatSettingState {
const factory LocalAIChatSettingState({
LLMModelInfoPB? modelInfo,
LLMModelPB? selectedLLMModel,
class LocalAISettingPanelState with _$LocalAISettingPanelState {
const factory LocalAISettingPanelState({
LocalAIProgress? progressIndicator,
@Default(AIModelProgress.init()) AIModelProgress aiModelProgress,
@Default(ChatLoadingState.loading()) ChatLoadingState selectLLMState,
@Default([]) List<LLMModelPB> models,
@Default(RunningStatePB.Connecting) RunningStatePB runningState,
}) = _LocalAIChatSettingState;
}
@freezed
class LocalAIProgress with _$LocalAIProgress {
// when user comes back to the setting page, it will auto detect current llm state
const factory LocalAIProgress.showDownload(
LocalModelResourcePB llmResource,
LLMModelPB llmModel,
) = _DownloadNeeded;
// when start downloading the model
const factory LocalAIProgress.startDownloading(LLMModelPB llmModel) =
_Downloading;
const factory LocalAIProgress.finishDownload() = _Finish;
const factory LocalAIProgress.checkPluginState() = _CheckPluginState;
const factory LocalAIProgress.startOfflineAIApp() = _StartOfflineAIApp;
}
@freezed
class AIModelProgress with _$AIModelProgress {
const factory AIModelProgress.init() = _AIModelProgressInit;
const factory AIModelProgress.loading() = _AIModelDownloading;
const factory AIModelProgress.finish({FlowyError? error}) = _AIModelFinish;
const factory LocalAIProgress.checkPluginState() = _CheckPluginStateProgress;
const factory LocalAIProgress.downloadLocalAIApp() =
_DownloadLocalAIAppProgress;
}
@@ -20,25 +20,9 @@ class LocalAIChatToggleBloc
) async {
await event.when(
started: () async {
final result = await AIEventGetLocalAIChatState().send();
final result = await AIEventGetLocalAIState().send();
_handleResult(emit, result);
},
toggle: () async {
emit(
state.copyWith(
pageIndicator: const LocalAIChatToggleStateIndicator.loading(),
),
);
unawaited(
AIEventToggleLocalAIChat().send().then(
(result) {
if (!isClosed) {
add(LocalAIChatToggleEvent.handleResult(result));
}
},
),
);
},
handleResult: (result) {
_handleResult(emit, result);
},
@@ -47,7 +31,7 @@ class LocalAIChatToggleBloc
void _handleResult(
Emitter<LocalAIChatToggleState> emit,
FlowyResult<LocalAIChatPB, FlowyError> result,
FlowyResult<LocalAIPB, FlowyError> result,
) {
result.fold(
(localAI) {
@@ -72,9 +56,8 @@ class LocalAIChatToggleBloc
@freezed
class LocalAIChatToggleEvent with _$LocalAIChatToggleEvent {
const factory LocalAIChatToggleEvent.started() = _Started;
const factory LocalAIChatToggleEvent.toggle() = _Toggle;
const factory LocalAIChatToggleEvent.handleResult(
FlowyResult<LocalAIChatPB, FlowyError> result,
FlowyResult<LocalAIPB, FlowyError> result,
) = _HandleResult;
}
@@ -8,8 +8,8 @@ import 'package:appflowy_backend/protobuf/flowy-notification/subject.pb.dart';
import 'package:appflowy_backend/rust_stream.dart';
import 'package:appflowy_result/appflowy_result.dart';
typedef PluginStateCallback = void Function(LocalAIPluginStatePB state);
typedef LocalAIChatCallback = void Function(LocalAIChatPB chatState);
typedef PluginStateCallback = void Function(LocalAIPB state);
typedef PluginResourceCallback = void Function(LackOfAIResourcePB data);
class LocalLLMListener {
LocalLLMListener() {
@@ -24,15 +24,14 @@ class LocalLLMListener {
ChatNotificationParser? _parser;
PluginStateCallback? stateCallback;
LocalAIChatCallback? chatStateCallback;
void Function()? finishStreamingCallback;
PluginResourceCallback? resourceCallback;
void start({
PluginStateCallback? stateCallback,
LocalAIChatCallback? chatStateCallback,
PluginResourceCallback? resourceCallback,
}) {
this.stateCallback = stateCallback;
this.chatStateCallback = chatStateCallback;
this.resourceCallback = resourceCallback;
}
void _callback(
@@ -41,11 +40,11 @@ class LocalLLMListener {
) {
result.map((r) {
switch (ty) {
case ChatNotification.UpdateChatPluginState:
stateCallback?.call(LocalAIPluginStatePB.fromBuffer(r));
case ChatNotification.UpdateLocalAIState:
stateCallback?.call(LocalAIPB.fromBuffer(r));
break;
case ChatNotification.UpdateLocalChatAI:
chatStateCallback?.call(LocalAIChatPB.fromBuffer(r));
case ChatNotification.LocalAIResourceUpdated:
resourceCallback?.call(LackOfAIResourcePB.fromBuffer(r));
break;
default:
break;
@@ -0,0 +1,221 @@
import 'dart:async';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart';
import 'package:appflowy_result/appflowy_result.dart';
import 'package:bloc/bloc.dart';
import 'package:collection/collection.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:equatable/equatable.dart';
part 'ollama_setting_bloc.freezed.dart';
class OllamaSettingBloc extends Bloc<OllamaSettingEvent, OllamaSettingState> {
OllamaSettingBloc() : super(const OllamaSettingState()) {
on<OllamaSettingEvent>(_handleEvent);
}
Future<void> _handleEvent(
OllamaSettingEvent event,
Emitter<OllamaSettingState> emit,
) async {
event.when(
started: () {
AIEventGetLocalAISetting().send().fold(
(setting) {
if (!isClosed) {
add(OllamaSettingEvent.didLoadSetting(setting));
}
},
Log.error,
);
},
didLoadSetting: (setting) => _updateSetting(setting, emit),
updateSetting: (setting) => _updateSetting(setting, emit),
onEdit: (content, settingType) {
final updatedSubmittedItems = state.submittedItems
.map(
(item) => item.settingType == settingType
? SubmittedItem(
content: content,
settingType: item.settingType,
)
: item,
)
.toList();
// Convert both lists to maps: {settingType: content}
final updatedMap = {
for (final item in updatedSubmittedItems)
item.settingType: item.content,
};
final inputMap = {
for (final item in state.inputItems) item.settingType: item.content,
};
// Compare maps instead of lists
final isEdited = !const MapEquality<SettingType, String>()
.equals(updatedMap, inputMap);
emit(
state.copyWith(
submittedItems: updatedSubmittedItems,
isEdited: isEdited,
),
);
},
submit: () {
final setting = LocalAISettingPB();
final settingUpdaters = <SettingType, void Function(String)>{
SettingType.serverUrl: (value) => setting.serverUrl = value,
SettingType.chatModel: (value) => setting.chatModelName = value,
SettingType.embeddingModel: (value) =>
setting.embeddingModelName = value,
};
for (final item in state.submittedItems) {
settingUpdaters[item.settingType]?.call(item.content);
}
add(OllamaSettingEvent.updateSetting(setting));
AIEventUpdateLocalAISetting(setting).send().fold(
(_) {
Log.info('AI setting updated successfully');
},
(err) => Log.error("update ai setting failed: $err"),
);
},
);
}
void _updateSetting(
LocalAISettingPB setting,
Emitter<OllamaSettingState> emit,
) {
emit(
state.copyWith(
setting: setting,
inputItems: _createInputItems(setting),
submittedItems: _createSubmittedItems(setting),
isEdited: false, // Reset to false when the setting is loaded/updated.
),
);
}
List<SettingItem> _createInputItems(LocalAISettingPB setting) => [
SettingItem(
content: setting.serverUrl,
hintText: 'http://localhost:11434',
settingType: SettingType.serverUrl,
),
SettingItem(
content: setting.chatModelName,
hintText: 'llama3.1',
settingType: SettingType.chatModel,
),
SettingItem(
content: setting.embeddingModelName,
hintText: 'nomic-embed-text',
settingType: SettingType.embeddingModel,
),
];
List<SubmittedItem> _createSubmittedItems(LocalAISettingPB setting) => [
SubmittedItem(
content: setting.serverUrl,
settingType: SettingType.serverUrl,
),
SubmittedItem(
content: setting.chatModelName,
settingType: SettingType.chatModel,
),
SubmittedItem(
content: setting.embeddingModelName,
settingType: SettingType.embeddingModel,
),
];
}
// Create an enum for setting type.
enum SettingType {
serverUrl,
chatModel,
embeddingModel; // semicolon needed after the enum values
String get title {
switch (this) {
case SettingType.serverUrl:
return 'Ollama server url';
case SettingType.chatModel:
return 'Chat model name';
case SettingType.embeddingModel:
return 'Embedding model name';
}
}
}
class SettingItem extends Equatable {
const SettingItem({
required this.content,
required this.hintText,
required this.settingType,
});
final String content;
final String hintText;
final SettingType settingType;
@override
List<Object?> get props => [content, settingType];
}
class SubmittedItem extends Equatable {
const SubmittedItem({
required this.content,
required this.settingType,
});
final String content;
final SettingType settingType;
@override
List<Object?> get props => [content, settingType];
}
@freezed
class OllamaSettingEvent with _$OllamaSettingEvent {
const factory OllamaSettingEvent.started() = _Started;
const factory OllamaSettingEvent.didLoadSetting(LocalAISettingPB setting) =
_DidLoadSetting;
const factory OllamaSettingEvent.updateSetting(LocalAISettingPB setting) =
_UpdateSetting;
const factory OllamaSettingEvent.onEdit(
String content,
SettingType settingType,
) = _OnEdit;
const factory OllamaSettingEvent.submit() = _OnSubmit;
}
@freezed
class OllamaSettingState with _$OllamaSettingState {
const factory OllamaSettingState({
LocalAISettingPB? setting,
@Default([
SettingItem(
content: 'http://localhost:11434',
hintText: 'http://localhost:11434',
settingType: SettingType.serverUrl,
),
SettingItem(
content: 'llama3.1',
hintText: 'llama3.1',
settingType: SettingType.chatModel,
),
SettingItem(
content: 'nomic-embed-text',
hintText: 'nomic-embed-text',
settingType: SettingType.embeddingModel,
),
])
List<SettingItem> inputItems,
@Default([]) List<SubmittedItem> submittedItems,
@Default(false) bool isEdited,
}) = _PluginStateState;
}
@@ -1,13 +1,11 @@
import 'dart:async';
import 'package:appflowy/core/helpers/url_launcher.dart';
import 'package:appflowy/workspace/application/settings/ai/local_llm_listener.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart';
import 'package:bloc/bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:url_launcher/url_launcher.dart' show launchUrl;
part 'plugin_state_bloc.freezed.dart';
@@ -16,13 +14,18 @@ class PluginStateBloc extends Bloc<PluginStateEvent, PluginStateState> {
: listener = LocalLLMListener(),
super(
const PluginStateState(
action: PluginStateAction.init(),
action: PluginStateAction.unknown(),
),
) {
listener.start(
stateCallback: (pluginState) {
if (!isClosed) {
add(PluginStateEvent.updateState(pluginState));
add(PluginStateEvent.updateLocalAIState(pluginState));
}
},
resourceCallback: (data) {
if (!isClosed) {
add(PluginStateEvent.resourceStateChange(data));
}
},
);
@@ -44,66 +47,71 @@ class PluginStateBloc extends Bloc<PluginStateEvent, PluginStateState> {
) async {
await event.when(
started: () async {
final result = await AIEventGetLocalAIPluginState().send();
final result = await AIEventGetLocalAIState().send();
result.fold(
(pluginState) {
if (!isClosed) {
add(PluginStateEvent.updateState(pluginState));
add(PluginStateEvent.updateLocalAIState(pluginState));
}
},
(err) => Log.error(err.toString()),
);
},
updateState: (LocalAIPluginStatePB pluginState) {
updateLocalAIState: (LocalAIPB aiState) {
// if the offline ai is not started, ask user to start it
if (pluginState.offlineAiReady) {
// Chech state of the plugin
switch (pluginState.state) {
case RunningStatePB.Connecting:
emit(
const PluginStateState(
action: PluginStateAction.loadingPlugin(),
),
);
case RunningStatePB.Running:
emit(const PluginStateState(action: PluginStateAction.ready()));
break;
default:
emit(
state.copyWith(action: const PluginStateAction.restartPlugin()),
);
break;
}
} else {
if (aiState.hasLackOfResource()) {
emit(
const PluginStateState(
action: PluginStateAction.startAIOfflineApp(),
PluginStateState(
action: PluginStateAction.lackOfResource(aiState.lackOfResource),
),
);
return;
}
// Chech state of the plugin
switch (aiState.state) {
case RunningStatePB.ReadyToRun:
emit(
const PluginStateState(
action: PluginStateAction.readToRun(),
),
);
case RunningStatePB.Connecting:
emit(
const PluginStateState(
action: PluginStateAction.initializingPlugin(),
),
);
case RunningStatePB.Connected:
emit(
const PluginStateState(
action: PluginStateAction.initializingPlugin(),
),
);
break;
case RunningStatePB.Running:
emit(const PluginStateState(action: PluginStateAction.running()));
break;
case RunningStatePB.Stopped:
emit(
state.copyWith(action: const PluginStateAction.restartPlugin()),
);
default:
break;
}
},
restartLocalAI: () async {
emit(
const PluginStateState(action: PluginStateAction.loadingPlugin()),
const PluginStateState(action: PluginStateAction.restartPlugin()),
);
unawaited(AIEventRestartLocalAIChat().send());
unawaited(AIEventRestartLocalAI().send());
},
openModelDirectory: () async {
final result = await AIEventGetModelStorageDirectory().send();
result.fold(
(data) {
afLaunchUri(Uri.file(data.filePath));
},
(err) => Log.error(err.toString()),
);
},
downloadOfflineAIApp: () async {
final result = await AIEventGetOfflineAIAppLink().send();
await result.fold(
(app) async {
await launchUrl(Uri.parse(app.link));
},
(err) {},
resourceStateChange: (data) {
emit(
PluginStateState(
action: PluginStateAction.lackOfResource(data.resourceDesc),
),
);
},
);
@@ -113,12 +121,11 @@ class PluginStateBloc extends Bloc<PluginStateEvent, PluginStateState> {
@freezed
class PluginStateEvent with _$PluginStateEvent {
const factory PluginStateEvent.started() = _Started;
const factory PluginStateEvent.updateState(LocalAIPluginStatePB pluginState) =
_UpdatePluginState;
const factory PluginStateEvent.updateLocalAIState(LocalAIPB aiState) =
_UpdateLocalAIState;
const factory PluginStateEvent.restartLocalAI() = _RestartLocalAI;
const factory PluginStateEvent.openModelDirectory() =
_OpenModelStorageDirectory;
const factory PluginStateEvent.downloadOfflineAIApp() = _DownloadOfflineAIApp;
const factory PluginStateEvent.resourceStateChange(LackOfAIResourcePB data) =
_ResourceStateChange;
}
@freezed
@@ -130,9 +137,10 @@ class PluginStateState with _$PluginStateState {
@freezed
class PluginStateAction with _$PluginStateAction {
const factory PluginStateAction.init() = _Init;
const factory PluginStateAction.loadingPlugin() = _LoadingPlugin;
const factory PluginStateAction.ready() = _Ready;
const factory PluginStateAction.unknown() = _Unknown;
const factory PluginStateAction.readToRun() = _ReadyToRun;
const factory PluginStateAction.initializingPlugin() = _InitializingPlugin;
const factory PluginStateAction.running() = _PluginRunning;
const factory PluginStateAction.restartPlugin() = _RestartPlugin;
const factory PluginStateAction.startAIOfflineApp() = _StartAIOfflineApp;
const factory PluginStateAction.lackOfResource(String desc) = _LackOfResource;
}
@@ -1,119 +0,0 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/application/settings/ai/download_model_bloc.dart';
import 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/style_widget/button.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:percent_indicator/linear_percent_indicator.dart';
class DownloadingIndicator extends StatelessWidget {
const DownloadingIndicator({
required this.llmModel,
required this.onCancel,
required this.onFinish,
super.key,
});
final LLMModelPB llmModel;
final VoidCallback onCancel;
final VoidCallback onFinish;
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) =>
DownloadModelBloc(llmModel)..add(const DownloadModelEvent.started()),
child: BlocListener<DownloadModelBloc, DownloadModelState>(
listener: (context, state) {
if (state.isFinish) {
onFinish();
}
},
child: DecoratedBox(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(8),
),
child: BlocBuilder<DownloadModelBloc, DownloadModelState>(
builder: (context, state) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
DownloadingProgressBar(onCancel: onCancel),
if (state.bigFileDownloadPrompt != null) ...[
const VSpace(2),
Opacity(
opacity: 0.6,
child:
FlowyText(state.bigFileDownloadPrompt!, fontSize: 11),
),
],
],
);
},
),
),
),
);
}
}
class DownloadingProgressBar extends StatelessWidget {
const DownloadingProgressBar({required this.onCancel, super.key});
final VoidCallback onCancel;
@override
Widget build(BuildContext context) {
return BlocBuilder<DownloadModelBloc, DownloadModelState>(
builder: (context, state) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Opacity(
opacity: 0.6,
child: FlowyText(
"${LocaleKeys.settings_aiPage_keys_downloadingModel.tr()}: ${state.object}",
fontSize: 11,
),
),
IntrinsicHeight(
child: Row(
children: [
Expanded(
child: LinearPercentIndicator(
lineHeight: 9.0,
percent: state.percent,
padding: EdgeInsets.zero,
progressColor: AFThemeExtension.of(context).success,
backgroundColor:
AFThemeExtension.of(context).progressBarBGColor,
barRadius: const Radius.circular(8),
trailing: FlowyText(
"${(state.percent * 100).toStringAsFixed(0)}%",
fontSize: 11,
color: AFThemeExtension.of(context).success,
),
),
),
const HSpace(12),
FlowyButton(
useIntrinsicWidth: true,
text: FlowyText(
LocaleKeys.button_cancel.tr(),
fontSize: 11,
),
onTap: onCancel,
),
],
),
),
],
);
},
);
}
}
@@ -22,7 +22,7 @@ class InitLocalAIIndicator extends StatelessWidget {
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
child: BlocBuilder<LocalAIChatSettingBloc, LocalAIChatSettingState>(
child: BlocBuilder<LocalAISettingPanelBloc, LocalAISettingPanelState>(
builder: (context, state) {
switch (state.runningState) {
case RunningStatePB.Connecting:
@@ -31,7 +31,7 @@ class InitLocalAIIndicator extends StatelessWidget {
children: [
const HSpace(8),
FlowyText(
LocaleKeys.settings_aiPage_keys_localAILoading.tr(),
LocaleKeys.settings_aiPage_keys_localAIInitializing.tr(),
fontSize: 11,
color: const Color(0xFF1E4620),
),
@@ -1,370 +0,0 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/workspace/application/settings/ai/local_ai_chat_bloc.dart';
import 'package:appflowy/workspace/application/settings/ai/local_ai_chat_toggle_bloc.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart';
import 'package:appflowy/workspace/presentation/settings/pages/setting_ai_view/downloading_model.dart';
import 'package:appflowy/workspace/presentation/settings/pages/setting_ai_view/init_local_ai.dart';
import 'package:appflowy/workspace/presentation/settings/pages/setting_ai_view/plugin_state.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';
import 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:expandable/expandable.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/presentation/settings/shared/af_dropdown_menu_entry.dart';
import 'package:appflowy/workspace/presentation/settings/shared/settings_dropdown.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class LocalAIChatSetting extends StatelessWidget {
const LocalAIChatSetting({super.key});
@override
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider(create: (context) => LocalAIChatSettingBloc()),
BlocProvider(
create: (context) => LocalAIChatToggleBloc()
..add(const LocalAIChatToggleEvent.started()),
),
],
child: ExpandableNotifier(
child: BlocListener<LocalAIChatToggleBloc, LocalAIChatToggleState>(
listener: (context, state) {
// Listen to the toggle state and expand the panel if the state is ready.
final controller = ExpandableController.of(
context,
required: true,
)!;
// Neet to wrap with WidgetsBinding.instance.addPostFrameCallback otherwise the
// ExpandablePanel not expanded sometimes. Maybe because the ExpandablePanel is not
// built yet when the listener is called.
WidgetsBinding.instance.addPostFrameCallback(
(_) {
state.pageIndicator.when(
error: (_) => controller.expanded = false,
ready: (enabled) {
controller.expanded = enabled;
context.read<LocalAIChatSettingBloc>().add(
const LocalAIChatSettingEvent.refreshAISetting(),
);
},
loading: () => controller.expanded = false,
);
},
debugLabel: 'LocalAI.showLocalAIChatSetting',
);
},
child: ExpandablePanel(
theme: const ExpandableThemeData(
headerAlignment: ExpandablePanelHeaderAlignment.center,
tapBodyToCollapse: false,
hasIcon: false,
tapBodyToExpand: false,
tapHeaderToExpand: false,
),
header: const SizedBox.shrink(),
collapsed: const SizedBox.shrink(),
expanded: Padding(
padding: const EdgeInsets.symmetric(vertical: 6),
// child: _LocalLLMInfoWidget(),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
BlocBuilder<LocalAIChatSettingBloc, LocalAIChatSettingState>(
builder: (context, state) {
// If the progress indicator is startOfflineAIApp, then don't show the LLM model.
if (state.progressIndicator ==
const LocalAIProgress.startOfflineAIApp()) {
return const SizedBox.shrink();
} else {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: FlowyText.medium(
LocaleKeys.settings_aiPage_keys_llmModel.tr(),
fontSize: 14,
),
),
const Spacer(),
state.aiModelProgress.when(
init: () => const SizedBox.shrink(),
loading: () {
return const Expanded(
child: Row(
children: [
Spacer(),
CircularProgressIndicator.adaptive(),
],
),
);
},
finish: (err) => (err == null)
? const _SelectLocalModelDropdownMenu()
: const SizedBox.shrink(),
),
],
);
}
},
),
const IntrinsicHeight(child: _LocalAIStateWidget()),
],
),
),
),
),
),
);
}
}
class LocalAIChatSettingHeader extends StatelessWidget {
const LocalAIChatSettingHeader({super.key});
@override
Widget build(BuildContext context) {
return BlocBuilder<LocalAIChatToggleBloc, LocalAIChatToggleState>(
builder: (context, state) {
return state.pageIndicator.when(
error: (error) {
return const SizedBox.shrink();
},
loading: () {
return Row(
children: [
FlowyText(
LocaleKeys.settings_aiPage_keys_localAIStart.tr(),
),
const Spacer(),
const CircularProgressIndicator.adaptive(),
const HSpace(8),
],
);
},
ready: (isEnabled) {
return Row(
children: [
const FlowyText('Enable Local AI Chat'),
const Spacer(),
Toggle(
value: isEnabled,
onChanged: (_) {
context
.read<LocalAIChatToggleBloc>()
.add(const LocalAIChatToggleEvent.toggle());
},
),
],
);
},
);
},
);
}
}
class _SelectLocalModelDropdownMenu extends StatelessWidget {
const _SelectLocalModelDropdownMenu();
@override
Widget build(BuildContext context) {
return BlocBuilder<LocalAIChatSettingBloc, LocalAIChatSettingState>(
builder: (context, state) {
return Flexible(
child: SettingsDropdown<LLMModelPB>(
key: const Key('_SelectLocalModelDropdownMenu'),
onChanged: (model) => context.read<LocalAIChatSettingBloc>().add(
LocalAIChatSettingEvent.selectLLMConfig(model),
),
selectedOption: state.selectedLLMModel!,
options: state.models
.map(
(llm) => buildDropdownMenuEntry<LLMModelPB>(
context,
value: llm,
label: llm.chatModel,
),
)
.toList(),
),
);
},
);
}
}
class _LocalAIStateWidget extends StatelessWidget {
const _LocalAIStateWidget();
@override
Widget build(BuildContext context) {
return BlocBuilder<LocalAIChatSettingBloc, LocalAIChatSettingState>(
builder: (context, state) {
final error = errorFromState(state);
if (error == null) {
// If the error is null, handle selected llm model.
if (state.progressIndicator != null) {
final child = state.progressIndicator!.when(
showDownload: (
LocalModelResourcePB llmResource,
LLMModelPB llmModel,
) =>
_ShowDownloadIndicator(
llmResource: llmResource,
llmModel: llmModel,
),
startDownloading: (llmModel) {
return DownloadingIndicator(
key: UniqueKey(),
llmModel: llmModel,
onFinish: () => context
.read<LocalAIChatSettingBloc>()
.add(const LocalAIChatSettingEvent.finishDownload()),
onCancel: () => context
.read<LocalAIChatSettingBloc>()
.add(const LocalAIChatSettingEvent.cancelDownload()),
);
},
finishDownload: () => const InitLocalAIIndicator(),
checkPluginState: () => const PluginStateIndicator(),
startOfflineAIApp: () => OpenOrDownloadOfflineAIApp(
onRetry: () {
context
.read<LocalAIChatSettingBloc>()
.add(const LocalAIChatSettingEvent.refreshAISetting());
},
),
);
return Padding(
padding: const EdgeInsets.only(top: 8),
child: child,
);
} else {
return const SizedBox.shrink();
}
} else {
return Opacity(
opacity: 0.5,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 6),
child: FlowyText(
error.msg,
maxLines: 10,
),
),
);
}
},
);
}
FlowyError? errorFromState(LocalAIChatSettingState state) {
final err = state.aiModelProgress.when(
loading: () => null,
finish: (err) => err,
init: () {},
);
if (err == null) {
state.selectLLMState.when(
loading: () => null,
finish: (err) => err,
);
}
return err;
}
}
void _showDownloadDialog(
BuildContext context,
LocalModelResourcePB llmResource,
LLMModelPB llmModel,
) {
if (llmResource.pendingResources.isEmpty) {
return;
}
final res = llmResource.pendingResources.first;
String desc = "";
switch (res.resType) {
case PendingResourceTypePB.AIModel:
desc = LocaleKeys.settings_aiPage_keys_downloadLLMPromptDetail.tr(
args: [
llmResource.pendingResources[0].name,
llmResource.pendingResources[0].fileSize,
],
);
break;
case PendingResourceTypePB.OfflineApp:
desc = LocaleKeys.settings_aiPage_keys_downloadAppFlowyOfflineAI.tr();
break;
}
showConfirmDialog(
context: context,
style: ConfirmPopupStyle.cancelAndOk,
title: LocaleKeys.settings_aiPage_keys_downloadLLMPrompt.tr(
args: [res.name],
),
description: desc,
confirmLabel: LocaleKeys.button_confirm.tr(),
onConfirm: () => context.read<LocalAIChatSettingBloc>().add(
LocalAIChatSettingEvent.startDownloadModel(
llmModel,
),
),
onCancel: () => context.read<LocalAIChatSettingBloc>().add(
const LocalAIChatSettingEvent.cancelDownload(),
),
);
}
class _ShowDownloadIndicator extends StatelessWidget {
const _ShowDownloadIndicator({
required this.llmResource,
required this.llmModel,
});
final LocalModelResourcePB llmResource;
final LLMModelPB llmModel;
@override
Widget build(BuildContext context) {
return BlocBuilder<LocalAIChatSettingBloc, LocalAIChatSettingState>(
builder: (context, state) {
return Row(
children: [
const Spacer(),
IntrinsicWidth(
child: SizedBox(
height: 30,
child: FlowyButton(
text: FlowyText(
LocaleKeys.settings_aiPage_keys_downloadAIModelButton.tr(),
fontSize: 14,
color: const Color(0xFF005483),
),
leftIcon: const FlowySvg(
FlowySvgs.local_model_download_s,
color: Color(0xFF005483),
),
onTap: () {
_showDownloadDialog(context, llmResource, llmModel);
},
),
),
),
],
);
},
);
}
}
@@ -3,7 +3,7 @@ import 'package:flutter/material.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/application/settings/ai/local_ai_bloc.dart';
import 'package:appflowy/workspace/application/settings/ai/settings_ai_bloc.dart';
import 'package:appflowy/workspace/presentation/settings/pages/setting_ai_view/local_ai_chat_setting.dart';
import 'package:appflowy/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting_panel.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';
import 'package:easy_localization/easy_localization.dart';
@@ -38,10 +38,11 @@ class _LocalAISettingState extends State<LocalAISetting> {
listener: (context, state) {
final controller =
ExpandableController.of(context, required: true)!;
state.pageIndicator.when(
error: (_) => controller.expanded = false,
ready: (enabled) => controller.expanded = enabled,
loading: () => controller.expanded = false,
error: (_) => controller.expanded = true,
isEnabled: (enabled) => controller.expanded = enabled,
loading: () => controller.expanded = true,
);
},
child: ExpandablePanel(
@@ -68,7 +69,7 @@ class _LocalAISettingState extends State<LocalAISetting> {
child: const Padding(
padding:
EdgeInsets.symmetric(horizontal: 12, vertical: 6),
child: LocalAIChatSetting(),
child: LocalAISettingPanel(),
),
),
],
@@ -95,9 +96,9 @@ class LocalAISettingHeader extends StatelessWidget {
return const SizedBox.shrink();
},
loading: () {
return const CircularProgressIndicator.adaptive();
return const SizedBox.shrink();
},
ready: (isEnabled) {
isEnabled: (isEnabled) {
return Row(
children: [
FlowyText(
@@ -0,0 +1,78 @@
import 'package:appflowy/workspace/application/settings/ai/local_ai_chat_bloc.dart';
import 'package:appflowy/workspace/application/settings/ai/local_ai_chat_toggle_bloc.dart';
import 'package:appflowy/workspace/presentation/settings/pages/setting_ai_view/ollma_setting.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';
import 'plugin_state.dart';
class LocalAISettingPanel extends StatelessWidget {
const LocalAISettingPanel({super.key});
@override
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider(create: (context) => LocalAISettingPanelBloc()),
BlocProvider(
create: (context) => LocalAIChatToggleBloc()
..add(const LocalAIChatToggleEvent.started()),
),
],
child: ExpandableNotifier(
initialExpanded: true,
child: ExpandablePanel(
theme: const ExpandableThemeData(
headerAlignment: ExpandablePanelHeaderAlignment.center,
tapBodyToCollapse: false,
hasIcon: false,
tapBodyToExpand: false,
tapHeaderToExpand: false,
),
header: const SizedBox.shrink(),
collapsed: const SizedBox.shrink(),
expanded: Padding(
padding: const EdgeInsets.symmetric(vertical: 6),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
BlocBuilder<LocalAISettingPanelBloc, LocalAISettingPanelState>(
builder: (context, state) {
// If the progress indicator is startLocalAIApp, then don't show the LLM model.
if (state.progressIndicator ==
const LocalAIProgress.downloadLocalAIApp()) {
return const SizedBox.shrink();
} else {
return Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: const [
OllamaSettingPage(),
VSpace(6),
_LocalAIStateWidget(),
],
);
}
},
),
],
),
),
),
),
);
}
}
class _LocalAIStateWidget extends StatelessWidget {
const _LocalAIStateWidget();
@override
Widget build(BuildContext context) {
return BlocBuilder<LocalAISettingPanelBloc, LocalAISettingPanelState>(
builder: (context, state) {
return const PluginStateIndicator();
},
);
}
}
@@ -0,0 +1,167 @@
import 'package:appflowy/core/helpers/url_launcher.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/application/settings/ai/ollama_setting_bloc.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra_ui/style_widget/button.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_infra_ui/style_widget/text_field.dart';
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class OllamaSettingPage extends StatelessWidget {
const OllamaSettingPage({super.key});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) =>
OllamaSettingBloc()..add(const OllamaSettingEvent.started()),
child: BlocBuilder<OllamaSettingBloc, OllamaSettingState>(
buildWhen: (previous, current) =>
previous.inputItems != current.inputItems ||
previous.isEdited != current.isEdited,
builder: (context, state) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ListView.separated(
shrinkWrap: true,
itemCount: state.inputItems.length,
separatorBuilder: (_, __) => const VSpace(10),
itemBuilder: (context, index) {
final item = state.inputItems[index];
return _SettingItemWidget(item: item);
},
),
const VSpace(6),
Opacity(
opacity: 0.6,
child: _InstallOllamaInstruction(),
),
_SaveButton(isEdited: state.isEdited),
],
);
},
),
);
}
}
class _SettingItemWidget extends StatelessWidget {
const _SettingItemWidget({required this.item});
final SettingItem item;
@override
Widget build(BuildContext context) {
return Column(
key: ValueKey(item.content + item.settingType.title),
crossAxisAlignment: CrossAxisAlignment.start,
children: [
FlowyText(item.settingType.title),
const VSpace(8),
FlowyTextField(
hintText: item.hintText,
text: item.content,
onChanged: (content) {
context.read<OllamaSettingBloc>().add(
OllamaSettingEvent.onEdit(content, item.settingType),
);
},
),
],
);
}
}
class _SaveButton extends StatelessWidget {
const _SaveButton({required this.isEdited});
final bool isEdited;
@override
Widget build(BuildContext context) {
final tooltipMessage = isEdited ? 'Click to apply changes' : 'No changes';
return SizedBox(
height: 50,
child: Row(
children: [
const Spacer(),
SizedBox(
width: 120,
child: FlowyTooltip(
message: tooltipMessage,
child: Opacity(
opacity: isEdited ? 1 : 0.5,
child: FlowyTextButton(
'Apply',
mainAxisAlignment: MainAxisAlignment.center,
onPressed: isEdited
? () {
context
.read<OllamaSettingBloc>()
.add(const OllamaSettingEvent.submit());
}
: null,
),
),
),
),
],
),
);
}
}
class _InstallOllamaInstruction extends StatelessWidget {
const _InstallOllamaInstruction();
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
RichText(
maxLines: 3,
textAlign: TextAlign.left,
text: TextSpan(
children: <TextSpan>[
TextSpan(
text:
"${LocaleKeys.settings_aiPage_keys_localAISetupInstruction1.tr()} ",
style: Theme.of(context)
.textTheme
.bodySmall!
.copyWith(height: 1.5),
),
TextSpan(
text:
" ${LocaleKeys.settings_aiPage_keys_localAISetupInstruction2.tr()} ",
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
fontSize: FontSizes.s14,
color: Theme.of(context).colorScheme.primary,
height: 1.5,
),
recognizer: TapGestureRecognizer()
..onTap = () => afLaunchUrlString(
"https://docs.appflowy.io/docs/appflowy/product/appflowy-ai-ollama",
),
),
TextSpan(
text:
" ${LocaleKeys.settings_aiPage_keys_localAISetupInstruction3.tr()} ",
style: Theme.of(context)
.textTheme
.bodySmall!
.copyWith(height: 1.5),
),
],
),
),
],
);
}
}
@@ -3,6 +3,7 @@ import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/application/settings/ai/download_offline_ai_app_bloc.dart';
import 'package:appflowy/workspace/application/settings/ai/plugin_state_bloc.dart';
import 'package:appflowy/workspace/presentation/settings/pages/setting_ai_view/init_local_ai.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra_ui/style_widget/button.dart';
@@ -23,17 +24,12 @@ class PluginStateIndicator extends StatelessWidget {
child: BlocBuilder<PluginStateBloc, PluginStateState>(
builder: (context, state) {
return state.action.when(
init: () => const _InitPlugin(),
ready: () => const _LocalAIReadyToUse(),
restartPlugin: () => const _ReloadButton(),
loadingPlugin: () => const _InitPlugin(),
startAIOfflineApp: () => OpenOrDownloadOfflineAIApp(
onRetry: () {
context
.read<PluginStateBloc>()
.add(const PluginStateEvent.started());
},
),
unknown: () => const SizedBox.shrink(),
readToRun: () => const SizedBox.shrink(),
initializingPlugin: () => const InitLocalAIIndicator(),
running: () => const _LocalAIRunning(),
restartPlugin: () => const _RestartPluginButton(),
lackOfResource: (desc) => _LackOfResource(desc: desc),
);
},
),
@@ -41,26 +37,8 @@ class PluginStateIndicator extends StatelessWidget {
}
}
class _InitPlugin extends StatelessWidget {
const _InitPlugin();
@override
Widget build(BuildContext context) {
return Row(
children: [
FlowyText(LocaleKeys.settings_aiPage_keys_localAIStart.tr()),
const Spacer(),
const SizedBox(
height: 20,
child: CircularProgressIndicator.adaptive(),
),
],
);
}
}
class _ReloadButton extends StatelessWidget {
const _ReloadButton();
class _RestartPluginButton extends StatelessWidget {
const _RestartPluginButton();
@override
Widget build(BuildContext context) {
@@ -91,8 +69,8 @@ class _ReloadButton extends StatelessWidget {
}
}
class _LocalAIReadyToUse extends StatelessWidget {
const _LocalAIReadyToUse();
class _LocalAIRunning extends StatelessWidget {
const _LocalAIRunning();
@override
Widget build(BuildContext context) {
@@ -119,7 +97,7 @@ class _LocalAIReadyToUse extends StatelessWidget {
const HSpace(6),
Flexible(
child: FlowyText(
LocaleKeys.settings_aiPage_keys_localAILoaded.tr(),
LocaleKeys.settings_aiPage_keys_localAIRunning.tr(),
fontSize: 11,
color: const Color(0xFF1E4620),
maxLines: 3,
@@ -128,22 +106,6 @@ class _LocalAIReadyToUse extends StatelessWidget {
],
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 6),
child: FlowyButton(
useIntrinsicWidth: true,
text: FlowyText(
LocaleKeys.settings_aiPage_keys_openModelDirectory.tr(),
fontSize: 11,
color: const Color(0xFF1E4620),
),
onTap: () {
context.read<PluginStateBloc>().add(
const PluginStateEvent.openModelDirectory(),
);
},
),
),
],
),
),
@@ -232,20 +194,6 @@ class OpenOrDownloadOfflineAIApp extends StatelessWidget {
],
),
),
// const SizedBox(
// height: 6,
// ), // Replaced VSpace with SizedBox for simplicity
// SizedBox(
// height: 30,
// child: FlowyButton(
// useIntrinsicWidth: true,
// margin: const EdgeInsets.symmetric(horizontal: 12),
// text: FlowyText(
// LocaleKeys.settings_aiPage_keys_activeOfflineAI.tr(),
// ),
// onTap: onRetry,
// ),
// ),
],
);
},
@@ -253,3 +201,30 @@ class OpenOrDownloadOfflineAIApp extends StatelessWidget {
);
}
}
class _LackOfResource extends StatelessWidget {
const _LackOfResource({required this.desc});
final String desc;
@override
Widget build(BuildContext context) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
FlowySvg(
FlowySvgs.toast_warning_filled_s,
size: const Size.square(20.0),
blendMode: null,
),
const HSpace(6),
Expanded(
child: FlowyText(
desc,
maxLines: 3,
),
),
],
);
}
}
@@ -1,6 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flutter/material.dart';
class ToggleStyle {
const ToggleStyle({
@@ -38,6 +37,7 @@ class Toggle extends StatelessWidget {
this.thumbColor,
this.activeBackgroundColor,
this.inactiveBackgroundColor,
this.duration = const Duration(milliseconds: 150),
this.padding = const EdgeInsets.all(8.0),
});
@@ -48,6 +48,7 @@ class Toggle extends StatelessWidget {
final Color? activeBackgroundColor;
final Color? inactiveBackgroundColor;
final EdgeInsets padding;
final Duration duration;
@override
Widget build(BuildContext context) {
@@ -70,7 +71,7 @@ class Toggle extends StatelessWidget {
),
),
AnimatedPositioned(
duration: const Duration(milliseconds: 150),
duration: duration,
top: (style.height - style.thumbRadius) / 2,
left: value ? style.width - style.thumbRadius - 1 : 1,
child: Container(
@@ -400,7 +400,7 @@ class LockedPageStatus extends StatelessWidget {
side: BorderSide(color: color),
borderRadius: BorderRadius.circular(6),
),
color: Colors.white.withValues(alpha: 0.75),
color: context.lockedPageButtonBackground,
),
child: FlowyButton(
useIntrinsicWidth: true,
@@ -440,7 +440,7 @@ class ReLockedPageStatus extends StatelessWidget {
side: BorderSide(color: iconColor),
borderRadius: BorderRadius.circular(6),
),
color: Colors.white.withValues(alpha: 0.75),
color: context.lockedPageButtonBackground,
),
child: FlowyButton(
useIntrinsicWidth: true,
@@ -465,3 +465,12 @@ class ReLockedPageStatus extends StatelessWidget {
);
}
}
extension on BuildContext {
Color get lockedPageButtonBackground {
if (Theme.of(this).brightness == Brightness.light) {
return Colors.white.withValues(alpha: 0.75);
}
return Color(0xB21B1A22);
}
}
@@ -1,7 +1,7 @@
cmake_minimum_required(VERSION 3.10)
project(runner LANGUAGES CXX)
set(BINARY_NAME "appflowy")
set(BINARY_NAME "AppFlowy")
set(APPLICATION_ID "io.appflowy.appflowy")
cmake_policy(SET CMP0063 NEW)
@@ -1,22 +0,0 @@
display_name: AppFlowy
icon: linux/packaging/assets/logo.png
keywords:
- AppFlowy
- Office
- Document
- Database
- Note
- Kanban
- Note
generic_name: AppFlowy
categories:
- Office
startup_notify: true
supported_mime_type:
- x-scheme-handler/appflowy-flutter
@@ -29,8 +29,7 @@ essential: false
section: x11
priority: optional
supported_mime_type:
- x-scheme-handler/appflowy-flutter
supportedMimeType: x-scheme-handler/appflowy-flutter
dependencies:
- libnotify-bin
@@ -26,8 +26,7 @@ categories:
startup_notify: true
supported_mime_type:
- x-scheme-handler/appflowy-flutter
supportedMimeType: x-scheme-handler/appflowy-flutter
requires:
- libnotify
+23 -23
View File
@@ -144,34 +144,34 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos
SPEC CHECKSUMS:
app_links: 9028728e32c83a0831d9db8cf91c526d16cc5468
appflowy_backend: 464aeb3e5c6966a41641a2111e5ead72ce2695f7
auto_updater_macos: 3a42f1a06be6981f1a18be37e6e7bf86aa732118
bitsdojo_window_macos: 7959fb0ca65a3ccda30095c181ecb856fae48ea9
connectivity_plus: e74b9f74717d2d99d45751750e266e55912baeb5
desktop_drop: e0b672a7d84c0a6cbc378595e82cdb15f2970a43
device_info_plus: a56e6e74dbbd2bb92f2da12c64ddd4f67a749041
file_selector_macos: 6280b52b459ae6c590af5d78fc35c7267a3c4b31
flowy_infra_ui: 8760ff42a789de40bf5007a5f176b454722a341e
app_links: 10e0a0ab602ffaf34d142cd4862f29d34b303b2a
appflowy_backend: 865496343de667fc8c600e04b9fd05234e130cf9
auto_updater_macos: 3e3462c418fe4e731917eacd8d28eef7af84086d
bitsdojo_window_macos: 44e3b8fe3dd463820e0321f6256c5b1c16bb6a00
connectivity_plus: 18d3c32514c886e046de60e9c13895109866c747
desktop_drop: 69eeff437544aa619c8db7f4481b3a65f7696898
device_info_plus: ce1b7762849d3ec103d0e0517299f2db7ad60720
file_selector_macos: cc3858c981fe6889f364731200d6232dac1d812d
flowy_infra_ui: 03301a39ad118771adbf051a664265c61c507f38
FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
HotKey: 400beb7caa29054ea8d864c96f5ba7e5b4852277
hotkey_manager: b443f35f4d772162937aa73fd8995e579f8ac4e2
irondash_engine_context: 893c7d96d20ce361d7e996f39d360c4c2f9869ba
local_notifier: ebf072651e35ae5e47280ad52e2707375cb2ae4e
package_info_plus: f0052d280d17aa382b932f399edf32507174e870
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
hotkey_manager: c32bf0bfe8f934b7bc17ab4ad5c4c142960b023c
irondash_engine_context: da62996ee25616d2f01bbeb85dc115d813359478
local_notifier: e9506bc66fc70311e8bc7291fb70f743c081e4ff
package_info_plus: 12f1c5c2cfe8727ca46cbd0b26677728972d9a5b
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
ReachabilitySwift: 32793e867593cfc1177f5d16491e3a197d2fccda
screen_retriever_macos: 452e51764a9e1cdb74b3c541238795849f21557f
screen_retriever_macos: 776e0fa5d42c6163d2bf772d22478df4b302b161
Sentry: 1fe34e9c2cbba1e347623610d26db121dcb569f1
sentry_flutter: e24b397f9a61fa5bbefd8279c3b2242ca86faa90
share_plus: 510bf0af1a42cd602274b4629920c9649c52f4cc
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
sentry_flutter: a39c2a2d67d5e5b9cb0b94a4985c76dd5b3fc737
share_plus: 1fa619de8392a4398bfaf176d441853922614e89
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
Sparkle: 5f8960a7a119aa7d45dacc0d5837017170bc5675
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
super_native_extensions: c2795d6d9aedf4a79fae25cb6160b71b50549189
url_launcher_macos: 0fba8ddabfc33ce0a9afe7c5fef5aab3d8d2d673
webview_flutter_wkwebview: 44d4dee7d7056d5ad185d25b38404436d56c547c
window_manager: 1d01fa7ac65a6e6f83b965471b1a7fdd3f06166c
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
super_native_extensions: 85efee3a7495b46b04befcfc86ed12069264ebf3
url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404
webview_flutter_wkwebview: 0982481e3d9c78fd5c6f62a002fcd24fc791f1e4
window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8
PODFILE CHECKSUM: 0532f3f001ca3110b8be345d6491fff690e95823
Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

@@ -1,26 +0,0 @@
{
"title": "AppFlowy",
"icon-size": 94,
"window": {
"size": {
"width": 600,
"height": 450
}
},
"background": "../../macos/packaging/assets/background.jpg",
"contents": [
{
"x": 141,
"y": 249,
"type": "file",
"path": "AppFlowy.app",
"name": "AppFlowy.app"
},
{
"x": 458,
"y": 249,
"type": "link",
"path": "/Applications"
}
]
}
@@ -1,17 +0,0 @@
title: "AppFlowy"
icon-size: 94
window:
size:
width: 600
height: 450
background: "../../../macos/packaging/assets/background.jpg"
contents:
- x: 141
y: 249
type: "file"
path: "AppFlowy.app"
name: "AppFlowy.app"
- x: 458
y: 249
type: "link"
path: "/Applications"
+4 -4
View File
@@ -90,8 +90,8 @@ packages:
dependency: "direct main"
description:
path: "."
ref: b2672f2
resolved-ref: b2672f297b0e9e7a028ea217ae6a57c8dca3da4e
ref: "5841b4c"
resolved-ref: "5841b4c1bab170da66c52c60977690f2817b8fc4"
url: "https://github.com/AppFlowy-IO/appflowy-editor.git"
source: git
version: "5.1.0"
@@ -1583,10 +1583,10 @@ packages:
dependency: transitive
description:
name: pdf
sha256: adbdec5bc84d20e6c8d67f9c64270aa64d1e9e1ed529f0fef7e7bc7e9400f894
sha256: "28eacad99bffcce2e05bba24e50153890ad0255294f4dd78a17075a2ba5c8416"
url: "https://pub.dev"
source: hosted
version: "3.11.2"
version: "3.11.3"
percent_indicator:
dependency: "direct main"
description:
+2 -2
View File
@@ -4,7 +4,7 @@ description: Bring projects, wikis, and teams together with AI. AppFlowy is an
your data. The best open source alternative to Notion.
publish_to: "none"
version: 0.8.5
version: 0.8.6
environment:
flutter: ">=3.27.4"
@@ -180,7 +180,7 @@ dependency_overrides:
appflowy_editor:
git:
url: https://github.com/AppFlowy-IO/appflowy-editor.git
ref: "b2672f2"
ref: "5841b4c"
appflowy_editor_plugins:
git:
@@ -33,6 +33,12 @@ void main() {
final node = editorState.document.root.children[0];
expect(node.delta!.toPlainText(), '⇒');
// use undo to revert the change
undoCommand.execute(editorState);
expect(editorState.document.root.children.length, 1);
final nodeAfterUndo = editorState.document.root.children[0];
expect(nodeAfterUndo.delta!.toPlainText(), '=>');
editorState.dispose();
});
@@ -56,6 +62,41 @@ void main() {
final node = editorState.document.root.children[0];
expect(node.delta!.toPlainText(), '→');
// use undo to revert the change
undoCommand.execute(editorState);
expect(editorState.document.root.children.length, 1);
final nodeAfterUndo = editorState.document.root.children[0];
expect(nodeAfterUndo.delta!.toPlainText(), '->');
editorState.dispose();
});
test('turn -- into —', () async {
final document = Document.blank()
..insert([
0,
], [
paragraphNode(text: '-'),
]);
final editorState = EditorState(document: document);
editorState.selection = Selection.collapsed(
Position(path: [0], offset: 1),
);
final result = await customFormatDoubleHyphenEmDash.execute(editorState);
expect(result, true);
expect(editorState.document.root.children.length, 1);
final node = editorState.document.root.children[0];
expect(node.delta!.toPlainText(), '—');
// use undo to revert the change
undoCommand.execute(editorState);
expect(editorState.document.root.children.length, 1);
final nodeAfterUndo = editorState.document.root.children[0];
expect(nodeAfterUndo.delta!.toPlainText(), '--');
editorState.dispose();
});
});
@@ -1,7 +1,8 @@
import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_option_cubit.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor/appflowy_editor.dart'
hide quoteNode, QuoteBlockKeys;
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
+7 -1
View File
@@ -850,6 +850,8 @@
"localAIStart": "Local AI Chat is starting...",
"localAILoading": "Local AI Chat Model is loading...",
"localAIStopped": "Local AI stopped",
"localAIRunning": "Local AI is running",
"localAIInitializing": "Local AI is initializing and may take a few minutes, depending on your device",
"failToLoadLocalAI": "Failed to start local AI",
"restartLocalAI": "Restart Local AI",
"disableLocalAITitle": "Disable local AI",
@@ -863,7 +865,11 @@
"offlineAIDownload3": "it first",
"activeOfflineAI": "Active",
"downloadOfflineAI": "Download",
"openModelDirectory": "Open folder"
"openModelDirectory": "Open folder",
"localAISetupInstruction1": "Follow these",
"localAISetupInstruction2": "instructions",
"localAISetupInstruction3": "to set up Ollama and AppFlowy Local AI. Skip if you've already set it up",
"startLocalAI": "It may take a few seconds to start the local AI"
}
},
"planPage": {
+131 -69
View File
@@ -163,7 +163,7 @@ checksum = "c1fd03a028ef38ba2276dce7e33fcd6369c158a1bca17946c4b1b701891c1ff7"
[[package]]
name = "app-error"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=80eea34e73cfffe71e484cbaddb2d547076eaf09#80eea34e73cfffe71e484cbaddb2d547076eaf09"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=eb92783#eb927831cd1d6e9091c9aee2a5f7413d548b2186"
dependencies = [
"anyhow",
"bincode",
@@ -183,7 +183,7 @@ dependencies = [
[[package]]
name = "appflowy-ai-client"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=80eea34e73cfffe71e484cbaddb2d547076eaf09#80eea34e73cfffe71e484cbaddb2d547076eaf09"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=eb92783#eb927831cd1d6e9091c9aee2a5f7413d548b2186"
dependencies = [
"anyhow",
"bytes",
@@ -198,11 +198,12 @@ dependencies = [
[[package]]
name = "appflowy-local-ai"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=6f064efe232268f8d396edbb4b84d57fbb640f13#6f064efe232268f8d396edbb4b84d57fbb640f13"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=87c47edf42e0bec0aecd8ab94de59a1b4be19f12#87c47edf42e0bec0aecd8ab94de59a1b4be19f12"
dependencies = [
"anyhow",
"appflowy-plugin",
"bytes",
"futures",
"reqwest 0.11.27",
"serde",
"serde_json",
@@ -217,7 +218,7 @@ dependencies = [
[[package]]
name = "appflowy-plugin"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=6f064efe232268f8d396edbb4b84d57fbb640f13#6f064efe232268f8d396edbb4b84d57fbb640f13"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=87c47edf42e0bec0aecd8ab94de59a1b4be19f12#87c47edf42e0bec0aecd8ab94de59a1b4be19f12"
dependencies = [
"anyhow",
"cfg-if",
@@ -231,6 +232,7 @@ dependencies = [
"tokio",
"tokio-stream",
"tracing",
"winreg 0.55.0",
"xattr",
]
@@ -786,7 +788,7 @@ dependencies = [
[[package]]
name = "client-api"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=80eea34e73cfffe71e484cbaddb2d547076eaf09#80eea34e73cfffe71e484cbaddb2d547076eaf09"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=eb92783#eb927831cd1d6e9091c9aee2a5f7413d548b2186"
dependencies = [
"again",
"anyhow",
@@ -794,7 +796,6 @@ dependencies = [
"arc-swap",
"async-trait",
"base64 0.22.1",
"bincode",
"brotli",
"bytes",
"chrono",
@@ -823,7 +824,6 @@ dependencies = [
"semver",
"serde",
"serde_json",
"serde_repr",
"serde_urlencoded",
"shared-entity",
"thiserror 1.0.64",
@@ -843,7 +843,7 @@ dependencies = [
[[package]]
name = "client-api-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=80eea34e73cfffe71e484cbaddb2d547076eaf09#80eea34e73cfffe71e484cbaddb2d547076eaf09"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=eb92783#eb927831cd1d6e9091c9aee2a5f7413d548b2186"
dependencies = [
"collab-entity",
"collab-rt-entity",
@@ -856,7 +856,7 @@ dependencies = [
[[package]]
name = "client-websocket"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=80eea34e73cfffe71e484cbaddb2d547076eaf09#80eea34e73cfffe71e484cbaddb2d547076eaf09"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=eb92783#eb927831cd1d6e9091c9aee2a5f7413d548b2186"
dependencies = [
"futures-channel",
"futures-util",
@@ -872,11 +872,12 @@ dependencies = [
[[package]]
name = "cmd_lib"
version = "1.3.0"
version = "1.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ba0f413777386d37f85afa5242f277a7b461905254c1af3c339d4af06800f62"
checksum = "371c15a3c178d0117091bd84414545309ca979555b1aad573ef591ad58818d41"
dependencies = [
"cmd_lib_macros",
"env_logger 0.10.2",
"faccess",
"lazy_static",
"log",
@@ -885,20 +886,20 @@ dependencies = [
[[package]]
name = "cmd_lib_macros"
version = "1.3.0"
version = "1.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e66605092ff6c6e37e0246601ae6c3f62dc1880e0599359b5f303497c112dc0"
checksum = "cb844bd05be34d91eb67101329aeba9d3337094c04fd8507d821db7ebb488eaf"
dependencies = [
"proc-macro-error",
"proc-macro-error2",
"proc-macro2",
"quote",
"syn 1.0.109",
"syn 2.0.94",
]
[[package]]
name = "collab"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=df936abcf8b5747c5d59efac5a0f733357060a1d#df936abcf8b5747c5d59efac5a0f733357060a1d"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=45239d2#45239d2ae871cc355ea2cc1d5d578e21c8263242"
dependencies = [
"anyhow",
"arc-swap",
@@ -923,7 +924,7 @@ dependencies = [
[[package]]
name = "collab-database"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=df936abcf8b5747c5d59efac5a0f733357060a1d#df936abcf8b5747c5d59efac5a0f733357060a1d"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=45239d2#45239d2ae871cc355ea2cc1d5d578e21c8263242"
dependencies = [
"anyhow",
"async-trait",
@@ -963,7 +964,7 @@ dependencies = [
[[package]]
name = "collab-document"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=df936abcf8b5747c5d59efac5a0f733357060a1d#df936abcf8b5747c5d59efac5a0f733357060a1d"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=45239d2#45239d2ae871cc355ea2cc1d5d578e21c8263242"
dependencies = [
"anyhow",
"arc-swap",
@@ -984,7 +985,7 @@ dependencies = [
[[package]]
name = "collab-entity"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=df936abcf8b5747c5d59efac5a0f733357060a1d#df936abcf8b5747c5d59efac5a0f733357060a1d"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=45239d2#45239d2ae871cc355ea2cc1d5d578e21c8263242"
dependencies = [
"anyhow",
"bytes",
@@ -1004,7 +1005,7 @@ dependencies = [
[[package]]
name = "collab-folder"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=df936abcf8b5747c5d59efac5a0f733357060a1d#df936abcf8b5747c5d59efac5a0f733357060a1d"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=45239d2#45239d2ae871cc355ea2cc1d5d578e21c8263242"
dependencies = [
"anyhow",
"arc-swap",
@@ -1026,7 +1027,7 @@ dependencies = [
[[package]]
name = "collab-importer"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=df936abcf8b5747c5d59efac5a0f733357060a1d#df936abcf8b5747c5d59efac5a0f733357060a1d"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=45239d2#45239d2ae871cc355ea2cc1d5d578e21c8263242"
dependencies = [
"anyhow",
"async-recursion",
@@ -1090,7 +1091,7 @@ dependencies = [
[[package]]
name = "collab-plugins"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=df936abcf8b5747c5d59efac5a0f733357060a1d#df936abcf8b5747c5d59efac5a0f733357060a1d"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=45239d2#45239d2ae871cc355ea2cc1d5d578e21c8263242"
dependencies = [
"anyhow",
"async-stream",
@@ -1128,13 +1129,12 @@ dependencies = [
[[package]]
name = "collab-rt-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=80eea34e73cfffe71e484cbaddb2d547076eaf09#80eea34e73cfffe71e484cbaddb2d547076eaf09"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=eb92783#eb927831cd1d6e9091c9aee2a5f7413d548b2186"
dependencies = [
"anyhow",
"bincode",
"bytes",
"chrono",
"client-websocket",
"collab",
"collab-entity",
"collab-rt-protocol",
@@ -1143,9 +1143,7 @@ dependencies = [
"prost-build",
"protoc-bin-vendored",
"serde",
"serde_json",
"serde_repr",
"thiserror 1.0.64",
"tokio-tungstenite",
"yrs",
]
@@ -1153,7 +1151,7 @@ dependencies = [
[[package]]
name = "collab-rt-protocol"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=80eea34e73cfffe71e484cbaddb2d547076eaf09#80eea34e73cfffe71e484cbaddb2d547076eaf09"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=eb92783#eb927831cd1d6e9091c9aee2a5f7413d548b2186"
dependencies = [
"anyhow",
"async-trait",
@@ -1170,7 +1168,7 @@ dependencies = [
[[package]]
name = "collab-user"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=df936abcf8b5747c5d59efac5a0f733357060a1d#df936abcf8b5747c5d59efac5a0f733357060a1d"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=45239d2#45239d2ae871cc355ea2cc1d5d578e21c8263242"
dependencies = [
"anyhow",
"collab",
@@ -1400,7 +1398,7 @@ dependencies = [
"cssparser-macros",
"dtoa-short",
"itoa",
"phf 0.8.0",
"phf 0.11.2",
"smallvec",
]
@@ -1548,11 +1546,8 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308"
[[package]]
name = "database-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=80eea34e73cfffe71e484cbaddb2d547076eaf09#80eea34e73cfffe71e484cbaddb2d547076eaf09"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=eb92783#eb927831cd1d6e9091c9aee2a5f7413d548b2186"
dependencies = [
"anyhow",
"app-error",
"appflowy-ai-client",
"bincode",
"bytes",
"chrono",
@@ -1798,6 +1793,19 @@ dependencies = [
"regex",
]
[[package]]
name = "env_logger"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580"
dependencies = [
"humantime",
"is-terminal",
"log",
"regex",
"termcolor",
]
[[package]]
name = "equivalent"
version = "1.0.1"
@@ -2050,6 +2058,7 @@ dependencies = [
"tracing-subscriber",
"uuid",
"validator 0.18.1",
"winreg 0.55.0",
"zip 2.2.0",
"zip-extensions",
]
@@ -2733,9 +2742,9 @@ dependencies = [
[[package]]
name = "futures"
version = "0.3.30"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0"
checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
dependencies = [
"futures-channel",
"futures-core",
@@ -2748,9 +2757,9 @@ dependencies = [
[[package]]
name = "futures-channel"
version = "0.3.30"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78"
checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
dependencies = [
"futures-core",
"futures-sink",
@@ -2758,15 +2767,15 @@ dependencies = [
[[package]]
name = "futures-core"
version = "0.3.30"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d"
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
[[package]]
name = "futures-executor"
version = "0.3.30"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d"
checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
dependencies = [
"futures-core",
"futures-task",
@@ -2775,9 +2784,9 @@ dependencies = [
[[package]]
name = "futures-io"
version = "0.3.30"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1"
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
[[package]]
name = "futures-lite"
@@ -2794,9 +2803,9 @@ dependencies = [
[[package]]
name = "futures-macro"
version = "0.3.30"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
dependencies = [
"proc-macro2",
"quote",
@@ -2805,21 +2814,21 @@ dependencies = [
[[package]]
name = "futures-sink"
version = "0.3.30"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5"
checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
[[package]]
name = "futures-task"
version = "0.3.30"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004"
checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
[[package]]
name = "futures-util"
version = "0.3.30"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48"
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
dependencies = [
"futures-channel",
"futures-core",
@@ -2970,28 +2979,24 @@ dependencies = [
[[package]]
name = "gotrue"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=80eea34e73cfffe71e484cbaddb2d547076eaf09#80eea34e73cfffe71e484cbaddb2d547076eaf09"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=eb92783#eb927831cd1d6e9091c9aee2a5f7413d548b2186"
dependencies = [
"anyhow",
"futures-util",
"getrandom 0.2.10",
"gotrue-entity",
"infra",
"reqwest 0.12.9",
"serde",
"serde_json",
"tokio",
"tracing",
]
[[package]]
name = "gotrue-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=80eea34e73cfffe71e484cbaddb2d547076eaf09#80eea34e73cfffe71e484cbaddb2d547076eaf09"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=eb92783#eb927831cd1d6e9091c9aee2a5f7413d548b2186"
dependencies = [
"anyhow",
"app-error",
"chrono",
"jsonwebtoken",
"lazy_static",
"serde",
@@ -3089,6 +3094,12 @@ version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b"
[[package]]
name = "hermit-abi"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fbd780fe5cc30f81464441920d82ac8740e2e46b29a6fad543ddd075229ce37e"
[[package]]
name = "hex"
version = "0.4.3"
@@ -3598,7 +3609,7 @@ dependencies = [
[[package]]
name = "infra"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=80eea34e73cfffe71e484cbaddb2d547076eaf09#80eea34e73cfffe71e484cbaddb2d547076eaf09"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=eb92783#eb927831cd1d6e9091c9aee2a5f7413d548b2186"
dependencies = [
"anyhow",
"bytes",
@@ -3659,6 +3670,17 @@ version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28b29a3cd74f0f4598934efe3aeba42bae0eb4680554128851ebbecb02af14e6"
[[package]]
name = "is-terminal"
version = "0.4.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9"
dependencies = [
"hermit-abi 0.5.0",
"libc",
"windows-sys 0.59.0",
]
[[package]]
name = "itertools"
version = "0.10.5"
@@ -4339,7 +4361,7 @@ version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43"
dependencies = [
"hermit-abi",
"hermit-abi 0.3.2",
"libc",
]
@@ -4429,12 +4451,12 @@ dependencies = [
[[package]]
name = "os_pipe"
version = "0.9.2"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb233f06c2307e1f5ce2ecad9f8121cffbbee2c95428f44ea85222e460d0d213"
checksum = "5ffd2b0a5634335b135d5728d84c5e0fd726954b87111f7506a61c502280d982"
dependencies = [
"libc",
"winapi",
"windows-sys 0.59.0",
]
[[package]]
@@ -4624,7 +4646,7 @@ version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12"
dependencies = [
"phf_macros",
"phf_macros 0.8.0",
"phf_shared 0.8.0",
"proc-macro-hack",
]
@@ -4644,6 +4666,7 @@ version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc"
dependencies = [
"phf_macros 0.11.3",
"phf_shared 0.11.2",
]
@@ -4711,6 +4734,19 @@ dependencies = [
"syn 1.0.109",
]
[[package]]
name = "phf_macros"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216"
dependencies = [
"phf_generator 0.11.2",
"phf_shared 0.11.2",
"proc-macro2",
"quote",
"syn 2.0.94",
]
[[package]]
name = "phf_shared"
version = "0.8.0"
@@ -5152,7 +5188,7 @@ version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "588f6378e4dd99458b60ec275b4477add41ce4fa9f64dcba6f15adccb19b50d6"
dependencies = [
"env_logger",
"env_logger 0.8.4",
"log",
"rand 0.8.5",
]
@@ -5217,7 +5253,7 @@ dependencies = [
"once_cell",
"socket2 0.5.5",
"tracing",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -5559,7 +5595,7 @@ dependencies = [
"wasm-streams",
"web-sys",
"webpki-roots 0.25.2",
"winreg",
"winreg 0.50.0",
]
[[package]]
@@ -6140,7 +6176,7 @@ dependencies = [
[[package]]
name = "shared-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=80eea34e73cfffe71e484cbaddb2d547076eaf09#80eea34e73cfffe71e484cbaddb2d547076eaf09"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=eb92783#eb927831cd1d6e9091c9aee2a5f7413d548b2186"
dependencies = [
"anyhow",
"app-error",
@@ -6152,14 +6188,12 @@ dependencies = [
"futures",
"gotrue-entity",
"infra",
"log",
"pin-project",
"reqwest 0.12.9",
"serde",
"serde_json",
"serde_repr",
"thiserror 1.0.64",
"tracing",
"uuid",
"validator 0.19.0",
]
@@ -6734,6 +6768,15 @@ dependencies = [
"unic-segment",
]
[[package]]
name = "termcolor"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755"
dependencies = [
"winapi-util",
]
[[package]]
name = "terminal_size"
version = "0.1.17"
@@ -7861,6 +7904,15 @@ dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-sys"
version = "0.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-targets"
version = "0.48.5"
@@ -8001,6 +8053,16 @@ dependencies = [
"windows-sys 0.48.0",
]
[[package]]
name = "winreg"
version = "0.55.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97"
dependencies = [
"cfg-if",
"windows-sys 0.59.0",
]
[[package]]
name = "write16"
version = "1.0.0"
+13 -13
View File
@@ -79,7 +79,7 @@ diesel = { version = "2.1.0", features = [
] }
uuid = { version = "1.5.0", features = ["serde", "v4", "v5"] }
serde_repr = "0.1"
futures = "0.3.29"
futures = "0.3.31"
tokio = "1.38.0"
tokio-stream = "0.1.14"
async-trait = "0.1.81"
@@ -103,8 +103,8 @@ dashmap = "6.0.1"
# Run the script.add_workspace_members:
# scripts/tool/update_client_api_rev.sh new_rev_id
# ⚠️⚠️⚠️️
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "80eea34e73cfffe71e484cbaddb2d547076eaf09" }
client-api-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "80eea34e73cfffe71e484cbaddb2d547076eaf09" }
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "eb92783" }
client-api-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "eb92783" }
[profile.dev]
opt-level = 0
@@ -139,18 +139,18 @@ rocksdb = { git = "https://github.com/rust-rocksdb/rust-rocksdb", rev = "1710120
# To switch to the local path, run:
# scripts/tool/update_collab_source.sh
# ⚠️⚠️⚠️️
collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "df936abcf8b5747c5d59efac5a0f733357060a1d" }
collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "df936abcf8b5747c5d59efac5a0f733357060a1d" }
collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "df936abcf8b5747c5d59efac5a0f733357060a1d" }
collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "df936abcf8b5747c5d59efac5a0f733357060a1d" }
collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "df936abcf8b5747c5d59efac5a0f733357060a1d" }
collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "df936abcf8b5747c5d59efac5a0f733357060a1d" }
collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "df936abcf8b5747c5d59efac5a0f733357060a1d" }
collab-importer = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "df936abcf8b5747c5d59efac5a0f733357060a1d" }
collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "45239d2" }
collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "45239d2" }
collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "45239d2" }
collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "45239d2" }
collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "45239d2" }
collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "45239d2" }
collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "45239d2" }
collab-importer = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "45239d2" }
# Working directory: frontend
# To update the commit ID, run:
# scripts/tool/update_local_ai_rev.sh new_rev_id
# ⚠️⚠️⚠️️
appflowy-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "6f064efe232268f8d396edbb4b84d57fbb640f13" }
appflowy-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "6f064efe232268f8d396edbb4b84d57fbb640f13" }
appflowy-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "87c47edf42e0bec0aecd8ab94de59a1b4be19f12" }
appflowy-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "87c47edf42e0bec0aecd8ab94de59a1b4be19f12" }
@@ -12,7 +12,7 @@ serde_json.workspace = true
flowy-ast.workspace = true
quote = "1.0"
cmd_lib = { version = "1.3.0", optional = true }
cmd_lib = { version = "1.9.5", optional = true }
protoc-rust = { version = "2.28.0", optional = true }
#protobuf-codegen = { version = "3.7.1" }
walkdir = { version = "2", optional = true }
@@ -22,7 +22,7 @@ tracing.workspace = true
async-trait.workspace = true
tokio = { workspace = true, features = ["sync"] }
lib-infra = { workspace = true }
futures = "0.3"
futures = "0.3.31"
arc-swap = "1.7"
flowy-sqlite = { workspace = true }
diesel.workspace = true
Loaded 100 of 144 files, more files were not shown because too many files have changed in this diff. Show more