Compare commits
44
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
58e04291c7 | ||
|
|
57a5b38509 | ||
|
|
83c53188e3 | ||
|
|
6ba7f93f69 | ||
|
|
702a486cce | ||
|
|
eb0ed1ad86 | ||
|
|
e264b3a5b8 | ||
|
|
667d15c627 | ||
|
|
bd06e1d559 | ||
|
|
459aca5291 | ||
|
|
e7cd90b6ab | ||
|
|
940db70447 | ||
|
|
59139ff323 | ||
|
|
22fed1bfbc | ||
|
|
5e593bd36e | ||
|
|
ba1dfc6de4 | ||
|
|
c81f87dcdc | ||
|
|
a8b55ca3f0 | ||
|
|
0cefaf633c | ||
|
|
ba4aebd005 | ||
|
|
7b32a92290 | ||
|
|
41b99209f1 | ||
|
|
c1612fe298 | ||
|
|
e69a09d332 | ||
|
|
4e0d9fdb0b | ||
|
|
8b2e769fca | ||
|
|
d29a90a472 | ||
|
|
2e4beb0652 | ||
|
|
addb041816 | ||
|
|
4ff71b5dce | ||
|
|
a0ae62d6f5 | ||
|
|
ea18aa7551 | ||
|
|
68e7069e92 | ||
|
|
556d929b67 | ||
|
|
7f3469a0f2 | ||
|
|
a062c4aadb | ||
|
|
3d3f81ad52 | ||
|
|
fc0fb0b3d3 | ||
|
|
884586f0af | ||
|
|
f8c18afbcf | ||
|
|
8046177d84 | ||
|
|
8d8fc91391 | ||
|
|
796fda159e | ||
|
|
4b2389dafd |
No files matched your search
+109
-121
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
+1
-1
@@ -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 {
|
||||
|
||||
+29
-27
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+9
@@ -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,
|
||||
|
||||
+9
@@ -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,
|
||||
|
||||
+6
-4
@@ -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),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
+1
@@ -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,
|
||||
|
||||
-1
@@ -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),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+41
-2
@@ -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(
|
||||
|
||||
+2
-1
@@ -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 {}
|
||||
|
||||
+15
-2
@@ -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,
|
||||
|
||||
+42
-35
@@ -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) {
|
||||
|
||||
+2
-1
@@ -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';
|
||||
|
||||
|
||||
+2
-1
@@ -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';
|
||||
|
||||
+25
-11
@@ -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 {
|
||||
|
||||
+8
-2
@@ -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);
|
||||
}
|
||||
+34
-22
@@ -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;
|
||||
|
||||
+40
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -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(),
|
||||
);
|
||||
|
||||
+41
-2
@@ -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,
|
||||
);
|
||||
|
||||
|
||||
+3
-3
@@ -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({
|
||||
|
||||
+7
-2
@@ -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;
|
||||
|
||||
+2
-4
@@ -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,
|
||||
|
||||
+55
-17
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
+14
-2
@@ -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;
|
||||
|
||||
+2
-1
@@ -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 = (
|
||||
|
||||
+40
-14
@@ -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,
|
||||
);
|
||||
|
||||
+49
-15
@@ -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;
|
||||
|
||||
+4
-4
@@ -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;
|
||||
}
|
||||
+44
-27
@@ -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);
|
||||
|
||||
|
||||
+5
@@ -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(),
|
||||
});
|
||||
|
||||
|
||||
+6
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
+2
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
-2
@@ -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(
|
||||
|
||||
+1
-1
@@ -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,
|
||||
|
||||
+4
-1
@@ -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;
|
||||
}
|
||||
|
||||
+2
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
+6
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
+2
-1
@@ -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';
|
||||
|
||||
+2
-1
@@ -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';
|
||||
|
||||
+2
-1
@@ -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';
|
||||
|
||||
+1
-1
@@ -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,
|
||||
|
||||
+41
-5
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
+1
@@ -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,
|
||||
|
||||
+9
-3
@@ -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
|
||||
|
||||
''';
|
||||
}
|
||||
|
||||
+1
@@ -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!},
|
||||
|
||||
+1
@@ -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';
|
||||
|
||||
+304
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+14
-2
@@ -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;
|
||||
|
||||
+2
@@ -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),
|
||||
),
|
||||
|
||||
|
||||
+6
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
+5
@@ -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,
|
||||
});
|
||||
|
||||
+5
@@ -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,
|
||||
});
|
||||
|
||||
+4
-9
@@ -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,
|
||||
);
|
||||
}
|
||||
+2
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
+6
@@ -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 {
|
||||
|
||||
+1
-1
@@ -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,
|
||||
|
||||
-167
@@ -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));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -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;
|
||||
}
|
||||
+29
-222
@@ -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;
|
||||
}
|
||||
+3
-20
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
+9
-10
@@ -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;
|
||||
|
||||
+221
@@ -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;
|
||||
}
|
||||
+63
-55
@@ -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;
|
||||
}
|
||||
-119
@@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -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),
|
||||
),
|
||||
|
||||
-370
@@ -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);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
+8
-7
@@ -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(
|
||||
|
||||
+78
@@ -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();
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
+167
@@ -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),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
+39
-64
@@ -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
|
||||
|
||||
@@ -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"
|
||||
@@ -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:
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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": {
|
||||
|
||||
Generated
+131
-69
@@ -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"
|
||||
|
||||
@@ -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
Reference in new issue
Block a user