Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bc70b04110 | ||
|
|
11a0ef319f | ||
|
|
bd32ce5543 | ||
|
|
c47f755155 | ||
|
|
d6cbbf3c2f | ||
|
|
8c7e2d341d | ||
|
|
039d62cbbb | ||
|
|
b6773a732b | ||
|
|
60267e674e | ||
|
|
182bfae5ad | ||
|
|
149c2a2725 | ||
|
|
69a7ae5201 | ||
|
|
462daee934 | ||
|
|
63059ea39d | ||
|
|
5a8f1db5a6 | ||
|
|
a8dc55b4f3 | ||
|
|
67a253c9c7 | ||
|
|
dcf6628aa3 | ||
|
|
dc462b3847 | ||
|
|
81bc31d1b1 | ||
|
|
500d5efc33 | ||
|
|
39254b7f17 | ||
|
|
9c40a501fe | ||
|
|
c1e6f27020 | ||
|
|
d7f14db6ab | ||
|
|
fc10ee2d6b | ||
|
|
af9b4b3922 | ||
|
|
f00a78746e | ||
|
|
eb35fb25af | ||
|
|
276df8202a | ||
|
|
0ee27097ce | ||
|
|
f1ac38dd59 | ||
|
|
66ed712952 | ||
|
|
3dddd907b0 | ||
|
|
92de27eae8 | ||
|
|
1ee1f9c775 | ||
|
|
5d20e16dc5 | ||
|
|
400220a312 | ||
|
|
6846bbf103 | ||
|
|
ef3837f246 | ||
|
|
c3a41ba9ad | ||
|
|
18c209848b | ||
|
|
e076c2be90 | ||
|
|
c80fa5da78 | ||
|
|
aaadab434c | ||
|
|
0e137f12f5 | ||
|
|
6aba344583 | ||
|
|
a0a16cc493 | ||
|
|
a1e0282df0 | ||
|
|
bb370b28b7 | ||
|
|
dd1dcba599 | ||
|
|
5b4a93ee34 | ||
|
|
f426745094 | ||
|
|
840095d73c | ||
|
|
bbd64fae81 | ||
|
|
eff1da2812 | ||
|
|
65f677b277 | ||
|
|
b6ad0ba597 | ||
|
|
9b56cbb648 | ||
|
|
e20ce9052a | ||
|
|
853be71bf5 | ||
|
|
9a908abfdd | ||
|
|
e482ac75f9 | ||
|
|
205b5f2c21 | ||
|
|
064ed16a7a | ||
|
|
8a53abef3f | ||
|
|
42c2c4738a | ||
|
|
cdf6f1b38a | ||
|
|
17536fdecb | ||
|
|
eb7356474c | ||
|
|
76d1267aa5 | ||
|
|
888c1b86f0 | ||
|
|
882d5535e6 | ||
|
|
4622a412b7 | ||
|
|
fc35f74751 | ||
|
|
f6e1f2185e | ||
|
|
2e7f803e02 | ||
|
|
9eaa79b558 | ||
|
|
c85ab276e9 | ||
|
|
7f6ad504fd | ||
|
|
130c5f8e6e | ||
|
|
d39b31f357 | ||
|
|
f36cc9a5d0 | ||
|
|
6425997508 | ||
|
|
bf06211bff | ||
|
|
506f789318 | ||
|
|
ebdd28cf1c | ||
|
|
de4c1b24ef | ||
|
|
ab664ebb2f | ||
|
|
20f527f4eb | ||
|
|
b3b24d0cc0 | ||
|
|
b15ed56008 | ||
|
|
992be378bb | ||
|
|
5156248918 | ||
|
|
adaad9367d | ||
|
|
8dc28e9286 | ||
|
|
49b2bcd20d | ||
|
|
e729c0a81f | ||
|
|
ff7aab73cc | ||
|
|
fcd214b1c0 | ||
|
|
a50f2a6eb9 | ||
|
|
2c71e4f885 | ||
|
|
f5dc9ed975 | ||
|
|
608a08eb76 | ||
|
|
cd8e4ddf00 | ||
|
|
9c8d00bb61 | ||
|
|
9015e6f160 | ||
|
|
1262653144 | ||
|
|
e9ad705ea3 | ||
|
|
b0d2cdf55a | ||
|
|
c7d8a0b7c3 | ||
|
|
286c89bf74 | ||
|
|
339e3d242f | ||
|
|
fad02eb09d | ||
|
|
8f1752a253 | ||
|
|
16a8dbc6ce | ||
|
|
1a53a0e375 | ||
|
|
52b713081d | ||
|
|
3fbcd928db | ||
|
|
fa4e3d0d2a | ||
|
|
319c358d1b | ||
|
|
bc5548ff75 | ||
|
|
46a20123c1 | ||
|
|
380d921dae | ||
|
|
6130dfe36a | ||
|
|
64b4a99aa1 | ||
|
|
9344ea23ca | ||
|
|
baeedf557d | ||
|
|
b7f369b3d9 | ||
|
|
3e76fa8e42 | ||
|
|
783fd40f63 | ||
|
|
10541502b8 | ||
|
|
95fdfd7da2 | ||
|
|
4a48a2af5a | ||
|
|
d82a6c5924 | ||
|
|
4d89b7a24e | ||
|
|
e5bca9e9cc | ||
|
|
2cbd74c716 | ||
|
|
43e4b1efad | ||
|
|
a8f791e98c | ||
|
|
b6dc18ba32 | ||
|
|
9c24597bbd | ||
|
|
ea46272b49 | ||
|
|
ac36d76eed | ||
|
|
90b9456b0b | ||
|
|
309bbbd8e7 | ||
|
|
862ba3173a | ||
|
|
76cdb689fb | ||
|
|
3bbf91ab2b | ||
|
|
c8044a92d1 | ||
|
|
799ceb432f | ||
|
|
c7c9048fe3 | ||
|
|
7e2a0144a1 | ||
|
|
3f97820094 | ||
|
|
e7adc3bcbf | ||
|
|
381913307c | ||
|
|
799ed2fdb9 | ||
|
|
a68903918c | ||
|
|
170bbb6db0 | ||
|
|
a702c06dc8 | ||
|
|
fac76ac5b8 | ||
|
|
2eea57aefa | ||
|
|
23a65bfa2a | ||
|
|
23ffc3d0a8 | ||
|
|
ff5a8c322d | ||
|
|
e320439049 | ||
|
|
8656cfeb1e | ||
|
|
6a853036a5 | ||
|
|
c5cea81be2 | ||
|
|
dde50d32d6 | ||
|
|
cdee706f46 | ||
|
|
586c20a14e | ||
|
|
1e6c4a4d88 | ||
|
|
18185cc90c | ||
|
|
67e4a759c7 | ||
|
|
68da3955c1 | ||
|
|
3fb997af84 | ||
|
|
6fb677d346 | ||
|
|
50814b2bee | ||
|
|
5a6103f5d6 | ||
|
|
da3d575295 | ||
|
|
10229ca8b5 | ||
|
|
58ad9a260c | ||
|
|
5d2ff8e07c | ||
|
|
2d66dfabe8 | ||
|
|
585e71aa9d |
No files matched your search
@@ -4,10 +4,12 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- "main"
|
||||
- "release/*"
|
||||
|
||||
pull_request:
|
||||
branches:
|
||||
- "main"
|
||||
- "release/*"
|
||||
paths:
|
||||
- "frontend/app_flowy/packages/appflowy_editor/**"
|
||||
|
||||
|
||||
@@ -4,10 +4,12 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- "main"
|
||||
- "release/*"
|
||||
|
||||
pull_request:
|
||||
branches:
|
||||
- "main"
|
||||
- "release/*"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -62,6 +64,8 @@ jobs:
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y dart curl build-essential libsqlite3-dev libssl-dev clang cmake ninja-build pkg-config libgtk-3-dev
|
||||
sudo apt-get install keybinder-3.0
|
||||
elif [ "$RUNNER_OS" == "Windows" ]; then
|
||||
vcpkg integrate install
|
||||
elif [ "$RUNNER_OS" == "macOS" ]; then
|
||||
echo 'do nothing'
|
||||
fi
|
||||
@@ -74,10 +78,10 @@ jobs:
|
||||
cargo install cargo-make
|
||||
cargo install duckscript_cli
|
||||
|
||||
- name: Cargo make flowy_dev
|
||||
- name: Cargo make appflowy-deps-tools
|
||||
working-directory: frontend
|
||||
run: |
|
||||
cargo make flowy_dev
|
||||
cargo make appflowy-deps-tools
|
||||
|
||||
- name: Config Flutter
|
||||
run: |
|
||||
@@ -85,7 +89,7 @@ jobs:
|
||||
flutter config --enable-linux-desktop
|
||||
elif [ "$RUNNER_OS" == "macOS" ]; then
|
||||
flutter config --enable-macos-desktop
|
||||
elif [ "$RUNNER_OS" == "windows" ]; then
|
||||
elif [ "$RUNNER_OS" == "Windows" ]; then
|
||||
flutter config --enable-windows-desktop
|
||||
fi
|
||||
shell: bash
|
||||
|
||||
@@ -9,12 +9,14 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- "main"
|
||||
- "release/*"
|
||||
paths:
|
||||
- "frontend/app_flowy/**"
|
||||
|
||||
pull_request:
|
||||
branches:
|
||||
- "main"
|
||||
- "release/*"
|
||||
paths:
|
||||
- "frontend/app_flowy/**"
|
||||
|
||||
@@ -62,7 +64,7 @@ jobs:
|
||||
- name: Cargo make flowy dev
|
||||
working-directory: frontend
|
||||
run: |
|
||||
cargo make flowy_dev
|
||||
cargo make appflowy-deps-tools
|
||||
|
||||
- name: Flutter Deps
|
||||
run: flutter packages pub get
|
||||
@@ -71,7 +73,7 @@ jobs:
|
||||
- name: Build FlowySDK
|
||||
working-directory: frontend
|
||||
run: |
|
||||
cargo make --profile development-linux-x86_64 flowy-sdk-dev
|
||||
cargo make --profile development-linux-x86_64 appflowy-sdk-dev
|
||||
|
||||
- name: Flutter Code Generation
|
||||
working-directory: frontend/app_flowy
|
||||
|
||||
@@ -4,12 +4,14 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- "main"
|
||||
- "release/*"
|
||||
paths:
|
||||
- "frontend/app_flowy/**"
|
||||
|
||||
pull_request:
|
||||
branches:
|
||||
- "main"
|
||||
- "release/*"
|
||||
paths:
|
||||
- "frontend/app_flowy/**"
|
||||
|
||||
@@ -52,7 +54,7 @@ jobs:
|
||||
working-directory: frontend
|
||||
run: |
|
||||
cargo install cargo-make
|
||||
cargo make flowy_dev
|
||||
cargo make appflowy-deps-tools
|
||||
|
||||
- name: Flutter Deps
|
||||
working-directory: frontend/app_flowy
|
||||
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tag_name: ${{ github.ref }}
|
||||
release_name: Release ${{ github.ref }}
|
||||
release_name: v${{ github.ref }}
|
||||
body_path: ${{ env.RELEASE_NOTES_PATH }}
|
||||
|
||||
build-linux-x86:
|
||||
@@ -37,6 +37,7 @@ jobs:
|
||||
env:
|
||||
LINUX_APP_RELEASE_PATH: frontend/app_flowy/product/${{ github.ref_name }}/linux/Release
|
||||
LINUX_ZIP_NAME: AppFlowy-linux-x86.tar.gz
|
||||
LINUX_PACKAGE_NAME: AppFlowy_${{ github.ref_name }}_linux-amd64.deb
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
@@ -62,7 +63,7 @@ jobs:
|
||||
source $HOME/.cargo/env
|
||||
cargo install --force cargo-make
|
||||
cargo install --force duckscript_cli
|
||||
cargo make flowy_dev
|
||||
cargo make appflowy-deps-tools
|
||||
|
||||
- name: Build Linux app
|
||||
working-directory: frontend
|
||||
@@ -70,6 +71,46 @@ jobs:
|
||||
flutter config --enable-linux-desktop
|
||||
cargo make --env APP_VERSION=${{ github.ref_name }} --profile production-linux-x86_64 appflowy
|
||||
|
||||
- name: Build Linux package
|
||||
working-directory: ${{ env.LINUX_APP_RELEASE_PATH }}
|
||||
run: |
|
||||
mkdir -p package/opt && mv AppFlowy package/opt/
|
||||
cd package && mkdir DEBIAN
|
||||
# Create control file
|
||||
printf 'Package: AppFlowy
|
||||
Version: %s
|
||||
Architecture: amd64
|
||||
Essential: no
|
||||
Priority: optional
|
||||
Maintainer: AppFlowy
|
||||
Description: An Open Source Alternative to Notion\n' "${{ github.ref_name }}" > DEBIAN/control
|
||||
|
||||
# postinst script for creating symlink
|
||||
printf '#!/bin/bash
|
||||
if [ -e /usr/local/bin/appflowy ]; then
|
||||
echo "Symlink already exists, skipping."
|
||||
else
|
||||
echo "Creating Symlink in /usr/local/bin/appflowy"
|
||||
ln -s /opt/AppFlowy/app_flowy /usr/local/bin/appflowy
|
||||
fi' > DEBIAN/postinst
|
||||
chmod 0755 DEBIAN/postinst
|
||||
|
||||
# postrm script for cleaning up residuals
|
||||
printf '#!/bin/bash
|
||||
if [ -e /usr/local/bin/appflowy ]; then
|
||||
rm /usr/local/bin/appflowy
|
||||
fi' > DEBIAN/postrm
|
||||
chmod 0755 DEBIAN/postrm
|
||||
|
||||
mkdir -p usr/share/applications
|
||||
# Update Exec & icon path in desktop entry
|
||||
grep -rl "\[CHANGE_THIS\]" ./opt/AppFlowy/appflowy.desktop.temp | xargs sed -i "s/\[CHANGE_THIS\]/\/opt/"
|
||||
# Add desktop entry in package
|
||||
mv ./opt/AppFlowy/appflowy.desktop.temp ./usr/share/applications/appflowy.desktop
|
||||
|
||||
# Build
|
||||
cd ../ && dpkg-deb --build --root-owner-group package ${{ env.LINUX_PACKAGE_NAME }}
|
||||
|
||||
- name: Upload Release Asset
|
||||
id: upload-release-asset
|
||||
uses: actions/upload-release-asset@v1
|
||||
@@ -81,12 +122,24 @@ jobs:
|
||||
asset_name: ${{ env.LINUX_ZIP_NAME }}
|
||||
asset_content_type: application/octet-stream
|
||||
|
||||
- name: Upload Release Asset Install Package
|
||||
id: upload-release-asset-install-package
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ needs.create-release.outputs.upload_url }}
|
||||
asset_path: ${{ env.LINUX_APP_RELEASE_PATH }}/${{ env.LINUX_PACKAGE_NAME }}
|
||||
asset_name: ${{ env.LINUX_PACKAGE_NAME }}
|
||||
asset_content_type: application/octet-stream
|
||||
|
||||
build-macos-x86_64:
|
||||
runs-on: macos-latest
|
||||
needs: create-release
|
||||
env:
|
||||
MACOS_APP_RELEASE_PATH: frontend/app_flowy/product/${{ github.ref_name }}/macos/Release
|
||||
MACOS_X86_ZIP_NAME: Appflowy-macos-x86_64.zip
|
||||
MACOS_DMG_NAME: Appflowy-macos-x86_64-installer
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
@@ -108,7 +161,7 @@ jobs:
|
||||
source $HOME/.cargo/env
|
||||
cargo install --force cargo-make
|
||||
cargo install --force duckscript_cli
|
||||
cargo make flowy_dev
|
||||
cargo make appflowy-deps-tools
|
||||
|
||||
- name: Build macOS app for x86_64
|
||||
working-directory: frontend
|
||||
@@ -116,6 +169,21 @@ jobs:
|
||||
flutter config --enable-macos-desktop
|
||||
cargo make --env APP_VERSION=${{ github.ref_name }} --profile production-mac-x86_64 appflowy
|
||||
|
||||
- name: Create MacOS dmg
|
||||
working-directory: frontend
|
||||
run: |
|
||||
brew install create-dmg
|
||||
create-dmg \
|
||||
--volname ${{ env.MACOS_DMG_NAME }} \
|
||||
--hide-extension "AppFlowy.app" \
|
||||
--background 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: Archive macOS app
|
||||
working-directory: ${{ env.MACOS_APP_RELEASE_PATH }}
|
||||
run: zip --symlinks -qr ${{ env.MACOS_X86_ZIP_NAME }} AppFlowy.app
|
||||
@@ -129,6 +197,15 @@ jobs:
|
||||
asset_path: ${{ env.MACOS_APP_RELEASE_PATH }}/${{ env.MACOS_X86_ZIP_NAME }}
|
||||
asset_name: ${{ env.MACOS_X86_ZIP_NAME }}
|
||||
asset_content_type: application/octet-stream
|
||||
- name: Upload DMG Release 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 }}.dmg
|
||||
asset_name: ${{ env.MACOS_DMG_NAME }}.dmg
|
||||
asset_content_type: application/octet-stream
|
||||
|
||||
build-windows-x86_64:
|
||||
runs-on: windows-latest
|
||||
@@ -136,6 +213,7 @@ jobs:
|
||||
env:
|
||||
WINDOWS_APP_RELEASE_PATH: frontend\app_flowy\product\${{ github.ref_name }}\windows
|
||||
WINDOWS_ZIP_NAME: AppFlowy-windows-x86_64.zip
|
||||
WINDOWS_INSTALLER_NAME: AppFlowy-${{ github.ref_name }}-windows-x86_64
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
@@ -157,7 +235,7 @@ jobs:
|
||||
vcpkg integrate install
|
||||
cargo install --force cargo-make
|
||||
cargo install --force duckscript_cli
|
||||
cargo make flowy_dev
|
||||
cargo make appflowy-deps-tools
|
||||
|
||||
- name: Build Windows app
|
||||
working-directory: frontend
|
||||
@@ -169,6 +247,16 @@ jobs:
|
||||
with:
|
||||
files: ${{ env.WINDOWS_APP_RELEASE_PATH }}\
|
||||
dest: ${{ env.WINDOWS_APP_RELEASE_PATH }}\${{ env.WINDOWS_ZIP_NAME }}
|
||||
|
||||
- name: Copy installer config & icon file
|
||||
working-directory: frontend
|
||||
run: |
|
||||
cp scripts/windows_installer/* ../${{ env.WINDOWS_APP_RELEASE_PATH }}
|
||||
|
||||
- name: Build installer executable
|
||||
working-directory: ${{ env.WINDOWS_APP_RELEASE_PATH }}
|
||||
run: |
|
||||
iscc /F${{ env.WINDOWS_INSTALLER_NAME }} inno_setup_config.iss /DAppVersion=${{ github.ref_name }}
|
||||
|
||||
- name: Upload Release Asset
|
||||
id: upload-release-asset
|
||||
@@ -180,3 +268,14 @@ jobs:
|
||||
asset_path: ${{ env.WINDOWS_APP_RELEASE_PATH }}\${{ env.WINDOWS_ZIP_NAME }}
|
||||
asset_name: ${{ env.WINDOWS_ZIP_NAME }}
|
||||
asset_content_type: application/octet-stream
|
||||
|
||||
- name: Upload Installer Asset
|
||||
id: upload-installer-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.WINDOWS_APP_RELEASE_PATH }}\Output\${{ env.WINDOWS_INSTALLER_NAME }}.exe
|
||||
asset_name: ${{ env.WINDOWS_INSTALLER_NAME }}.exe
|
||||
asset_content_type: application/octet-stream
|
||||
@@ -4,6 +4,7 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- "main"
|
||||
- "release/*"
|
||||
paths:
|
||||
- "frontend/rust-lib/**"
|
||||
- "shared-lib/**"
|
||||
@@ -11,6 +12,7 @@ on:
|
||||
pull_request:
|
||||
branches:
|
||||
- "main"
|
||||
- "release/*"
|
||||
paths:
|
||||
- "frontend/rust-lib/**"
|
||||
- "shared-lib/**"
|
||||
|
||||
@@ -4,6 +4,7 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- "main"
|
||||
- "release/*"
|
||||
paths:
|
||||
- "frontend/rust-lib/**"
|
||||
- "shared-lib/**"
|
||||
@@ -11,6 +12,7 @@ on:
|
||||
pull_request:
|
||||
branches:
|
||||
- "main"
|
||||
- "release/*"
|
||||
paths:
|
||||
- "frontend/rust-lib/**"
|
||||
- "shared-lib/**"
|
||||
@@ -37,12 +39,12 @@ jobs:
|
||||
working-directory: frontend
|
||||
run: |
|
||||
cargo install cargo-make
|
||||
cargo make flowy_dev
|
||||
cargo make appflowy-deps-tools
|
||||
|
||||
- name: Build FlowySDK
|
||||
working-directory: frontend
|
||||
run: |
|
||||
cargo make --profile development-linux-x86_64 flowy-sdk-dev
|
||||
cargo make --profile development-linux-x86_64 appflowy-sdk-dev
|
||||
|
||||
- run: rustup component add rustfmt
|
||||
working-directory: frontend/rust-lib
|
||||
|
||||
@@ -4,6 +4,7 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- "main"
|
||||
- "release/*"
|
||||
paths:
|
||||
- "frontend/rust-lib/**"
|
||||
- "shared-lib/**"
|
||||
@@ -11,6 +12,7 @@ on:
|
||||
pull_request:
|
||||
branches:
|
||||
- "main"
|
||||
- "release/*"
|
||||
paths:
|
||||
- "frontend/rust-lib/**"
|
||||
- "shared-lib/**"
|
||||
|
||||
+26
-13
@@ -1,14 +1,27 @@
|
||||
# Release Notes
|
||||
## Version 0.0.7 - 11/27/2022
|
||||
### New features
|
||||
- Support adding filters by the text/checkbox/single-select property in Grid
|
||||
|
||||
## Version 0.0.6.2 - 10/30/2022
|
||||
- Fix some bugs
|
||||
|
||||
## Version 0.0.6.1 - 10/26/2022
|
||||
### New features
|
||||
- Optimize appflowy_editor dark mode style
|
||||
|
||||
### Bug Fixes
|
||||
- Unable to copy the text with checkbox or link style
|
||||
|
||||
## Version 0.0.6 - 10/23/2022
|
||||
|
||||
New features
|
||||
### New features
|
||||
- Integrate **appflowy_editor**
|
||||
|
||||
|
||||
## Version 0.0.5.3 - 09/26/2022
|
||||
|
||||
New features
|
||||
### New features
|
||||
- Open the next page automatically after deleting the current page
|
||||
- Refresh the Kanban board after altering a property type
|
||||
|
||||
@@ -22,7 +35,7 @@ New features
|
||||
|
||||
## Version 0.0.5.2 - 09/16/2022
|
||||
|
||||
New features
|
||||
### New features
|
||||
- Enable adding a new card to the "No Status" group
|
||||
- Fix some bugs
|
||||
|
||||
@@ -33,19 +46,19 @@ New features
|
||||
|
||||
## Version 0.0.5.1 - 09/14/2022
|
||||
|
||||
New features
|
||||
### New features
|
||||
- Enable deleting a field in board
|
||||
- Fix some bugs
|
||||
|
||||
|
||||
## Version 0.0.5 - 09/08/2022
|
||||
New Features - Kanban Board like Notion and Trello beta
|
||||
### New features - Kanban Board like Notion and Trello beta
|
||||
Boards are the best way to manage projects & tasks. Use them to group your databases by select, multiselect, and checkbox.
|
||||
|
||||
<p align="left"><img src="https://user-images.githubusercontent.com/12026239/190055984-6efa2d7a-ee38-4551-859e-ee56388e1859.gif" width="1000px" /></p>
|
||||
|
||||
- Set up columns that represent a specific phase of the project cycle and use cards to represent each project / task
|
||||
- Drag and drop a card from one phase / column to another phase / column
|
||||
- Set up columns that represent a specific phase of the project cycle and use cards to represent each project/task
|
||||
- Drag and drop a card from one phase/column to another phase/column
|
||||
- Update database properties in the Board view by clicking on a property and making edits on the card
|
||||
|
||||
### Other Features & Improvements
|
||||
@@ -55,7 +68,7 @@ Boards are the best way to manage projects & tasks. Use them to group your datab
|
||||
|
||||
## Version 0.0.5 - beta.2 - beta.1 - 09/01/2022
|
||||
|
||||
New features
|
||||
### New features
|
||||
- Board-view database
|
||||
- Support start editing after creating a new card
|
||||
- Support editing the card directly by clicking the edit button
|
||||
@@ -67,7 +80,7 @@ New features
|
||||
|
||||
## Version 0.0.5 - beta.1 - 08/25/2022
|
||||
|
||||
New features
|
||||
### New features
|
||||
- Board-view database
|
||||
- Group by single select
|
||||
- drag and drop cards
|
||||
@@ -90,7 +103,7 @@ New features
|
||||
|
||||
## Version 0.0.4 - beta.3 - 05/02/2022
|
||||
- Drag to reorder app/ view/ field
|
||||
- Row record open as a page
|
||||
- Row record opens as a page
|
||||
- Auto resize the height of the row in the grid
|
||||
- Support more number formats
|
||||
- Search column options, supporting Single-select, Multi-select, and number format
|
||||
@@ -114,7 +127,7 @@ New features
|
||||
## Version 0.0.4 - beta.1 - 04/08/2022
|
||||
v0.0.4 - beta.1 is pre-release
|
||||
|
||||
New features
|
||||
### New features
|
||||
- Table-view database
|
||||
- supported column types: Text, Checkbox, Single-select, Multi-select, Numbers
|
||||
- hide / delete columns
|
||||
@@ -123,12 +136,12 @@ New features
|
||||
## Version 0.0.3 - 02/23/2022
|
||||
v0.0.3 is production ready, available on Linux, macOS, and Windows
|
||||
|
||||
New features
|
||||
### New features
|
||||
- Dark Mode
|
||||
- Support new languages: French, Italian, Russian, Simplified Chinese, Spanish
|
||||
- Add Settings: Toggle on Dark Mode; Select a language
|
||||
- Show device info
|
||||
- Add tooltip on toolbar icons
|
||||
- Add tooltip on the toolbar icons
|
||||
|
||||
Bug fixes and improvements
|
||||
- Increased height of action
|
||||
|
||||
@@ -23,13 +23,15 @@ You are in charge of your data and customizations.
|
||||
<a href="https://twitter.com/appflowy"><b>Twitter</b></a>
|
||||
</p>
|
||||
|
||||
<p align="center"><img src="https://user-images.githubusercontent.com/12026239/174754661-682980e4-e386-4685-bb6f-2da357390b61.png" alt="The Open Source Alternative To Notion." width="1000px" /></p>
|
||||
<p align="center"><img src="https://user-images.githubusercontent.com/12026239/200787830-96be260b-d0a0-4152-864e-6730b19095cd.png" alt="The Open Source Alternative To Notion." width="1000px" /></p>
|
||||
<p align="center"><img src="https://user-images.githubusercontent.com/12026239/174753177-98e4c899-2356-4137-bb42-374bba2b127b.png" alt="The Open Source Alternative To Notion." width="1000px" /></p>
|
||||
<p align="center"><img src="https://user-images.githubusercontent.com/12026239/190650183-a940f1e0-a2c5-4797-ab3a-56758f6f696c.png" alt="The Open Source Alternative To Notion." width="1000px" /></p>
|
||||
|
||||
## User Installation
|
||||
|
||||
Please view the [documentation](https://appflowy.gitbook.io/docs/essential-documentation/install-appflowy/installation-methods) for OS specific installation instructions.
|
||||
* [Windows/Mac/Linux](https://appflowy.gitbook.io/docs/essential-documentation/install-appflowy/installation-methods/mac-windows-linux-packages)
|
||||
* [Docker](https://appflowy.gitbook.io/docs/essential-documentation/install-appflowy/installation-methods/installing-with-docker)
|
||||
* [Source](https://appflowy.gitbook.io/docs/essential-documentation/install-appflowy/installation-methods/from-source)
|
||||
|
||||
## Built With
|
||||
|
||||
@@ -49,7 +51,9 @@ Please view the [documentation](https://appflowy.gitbook.io/docs/essential-docum
|
||||
- [AppFlowy Roadmap ReadMe](https://appflowy.gitbook.io/docs/essential-documentation/roadmap)
|
||||
- [AppFlowy Public Roadmap](https://github.com/orgs/AppFlowy-IO/projects/5/views/12)
|
||||
|
||||
If you'd like to propose a feature, submit an issue [here](https://github.com/AppFlowy-IO/appflowy/issues).
|
||||
|
||||
If you'd like to propose a feature, submit a feature request [here](https://github.com/AppFlowy-IO/AppFlowy/issues/new?assignees=&labels=&template=feature_request.yaml&title=%5BFR%5D+) <br/>
|
||||
If you'd like to report a bug, submit bug report [here](https://github.com/AppFlowy-IO/AppFlowy/issues/new?assignees=&labels=&template=bug_report.yaml&title=%5BBug%5D+)
|
||||
|
||||
## **Releases**
|
||||
|
||||
@@ -91,7 +95,7 @@ To be honest, we do not claim to outperform Notion in terms of functionality and
|
||||
|
||||
## License
|
||||
|
||||
Distributed under the AGPLv3 License. See `LICENSE.md` for more information.
|
||||
Distributed under the AGPLv3 License. See [`LICENSE.md`](https://github.com/AppFlowy-IO/AppFlowy/blob/main/LICENSE) for more information.
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
|
||||
Vendored
+1
-1
@@ -48,7 +48,7 @@
|
||||
{
|
||||
"label": "AF: build_flowy_sdk_for_android",
|
||||
"type": "shell",
|
||||
"command": "cargo make --profile development-android flowy-sdk-dev-android",
|
||||
"command": "cargo make --profile development-android appflowy-sdk-dev-android",
|
||||
"group": "build",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}"
|
||||
|
||||
@@ -22,7 +22,7 @@ CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true
|
||||
CARGO_MAKE_CRATE_FS_NAME = "dart_ffi"
|
||||
CARGO_MAKE_CRATE_NAME = "dart-ffi"
|
||||
LIB_NAME = "dart_ffi"
|
||||
CURRENT_APP_VERSION = "0.0.6"
|
||||
CURRENT_APP_VERSION = "0.0.7"
|
||||
FEATURES = "flutter"
|
||||
PRODUCT_NAME = "AppFlowy"
|
||||
# CRATE_TYPE: https://doc.rust-lang.org/reference/linkage.html
|
||||
@@ -47,6 +47,7 @@ PROTOBUF_DERIVE_CACHE = "../shared-lib/flowy-derive/src/derive_cache/derive_cach
|
||||
# Test default config
|
||||
TEST_CRATE_TYPE = "cdylib"
|
||||
TEST_LIB_EXT = "dylib"
|
||||
TEST_RUST_LOG = "info"
|
||||
TEST_BUILD_FLAG = "debug"
|
||||
TEST_COMPILE_TARGET = "x86_64-apple-darwin"
|
||||
|
||||
|
||||
@@ -12,8 +12,8 @@ When compiling for android we need the following pre-requisites:
|
||||
- [Download](https://developer.android.com/ndk/downloads/) Android NDK version 24.
|
||||
- When downloading Android NDK you can get the compressed version as a standalone from the site.
|
||||
Or you can download it through [Android Studio](https://developer.android.com/studio).
|
||||
- After downloading the two you need to set the environment variables. For Windows that's a seperate process.
|
||||
On MacOs and Linux the process is similar.
|
||||
- After downloading the two you need to set the environment variables. For Windows that's a separate process.
|
||||
On macOS and Linux the process is similar.
|
||||
- The variables needed are '$ANDROID_NDK_HOME', this will point to where the NDK is located.
|
||||
---
|
||||
|
||||
@@ -48,9 +48,9 @@ linker = "/home/user/Android/Sdk/ndk/24.0.8215888/toolchains/llvm/prebuilt/linux
|
||||
|
||||
**Folder path: 'Android/Sdk/ndk/24.0.8215888/toolchains/llvm/prebuilt/linux-x86_64/lib64/clang/14.0.1/lib/linux'.**
|
||||
After that you have to copy this file into three different folders namely aarch64, arm, i386 and x86_64.
|
||||
We have to do this so we Android NDK can find clang on our system, if we used NDK 22 we wouldnt have to do this process.
|
||||
Though using NDK v22 will not give us alot of features to work with.
|
||||
This github [issue](https://github.com/fzyzcjy/flutter_rust_bridge/issues/419) explains the reason why we are doing this.
|
||||
We have to do this so we Android NDK can find clang on our system, if we used NDK 22 we wouldn't have to do this process.
|
||||
Though using NDK v22 will not give us a lot of features to work with.
|
||||
This GitHub [issue](https://github.com/fzyzcjy/flutter_rust_bridge/issues/419) explains the reason why we are doing this.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -161,7 +161,47 @@
|
||||
"filter": "Filter",
|
||||
"sortBy": "Sort by",
|
||||
"Properties": "Properties",
|
||||
"group": "Group"
|
||||
"group": "Group",
|
||||
"addFilter": "Add Filter",
|
||||
"deleteFilter": "Delete filter",
|
||||
"filterBy": "Filter by...",
|
||||
"typeAValue": "Type a value..."
|
||||
},
|
||||
"textFilter": {
|
||||
"contains": "Contains",
|
||||
"doesNotContain": "Does not contain",
|
||||
"endsWith": "Ends with",
|
||||
"startWith": "Starts with",
|
||||
"is": "Is",
|
||||
"isNot": "Is not",
|
||||
"isEmpty": "Is empty",
|
||||
"isNotEmpty": "Is not empty",
|
||||
"choicechipPrefix": {
|
||||
"isNot": "Not",
|
||||
"startWith": "Starts with",
|
||||
"endWith": "Ends with",
|
||||
"isEmpty": "is empty",
|
||||
"isNotEmpty": "is not empty"
|
||||
}
|
||||
},
|
||||
"checkboxFilter": {
|
||||
"isChecked": "Checked",
|
||||
"isUnchecked": "Unchecked",
|
||||
"choicechipPrefix": {
|
||||
"is": "is"
|
||||
}
|
||||
},
|
||||
"singleSelectOptionFilter": {
|
||||
"is": "Is",
|
||||
"isNot": "Is not",
|
||||
"isEmpty": "Is empty",
|
||||
"isNotEmpty": "Is not empty"
|
||||
},
|
||||
"multiSelectOptionFilter": {
|
||||
"contains": "Contains",
|
||||
"doesNotContain": "Does not contain",
|
||||
"isEmpty": "Is empty",
|
||||
"isNotEmpty": "Is not empty"
|
||||
},
|
||||
"field": {
|
||||
"hide": "Hide",
|
||||
@@ -221,7 +261,7 @@
|
||||
"menuName": "Grid"
|
||||
},
|
||||
"document": {
|
||||
"menuName": "Doc",
|
||||
"menuName": "Document",
|
||||
"date": {
|
||||
"timeHintTextInTwelveHour": "01:00 PM",
|
||||
"timeHintTextInTwentyFourHour": "13:00"
|
||||
@@ -232,4 +272,4 @@
|
||||
"create_new_card": "New"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -221,7 +221,7 @@
|
||||
"menuName": "Grille"
|
||||
},
|
||||
"document": {
|
||||
"menuName": "Doc",
|
||||
"menuName": "Document",
|
||||
"date": {
|
||||
"timeHintTextInTwelveHour": "01:00 PM",
|
||||
"timeHintTextInTwentyFourHour": "13:00"
|
||||
@@ -232,4 +232,4 @@
|
||||
"create_new_card": "Nouveau"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -209,7 +209,7 @@
|
||||
"menuName": "Grid"
|
||||
},
|
||||
"document": {
|
||||
"menuName": "Doc",
|
||||
"menuName": "Dokter",
|
||||
"date": {
|
||||
"timeHintTextInTwelveHour": "01:00 PM",
|
||||
"timeHintTextInTwentyFourHour": "13:00"
|
||||
|
||||
@@ -220,10 +220,10 @@
|
||||
"menuName": "Grade"
|
||||
},
|
||||
"document": {
|
||||
"menuName": "Doc",
|
||||
"menuName": "Documento",
|
||||
"date": {
|
||||
"timeHintTextInTwelveHour": "12:00 AM",
|
||||
"timeHintTextInTwentyFourHour": "12:00"
|
||||
"timeHintTextInTwelveHour": "01:00 PM",
|
||||
"timeHintTextInTwentyFourHour": "13:00"
|
||||
}
|
||||
},
|
||||
"board": {
|
||||
|
||||
@@ -9,31 +9,38 @@ import 'package:flowy_sdk/rust_stream.dart';
|
||||
import 'notification_helper.dart';
|
||||
|
||||
// GridPB
|
||||
typedef GridNotificationCallback = void Function(GridNotification, Either<Uint8List, FlowyError>);
|
||||
typedef GridNotificationCallback = void Function(
|
||||
GridDartNotification, Either<Uint8List, FlowyError>);
|
||||
|
||||
class GridNotificationParser extends NotificationParser<GridNotification, FlowyError> {
|
||||
GridNotificationParser({String? id, required GridNotificationCallback callback})
|
||||
class GridNotificationParser
|
||||
extends NotificationParser<GridDartNotification, FlowyError> {
|
||||
GridNotificationParser(
|
||||
{String? id, required GridNotificationCallback callback})
|
||||
: super(
|
||||
id: id,
|
||||
callback: callback,
|
||||
tyParser: (ty) => GridNotification.valueOf(ty),
|
||||
tyParser: (ty) => GridDartNotification.valueOf(ty),
|
||||
errorParser: (bytes) => FlowyError.fromBuffer(bytes),
|
||||
);
|
||||
}
|
||||
|
||||
typedef GridNotificationHandler = Function(GridNotification ty, Either<Uint8List, FlowyError> result);
|
||||
typedef GridNotificationHandler = Function(
|
||||
GridDartNotification ty, Either<Uint8List, FlowyError> result);
|
||||
|
||||
class GridNotificationListener {
|
||||
StreamSubscription<SubscribeObject>? _subscription;
|
||||
GridNotificationParser? _parser;
|
||||
|
||||
GridNotificationListener({required String objectId, required GridNotificationHandler handler})
|
||||
GridNotificationListener(
|
||||
{required String objectId, required GridNotificationHandler handler})
|
||||
: _parser = GridNotificationParser(id: objectId, callback: handler) {
|
||||
_subscription = RustStreamReceiver.listen((observable) => _parser?.parse(observable));
|
||||
_subscription =
|
||||
RustStreamReceiver.listen((observable) => _parser?.parse(observable));
|
||||
}
|
||||
|
||||
Future<void> stop() async {
|
||||
_parser = null;
|
||||
await _subscription?.cancel();
|
||||
_subscription = null;
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,8 @@ class NetworkListener {
|
||||
late StreamSubscription<ConnectivityResult> _connectivitySubscription;
|
||||
|
||||
NetworkListener() {
|
||||
_connectivitySubscription = _connectivity.onConnectivityChanged.listen(_updateConnectionStatus);
|
||||
_connectivitySubscription =
|
||||
_connectivity.onConnectivityChanged.listen(_updateConnectionStatus);
|
||||
}
|
||||
|
||||
Future<void> start() async {
|
||||
@@ -39,9 +40,11 @@ class NetworkListener {
|
||||
return NetworkType.Ethernet;
|
||||
case ConnectivityResult.mobile:
|
||||
return NetworkType.Cell;
|
||||
case ConnectivityResult.none:
|
||||
return NetworkType.UnknownNetworkType;
|
||||
case ConnectivityResult.bluetooth:
|
||||
return NetworkType.Bluetooth;
|
||||
case ConnectivityResult.vpn:
|
||||
return NetworkType.VPN;
|
||||
case ConnectivityResult.none:
|
||||
return NetworkType.UnknownNetworkType;
|
||||
}
|
||||
}();
|
||||
|
||||
@@ -15,6 +15,9 @@ class BlankPluginBuilder extends PluginBuilder {
|
||||
@override
|
||||
String get menuName => "Blank";
|
||||
|
||||
@override
|
||||
String get menuIcon => "";
|
||||
|
||||
@override
|
||||
PluginType get pluginType => PluginType.blank;
|
||||
}
|
||||
@@ -37,8 +40,7 @@ class BlankPagePlugin extends Plugin {
|
||||
|
||||
class BlankPagePluginDisplay extends PluginDisplay with NavigationItem {
|
||||
@override
|
||||
Widget get leftBarItem =>
|
||||
FlowyText.medium(LocaleKeys.blankPageTitle.tr(), fontSize: 12);
|
||||
Widget get leftBarItem => FlowyText.medium(LocaleKeys.blankPageTitle.tr());
|
||||
|
||||
@override
|
||||
Widget buildWidget(PluginContext context) => const BlankPage();
|
||||
|
||||
@@ -24,7 +24,8 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
|
||||
final BoardDataController _gridDataController;
|
||||
late final AppFlowyBoardController boardController;
|
||||
final MoveRowFFIService _rowService;
|
||||
LinkedHashMap<String, GroupController> groupControllers = LinkedHashMap();
|
||||
final LinkedHashMap<String, GroupController> groupControllers =
|
||||
LinkedHashMap();
|
||||
|
||||
GridFieldController get fieldController =>
|
||||
_gridDataController.fieldController;
|
||||
@@ -69,7 +70,7 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
|
||||
await event.when(
|
||||
initial: () async {
|
||||
_startListening();
|
||||
await _loadGrid(emit);
|
||||
await _openGrid(emit);
|
||||
},
|
||||
createBottomRow: (groupId) async {
|
||||
final startRowId = groupControllers[groupId]?.lastRow()?.id;
|
||||
@@ -135,9 +136,9 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
|
||||
}
|
||||
|
||||
void _groupItemStartEditing(GroupPB group, RowPB row, bool isEdit) {
|
||||
final fieldContext = fieldController.getField(group.fieldId);
|
||||
if (fieldContext == null) {
|
||||
Log.warn("FieldContext should not be null");
|
||||
final fieldInfo = fieldController.getField(group.fieldId);
|
||||
if (fieldInfo == null) {
|
||||
Log.warn("fieldInfo should not be null");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -146,7 +147,7 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
|
||||
// group.groupId,
|
||||
// GroupItem(
|
||||
// row: row,
|
||||
// fieldContext: fieldContext,
|
||||
// fieldInfo: fieldInfo,
|
||||
// isDraggable: !isEdit,
|
||||
// ),
|
||||
// );
|
||||
@@ -203,7 +204,7 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
|
||||
items: _buildGroupItems(group),
|
||||
customData: GroupData(
|
||||
group: group,
|
||||
fieldContext: fieldController.getField(group.fieldId)!,
|
||||
fieldInfo: fieldController.getField(group.fieldId)!,
|
||||
),
|
||||
);
|
||||
}).toList();
|
||||
@@ -274,18 +275,18 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
|
||||
|
||||
List<AppFlowyGroupItem> _buildGroupItems(GroupPB group) {
|
||||
final items = group.rows.map((row) {
|
||||
final fieldContext = fieldController.getField(group.fieldId);
|
||||
final fieldInfo = fieldController.getField(group.fieldId);
|
||||
return GroupItem(
|
||||
row: row,
|
||||
fieldContext: fieldContext!,
|
||||
fieldInfo: fieldInfo!,
|
||||
);
|
||||
}).toList();
|
||||
|
||||
return <AppFlowyGroupItem>[...items];
|
||||
}
|
||||
|
||||
Future<void> _loadGrid(Emitter<BoardState> emit) async {
|
||||
final result = await _gridDataController.loadData();
|
||||
Future<void> _openGrid(Emitter<BoardState> emit) async {
|
||||
final result = await _gridDataController.openGrid();
|
||||
result.fold(
|
||||
(grid) => emit(
|
||||
state.copyWith(loadingState: GridLoadingState.finish(left(unit))),
|
||||
@@ -373,11 +374,11 @@ class GridFieldEquatable extends Equatable {
|
||||
|
||||
class GroupItem extends AppFlowyGroupItem {
|
||||
final RowPB row;
|
||||
final GridFieldContext fieldContext;
|
||||
final FieldInfo fieldInfo;
|
||||
|
||||
GroupItem({
|
||||
required this.row,
|
||||
required this.fieldContext,
|
||||
required this.fieldInfo,
|
||||
bool draggable = true,
|
||||
}) {
|
||||
super.draggable = draggable;
|
||||
@@ -400,22 +401,22 @@ class GroupControllerDelegateImpl extends GroupControllerDelegate {
|
||||
|
||||
@override
|
||||
void insertRow(GroupPB group, RowPB row, int? index) {
|
||||
final fieldContext = fieldController.getField(group.fieldId);
|
||||
if (fieldContext == null) {
|
||||
Log.warn("FieldContext should not be null");
|
||||
final fieldInfo = fieldController.getField(group.fieldId);
|
||||
if (fieldInfo == null) {
|
||||
Log.warn("fieldInfo should not be null");
|
||||
return;
|
||||
}
|
||||
|
||||
if (index != null) {
|
||||
final item = GroupItem(
|
||||
row: row,
|
||||
fieldContext: fieldContext,
|
||||
fieldInfo: fieldInfo,
|
||||
);
|
||||
controller.insertGroupItem(group.groupId, index, item);
|
||||
} else {
|
||||
final item = GroupItem(
|
||||
row: row,
|
||||
fieldContext: fieldContext,
|
||||
fieldInfo: fieldInfo,
|
||||
);
|
||||
controller.addGroupItem(group.groupId, item);
|
||||
}
|
||||
@@ -428,30 +429,30 @@ class GroupControllerDelegateImpl extends GroupControllerDelegate {
|
||||
|
||||
@override
|
||||
void updateRow(GroupPB group, RowPB row) {
|
||||
final fieldContext = fieldController.getField(group.fieldId);
|
||||
if (fieldContext == null) {
|
||||
Log.warn("FieldContext should not be null");
|
||||
final fieldInfo = fieldController.getField(group.fieldId);
|
||||
if (fieldInfo == null) {
|
||||
Log.warn("fieldInfo should not be null");
|
||||
return;
|
||||
}
|
||||
controller.updateGroupItem(
|
||||
group.groupId,
|
||||
GroupItem(
|
||||
row: row,
|
||||
fieldContext: fieldContext,
|
||||
fieldInfo: fieldInfo,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void addNewRow(GroupPB group, RowPB row, int? index) {
|
||||
final fieldContext = fieldController.getField(group.fieldId);
|
||||
if (fieldContext == null) {
|
||||
Log.warn("FieldContext should not be null");
|
||||
final fieldInfo = fieldController.getField(group.fieldId);
|
||||
if (fieldInfo == null) {
|
||||
Log.warn("fieldInfo should not be null");
|
||||
return;
|
||||
}
|
||||
final item = GroupItem(
|
||||
row: row,
|
||||
fieldContext: fieldContext,
|
||||
fieldInfo: fieldInfo,
|
||||
draggable: false,
|
||||
);
|
||||
|
||||
@@ -478,10 +479,10 @@ class BoardEditingRow {
|
||||
|
||||
class GroupData {
|
||||
final GroupPB group;
|
||||
final GridFieldContext fieldContext;
|
||||
final FieldInfo fieldInfo;
|
||||
GroupData({
|
||||
required this.group,
|
||||
required this.fieldContext,
|
||||
required this.fieldInfo,
|
||||
});
|
||||
|
||||
CheckboxGroup? asCheckboxGroup() {
|
||||
@@ -489,7 +490,7 @@ class GroupData {
|
||||
return CheckboxGroup(group);
|
||||
}
|
||||
|
||||
FieldType get fieldType => fieldContext.fieldType;
|
||||
FieldType get fieldType => fieldInfo.fieldType;
|
||||
}
|
||||
|
||||
class CheckboxGroup {
|
||||
|
||||
@@ -12,7 +12,7 @@ import 'package:flowy_sdk/protobuf/flowy-grid/protobuf.dart';
|
||||
|
||||
import 'board_listener.dart';
|
||||
|
||||
typedef OnFieldsChanged = void Function(UnmodifiableListView<GridFieldContext>);
|
||||
typedef OnFieldsChanged = void Function(UnmodifiableListView<FieldInfo>);
|
||||
typedef OnGridChanged = void Function(GridPB);
|
||||
typedef DidLoadGroups = void Function(List<GroupPB>);
|
||||
typedef OnUpdatedGroup = void Function(List<GroupPB>);
|
||||
@@ -34,7 +34,8 @@ class BoardDataController {
|
||||
|
||||
// key: the block id
|
||||
final LinkedHashMap<String, GridBlockCache> _blocks;
|
||||
LinkedHashMap<String, GridBlockCache> get blocks => _blocks;
|
||||
UnmodifiableMapView<String, GridBlockCache> get blocks =>
|
||||
UnmodifiableMapView(_blocks);
|
||||
|
||||
OnFieldsChanged? _onFieldsChanged;
|
||||
OnGridChanged? _onGridChanged;
|
||||
@@ -107,21 +108,22 @@ class BoardDataController {
|
||||
);
|
||||
}
|
||||
|
||||
Future<Either<Unit, FlowyError>> loadData() async {
|
||||
final result = await _gridFFIService.loadGrid();
|
||||
Future<Either<Unit, FlowyError>> openGrid() async {
|
||||
final result = await _gridFFIService.openGrid();
|
||||
return Future(
|
||||
() => result.fold(
|
||||
(grid) async {
|
||||
_onGridChanged?.call(grid);
|
||||
return await fieldController.loadFields(fieldIds: grid.fields).then(
|
||||
(result) => result.fold(
|
||||
(l) {
|
||||
_loadGroups(grid.blocks);
|
||||
return left(l);
|
||||
},
|
||||
(err) => right(err),
|
||||
),
|
||||
);
|
||||
final result = await fieldController.loadFields(
|
||||
fieldIds: grid.fields,
|
||||
);
|
||||
return result.fold(
|
||||
(l) {
|
||||
_loadGroups(grid.blocks);
|
||||
return left(l);
|
||||
},
|
||||
(err) => right(err),
|
||||
);
|
||||
},
|
||||
(err) => right(err),
|
||||
),
|
||||
|
||||
@@ -32,18 +32,18 @@ class BoardListener {
|
||||
}
|
||||
|
||||
void _handler(
|
||||
GridNotification ty,
|
||||
GridDartNotification ty,
|
||||
Either<Uint8List, FlowyError> result,
|
||||
) {
|
||||
switch (ty) {
|
||||
case GridNotification.DidUpdateGroupView:
|
||||
case GridDartNotification.DidUpdateGroupView:
|
||||
result.fold(
|
||||
(payload) => _groupUpdateNotifier?.value =
|
||||
left(GroupViewChangesetPB.fromBuffer(payload)),
|
||||
(error) => _groupUpdateNotifier?.value = right(error),
|
||||
);
|
||||
break;
|
||||
case GridNotification.DidGroupByNewField:
|
||||
case GridDartNotification.DidGroupByNewField:
|
||||
result.fold(
|
||||
(payload) => _groupByNewFieldNotifier?.value =
|
||||
left(GroupViewChangesetPB.fromBuffer(payload).newGroups),
|
||||
|
||||
@@ -35,7 +35,7 @@ class BoardCheckboxCellBloc
|
||||
cellController.removeListener(_onCellChangedFn!);
|
||||
_onCellChangedFn = null;
|
||||
}
|
||||
cellController.dispose();
|
||||
await cellController.dispose();
|
||||
return super.close();
|
||||
}
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ class BoardDateCellBloc extends Bloc<BoardDateCellEvent, BoardDateCellState> {
|
||||
cellController.removeListener(_onCellChangedFn!);
|
||||
_onCellChangedFn = null;
|
||||
}
|
||||
cellController.dispose();
|
||||
await cellController.dispose();
|
||||
return super.close();
|
||||
}
|
||||
|
||||
@@ -58,14 +58,14 @@ class BoardDateCellState with _$BoardDateCellState {
|
||||
const factory BoardDateCellState({
|
||||
required DateCellDataPB? data,
|
||||
required String dateStr,
|
||||
required GridFieldContext fieldContext,
|
||||
required FieldInfo fieldInfo,
|
||||
}) = _BoardDateCellState;
|
||||
|
||||
factory BoardDateCellState.initial(GridDateCellController context) {
|
||||
final cellData = context.getCellData();
|
||||
|
||||
return BoardDateCellState(
|
||||
fieldContext: context.fieldContext,
|
||||
fieldInfo: context.fieldInfo,
|
||||
data: cellData,
|
||||
dateStr: _dateStrFromCellData(cellData),
|
||||
);
|
||||
|
||||
@@ -32,7 +32,7 @@ class BoardNumberCellBloc
|
||||
cellController.removeListener(_onCellChangedFn!);
|
||||
_onCellChangedFn = null;
|
||||
}
|
||||
cellController.dispose();
|
||||
await cellController.dispose();
|
||||
return super.close();
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -34,7 +34,7 @@ class BoardSelectOptionCellBloc
|
||||
cellController.removeListener(_onCellChangedFn!);
|
||||
_onCellChangedFn = null;
|
||||
}
|
||||
cellController.dispose();
|
||||
await cellController.dispose();
|
||||
return super.close();
|
||||
}
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ class BoardTextCellBloc extends Bloc<BoardTextCellEvent, BoardTextCellState> {
|
||||
cellController.removeListener(_onCellChangedFn!);
|
||||
_onCellChangedFn = null;
|
||||
}
|
||||
cellController.dispose();
|
||||
await cellController.dispose();
|
||||
return super.close();
|
||||
}
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ class BoardURLCellBloc extends Bloc<BoardURLCellEvent, BoardURLCellState> {
|
||||
cellController.removeListener(_onCellChangedFn!);
|
||||
_onCellChangedFn = null;
|
||||
}
|
||||
cellController.dispose();
|
||||
await cellController.dispose();
|
||||
return super.close();
|
||||
}
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ class BoardCardBloc extends Bloc<BoardCardEvent, BoardCardState> {
|
||||
return RowInfo(
|
||||
gridId: _rowService.gridId,
|
||||
fields: UnmodifiableListView(
|
||||
state.cells.map((cell) => cell.identifier.fieldContext).toList(),
|
||||
state.cells.map((cell) => cell.identifier.fieldInfo).toList(),
|
||||
),
|
||||
rowPB: state.rowPB,
|
||||
);
|
||||
@@ -132,10 +132,10 @@ class BoardCellEquatable extends Equatable {
|
||||
@override
|
||||
List<Object?> get props {
|
||||
return [
|
||||
identifier.fieldContext.id,
|
||||
identifier.fieldContext.fieldType,
|
||||
identifier.fieldContext.visibility,
|
||||
identifier.fieldContext.width,
|
||||
identifier.fieldInfo.id,
|
||||
identifier.fieldInfo.fieldType,
|
||||
identifier.fieldInfo.visibility,
|
||||
identifier.fieldInfo.width,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -39,7 +39,7 @@ class GroupController {
|
||||
void startListening() {
|
||||
_listener.start(onGroupChanged: (result) {
|
||||
result.fold(
|
||||
(GroupChangesetPB changeset) {
|
||||
(GroupRowsNotificationPB changeset) {
|
||||
for (final deletedRow in changeset.deletedRows) {
|
||||
group.rows.removeWhere((rowPB) => rowPB.id == deletedRow);
|
||||
delegate.removeRow(group, deletedRow);
|
||||
|
||||
@@ -8,7 +8,7 @@ import 'package:flowy_sdk/protobuf/flowy-grid/group.pb.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/group_changeset.pb.dart';
|
||||
|
||||
typedef UpdateGroupNotifiedValue = Either<GroupChangesetPB, FlowyError>;
|
||||
typedef UpdateGroupNotifiedValue = Either<GroupRowsNotificationPB, FlowyError>;
|
||||
|
||||
class GroupListener {
|
||||
final GroupPB group;
|
||||
@@ -27,14 +27,14 @@ class GroupListener {
|
||||
}
|
||||
|
||||
void _handler(
|
||||
GridNotification ty,
|
||||
GridDartNotification ty,
|
||||
Either<Uint8List, FlowyError> result,
|
||||
) {
|
||||
switch (ty) {
|
||||
case GridNotification.DidUpdateGroup:
|
||||
case GridDartNotification.DidUpdateGroup:
|
||||
result.fold(
|
||||
(payload) => _groupNotifier?.value =
|
||||
left(GroupChangesetPB.fromBuffer(payload)),
|
||||
left(GroupRowsNotificationPB.fromBuffer(payload)),
|
||||
(error) => _groupNotifier?.value = right(error),
|
||||
);
|
||||
break;
|
||||
|
||||
@@ -20,6 +20,9 @@ class BoardPluginBuilder implements PluginBuilder {
|
||||
@override
|
||||
String get menuName => "Board";
|
||||
|
||||
@override
|
||||
String get menuIcon => "editor/board";
|
||||
|
||||
@override
|
||||
PluginType get pluginType => PluginType.board;
|
||||
|
||||
|
||||
@@ -9,11 +9,9 @@ import 'package:app_flowy/plugins/grid/application/field/field_controller.dart';
|
||||
import 'package:app_flowy/plugins/grid/application/row/row_data_controller.dart';
|
||||
import 'package:app_flowy/plugins/grid/presentation/widgets/cell/cell_builder.dart';
|
||||
import 'package:app_flowy/plugins/grid/presentation/widgets/row/row_detail.dart';
|
||||
import 'package:app_flowy/workspace/application/appearance.dart';
|
||||
import 'package:appflowy_board/appflowy_board.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/image.dart';
|
||||
import 'package:flowy_infra/theme.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui_web.dart';
|
||||
import 'package:flowy_infra_ui/widget/error_page.dart';
|
||||
@@ -22,7 +20,6 @@ import 'package:flowy_sdk/protobuf/flowy-grid/block_entities.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../../grid/application/row/row_cache.dart';
|
||||
import '../application/board_bloc.dart';
|
||||
import 'card/card.dart';
|
||||
@@ -102,27 +99,21 @@ class _BoardContentState extends State<BoardContent> {
|
||||
}
|
||||
|
||||
Widget _buildBoard(BuildContext context) {
|
||||
return ChangeNotifierProvider.value(
|
||||
value: Provider.of<AppearanceSetting>(context, listen: true),
|
||||
child: Selector<AppearanceSetting, AppTheme>(
|
||||
selector: (ctx, notifier) => notifier.theme,
|
||||
builder: (ctx, theme, child) => Expanded(
|
||||
child: AppFlowyBoard(
|
||||
boardScrollController: scrollManager,
|
||||
scrollController: ScrollController(),
|
||||
controller: context.read<BoardBloc>().boardController,
|
||||
headerBuilder: _buildHeader,
|
||||
footerBuilder: _buildFooter,
|
||||
cardBuilder: (_, column, columnItem) => _buildCard(
|
||||
context,
|
||||
column,
|
||||
columnItem,
|
||||
),
|
||||
groupConstraints: const BoxConstraints.tightFor(width: 300),
|
||||
config: AppFlowyBoardConfig(
|
||||
groupBackgroundColor: theme.bg1,
|
||||
),
|
||||
),
|
||||
return Expanded(
|
||||
child: AppFlowyBoard(
|
||||
boardScrollController: scrollManager,
|
||||
scrollController: ScrollController(),
|
||||
controller: context.read<BoardBloc>().boardController,
|
||||
headerBuilder: _buildHeader,
|
||||
footerBuilder: _buildFooter,
|
||||
cardBuilder: (_, column, columnItem) => _buildCard(
|
||||
context,
|
||||
column,
|
||||
columnItem,
|
||||
),
|
||||
groupConstraints: const BoxConstraints.tightFor(width: 300),
|
||||
config: AppFlowyBoardConfig(
|
||||
groupBackgroundColor: Theme.of(context).colorScheme.surfaceVariant,
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -159,7 +150,6 @@ class _BoardContentState extends State<BoardContent> {
|
||||
groupData.headerData.groupName,
|
||||
fontSize: 14,
|
||||
overflow: TextOverflow.clip,
|
||||
color: context.read<AppTheme>().textColor,
|
||||
),
|
||||
),
|
||||
icon: _buildHeaderIcon(boardCustomData),
|
||||
@@ -168,7 +158,7 @@ class _BoardContentState extends State<BoardContent> {
|
||||
width: 20,
|
||||
child: svgWidget(
|
||||
"home/add",
|
||||
color: context.read<AppTheme>().iconColor,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
onAddButtonClick: () {
|
||||
@@ -191,13 +181,12 @@ class _BoardContentState extends State<BoardContent> {
|
||||
width: 20,
|
||||
child: svgWidget(
|
||||
"home/add",
|
||||
color: context.read<AppTheme>().iconColor,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
title: FlowyText.medium(
|
||||
LocaleKeys.board_column_create_new_card.tr(),
|
||||
fontSize: 14,
|
||||
color: context.read<AppTheme>().textColor,
|
||||
),
|
||||
height: 50,
|
||||
margin: config.footerPadding,
|
||||
@@ -247,7 +236,7 @@ class _BoardContentState extends State<BoardContent> {
|
||||
child: BoardCard(
|
||||
gridId: gridId,
|
||||
groupId: groupData.group.groupId,
|
||||
fieldId: groupItem.fieldContext.id,
|
||||
fieldId: groupItem.fieldInfo.id,
|
||||
isEditing: isEditing,
|
||||
cellBuilder: cellBuilder,
|
||||
dataController: cardController,
|
||||
@@ -276,10 +265,12 @@ class _BoardContentState extends State<BoardContent> {
|
||||
}
|
||||
|
||||
BoxDecoration _makeBoxDecoration(BuildContext context) {
|
||||
final theme = context.read<AppTheme>();
|
||||
final borderSide = BorderSide(color: theme.shader6, width: 1.0);
|
||||
final borderSide = BorderSide(
|
||||
color: Theme.of(context).dividerColor,
|
||||
width: 1.0,
|
||||
);
|
||||
return BoxDecoration(
|
||||
color: theme.surface,
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
border: Border.fromBorderSide(borderSide),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(6)),
|
||||
);
|
||||
@@ -294,7 +285,7 @@ class _BoardContentState extends State<BoardContent> {
|
||||
) {
|
||||
final rowInfo = RowInfo(
|
||||
gridId: gridId,
|
||||
fields: UnmodifiableListView(fieldController.fieldContexts),
|
||||
fields: UnmodifiableListView(fieldController.fieldInfos),
|
||||
rowPB: rowPB,
|
||||
);
|
||||
|
||||
@@ -329,15 +320,7 @@ class _ToolbarBlocAdaptor extends StatelessWidget {
|
||||
fieldController: bloc.fieldController,
|
||||
);
|
||||
|
||||
return ChangeNotifierProvider.value(
|
||||
value: Provider.of<AppearanceSetting>(context, listen: true),
|
||||
child: Selector<AppearanceSetting, AppTheme>(
|
||||
selector: (ctx, notifier) => notifier.theme,
|
||||
builder: (ctx, theme, child) {
|
||||
return BoardToolbar(toolbarContext: toolbarContext);
|
||||
},
|
||||
),
|
||||
);
|
||||
return BoardToolbar(toolbarContext: toolbarContext);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import 'package:app_flowy/plugins/board/application/card/board_date_cell_bloc.dart';
|
||||
import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart';
|
||||
import 'package:flowy_infra/theme.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
@@ -53,7 +52,7 @@ class _BoardDateCellState extends State<BoardDateCell> {
|
||||
child: FlowyText.regular(
|
||||
state.dateStr,
|
||||
fontSize: 13,
|
||||
color: context.read<AppTheme>().shader3,
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import 'package:app_flowy/plugins/board/application/card/board_text_cell_bloc.dart';
|
||||
import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart';
|
||||
import 'package:app_flowy/plugins/grid/presentation/widgets/cell/cell_builder.dart';
|
||||
import 'package:flowy_infra/size.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:textstyle_extensions/textstyle_extensions.dart';
|
||||
import 'board_cell.dart';
|
||||
import 'define.dart';
|
||||
|
||||
@@ -150,11 +152,7 @@ class _BoardTextCellState extends State<BoardTextCell> {
|
||||
onChanged: (value) => focusChanged(),
|
||||
onEditingComplete: () => focusNode.unfocus(),
|
||||
maxLines: null,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
fontFamily: 'Mulish',
|
||||
),
|
||||
style: Theme.of(context).textTheme.bodyMedium!.size(FontSizes.s14),
|
||||
decoration: InputDecoration(
|
||||
// Magic number 4 makes the textField take up the same space as FlowyText
|
||||
contentPadding: EdgeInsets.symmetric(
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import 'package:app_flowy/plugins/board/application/card/board_url_cell_bloc.dart';
|
||||
import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart';
|
||||
import 'package:flowy_infra/theme.dart';
|
||||
import 'package:flowy_infra/size.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:textstyle_extensions/textstyle_extensions.dart';
|
||||
|
||||
import 'define.dart';
|
||||
|
||||
@@ -34,7 +35,6 @@ class _BoardUrlCellState extends State<BoardUrlCell> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = context.watch<AppTheme>();
|
||||
return BlocProvider.value(
|
||||
value: _cellBloc,
|
||||
child: BlocBuilder<BoardURLCellBloc, BoardURLCellState>(
|
||||
@@ -53,11 +53,12 @@ class _BoardUrlCellState extends State<BoardUrlCell> {
|
||||
textAlign: TextAlign.left,
|
||||
text: TextSpan(
|
||||
text: state.content,
|
||||
style: TextStyle(
|
||||
color: theme.main2,
|
||||
fontSize: 14,
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium!
|
||||
.size(FontSizes.s14)
|
||||
.textColor(Theme.of(context).colorScheme.primary)
|
||||
.underline,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -3,7 +3,6 @@ import 'package:app_flowy/plugins/board/application/card/card_data_controller.da
|
||||
import 'package:app_flowy/plugins/grid/presentation/widgets/row/row_action_sheet.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:flowy_infra/image.dart';
|
||||
import 'package:flowy_infra/theme.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -221,8 +220,10 @@ class _CardMoreOption extends StatelessWidget with CardAccessory {
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(3.0),
|
||||
child:
|
||||
svgWidget('grid/details', color: context.read<AppTheme>().iconColor),
|
||||
child: svgWidget(
|
||||
'grid/details',
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -243,7 +244,7 @@ class _CardEditOption extends StatelessWidget with CardAccessory {
|
||||
padding: const EdgeInsets.all(3.0),
|
||||
child: svgWidget(
|
||||
'editor/edit',
|
||||
color: context.read<AppTheme>().iconColor,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import 'package:flowy_infra/theme.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
enum AccessoryType {
|
||||
edit,
|
||||
@@ -28,7 +26,6 @@ class CardAccessoryContainer extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = context.read<AppTheme>();
|
||||
final children = accessories.map((accessory) {
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
@@ -36,17 +33,16 @@ class CardAccessoryContainer extends StatelessWidget {
|
||||
accessory.onTap(context);
|
||||
onTapAccessory(accessory.type);
|
||||
},
|
||||
child: _wrapHover(theme, accessory),
|
||||
child: _wrapHover(context, accessory),
|
||||
);
|
||||
}).toList();
|
||||
return _wrapDecoration(context, Row(children: children));
|
||||
}
|
||||
|
||||
FlowyHover _wrapHover(AppTheme theme, CardAccessory accessory) {
|
||||
FlowyHover _wrapHover(BuildContext context, CardAccessory accessory) {
|
||||
return FlowyHover(
|
||||
style: HoverStyle(
|
||||
hoverColor: theme.hover,
|
||||
backgroundColor: theme.surface,
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
borderRadius: BorderRadius.zero,
|
||||
),
|
||||
builder: (_, onHover) => SizedBox(
|
||||
@@ -58,8 +54,10 @@ class CardAccessoryContainer extends StatelessWidget {
|
||||
}
|
||||
|
||||
Widget _wrapDecoration(BuildContext context, Widget child) {
|
||||
final theme = context.read<AppTheme>();
|
||||
final borderSide = BorderSide(color: theme.shader6, width: 1.0);
|
||||
final borderSide = BorderSide(
|
||||
color: Theme.of(context).dividerColor,
|
||||
width: 1.0,
|
||||
);
|
||||
final decoration = BoxDecoration(
|
||||
color: Colors.transparent,
|
||||
border: Border.fromBorderSide(borderSide),
|
||||
|
||||
@@ -7,7 +7,6 @@ import 'package:app_flowy/plugins/grid/presentation/widgets/toolbar/grid_propert
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/image.dart';
|
||||
import 'package:flowy_infra/theme.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/button.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/scrolling/styled_list.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
@@ -95,7 +94,6 @@ class _SettingItem extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = context.read<AppTheme>();
|
||||
final isSelected = context
|
||||
.read<BoardSettingBloc>()
|
||||
.state
|
||||
@@ -106,18 +104,16 @@ class _SettingItem extends StatelessWidget {
|
||||
height: 30,
|
||||
child: FlowyButton(
|
||||
isSelected: isSelected,
|
||||
text: FlowyText.medium(
|
||||
action.title(),
|
||||
fontSize: 12,
|
||||
color: theme.textColor,
|
||||
),
|
||||
hoverColor: theme.hover,
|
||||
text: FlowyText.medium(action.title()),
|
||||
onTap: () {
|
||||
context
|
||||
.read<BoardSettingBloc>()
|
||||
.add(BoardSettingEvent.performAction(action));
|
||||
},
|
||||
leftIcon: svgWidget(action.iconName(), color: theme.iconColor),
|
||||
leftIcon: svgWidget(
|
||||
action.iconName(),
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import 'package:app_flowy/plugins/grid/application/field/field_controller.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:flowy_infra/image.dart';
|
||||
import 'package:flowy_infra/theme.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/color_extension.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/button.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../../generated/locale_keys.g.dart';
|
||||
import 'board_setting.dart';
|
||||
|
||||
class BoardToolbarContext {
|
||||
@@ -32,6 +32,7 @@ class BoardToolbar extends StatelessWidget {
|
||||
height: 40,
|
||||
child: Row(
|
||||
children: [
|
||||
const Spacer(),
|
||||
_SettingButton(
|
||||
settingContext: BoardSettingContext.from(toolbarContext),
|
||||
),
|
||||
@@ -61,17 +62,20 @@ class _SettingButtonState extends State<_SettingButton> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = context.read<AppTheme>();
|
||||
return AppFlowyPopover(
|
||||
controller: popoverController,
|
||||
direction: PopoverDirection.leftWithTopAligned,
|
||||
triggerActions: PopoverTriggerFlags.none,
|
||||
constraints: BoxConstraints.loose(const Size(260, 400)),
|
||||
child: FlowyIconButton(
|
||||
hoverColor: theme.hover,
|
||||
width: 22,
|
||||
icon: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 3.0, horizontal: 3.0),
|
||||
child: svgWidget("grid/setting/setting", color: theme.iconColor),
|
||||
),
|
||||
child: FlowyTextButton(
|
||||
LocaleKeys.settings_title.tr(),
|
||||
fontSize: 14,
|
||||
fillColor: Colors.transparent,
|
||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||
padding: const EdgeInsets.symmetric(vertical: 2, horizontal: 6),
|
||||
onPressed: () {
|
||||
popoverController.show();
|
||||
},
|
||||
),
|
||||
popupBuilder: (BuildContext popoverContext) {
|
||||
return BoardSettingListPopover(
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flowy_infra/theme.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
EditorStyle customEditorStyle(BuildContext context) {
|
||||
final theme = context.watch<AppTheme>();
|
||||
const baseFontSize = 14.0;
|
||||
const basePadding = 12.0;
|
||||
var textStyle = theme.isDark
|
||||
? BuiltInTextStyle.builtInDarkMode()
|
||||
: BuiltInTextStyle.builtIn();
|
||||
textStyle = textStyle.copyWith(
|
||||
defaultTextStyle: textStyle.defaultTextStyle.copyWith(
|
||||
fontFamily: 'poppins',
|
||||
fontSize: baseFontSize,
|
||||
),
|
||||
bold: textStyle.bold.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
);
|
||||
return EditorStyle.defaultStyle().copyWith(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 80),
|
||||
textStyle: textStyle,
|
||||
pluginStyles: {
|
||||
'text/heading': builtInPluginStyle
|
||||
..update(
|
||||
'textStyle',
|
||||
(_) => (EditorState editorState, Node node) {
|
||||
final headingToFontSize = {
|
||||
'h1': baseFontSize + 12,
|
||||
'h2': baseFontSize + 8,
|
||||
'h3': baseFontSize + 4,
|
||||
'h4': baseFontSize,
|
||||
'h5': baseFontSize,
|
||||
'h6': baseFontSize,
|
||||
};
|
||||
final fontSize =
|
||||
headingToFontSize[node.attributes.heading] ?? baseFontSize;
|
||||
return TextStyle(fontSize: fontSize, fontWeight: FontWeight.w600);
|
||||
},
|
||||
)
|
||||
..update(
|
||||
'padding',
|
||||
(_) => (EditorState editorState, Node node) {
|
||||
final headingToPadding = {
|
||||
'h1': basePadding + 6,
|
||||
'h2': basePadding + 4,
|
||||
'h3': basePadding + 2,
|
||||
'h4': basePadding,
|
||||
'h5': basePadding,
|
||||
'h6': basePadding,
|
||||
};
|
||||
final padding =
|
||||
headingToPadding[node.attributes.heading] ?? basePadding;
|
||||
return EdgeInsets.only(bottom: padding);
|
||||
},
|
||||
)
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
import 'package:flowy_infra/image.dart';
|
||||
import 'package:flowy_infra/theme.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_quill/flutter_quill.dart';
|
||||
|
||||
class StyleWidgetBuilder {
|
||||
static QuillCheckboxBuilder checkbox(AppTheme theme) {
|
||||
return EditorCheckboxBuilder(theme);
|
||||
}
|
||||
}
|
||||
|
||||
class EditorCheckboxBuilder extends QuillCheckboxBuilder {
|
||||
final AppTheme theme;
|
||||
|
||||
EditorCheckboxBuilder(this.theme);
|
||||
|
||||
@override
|
||||
Widget build(
|
||||
{required BuildContext context,
|
||||
required bool isChecked,
|
||||
required ValueChanged<bool> onChanged}) {
|
||||
return FlowyEditorCheckbox(
|
||||
theme: theme,
|
||||
isChecked: isChecked,
|
||||
onChanged: onChanged,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class FlowyEditorCheckbox extends StatefulWidget {
|
||||
final bool isChecked;
|
||||
final ValueChanged<bool> onChanged;
|
||||
final AppTheme theme;
|
||||
const FlowyEditorCheckbox({
|
||||
required this.theme,
|
||||
required this.isChecked,
|
||||
required this.onChanged,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
FlowyEditorCheckboxState createState() => FlowyEditorCheckboxState();
|
||||
}
|
||||
|
||||
class FlowyEditorCheckboxState extends State<FlowyEditorCheckbox> {
|
||||
late bool isChecked;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
isChecked = widget.isChecked;
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final icon = isChecked
|
||||
? svgWidget('editor/editor_check')
|
||||
: svgWidget('editor/editor_uncheck');
|
||||
return Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: FlowyIconButton(
|
||||
onPressed: () {
|
||||
isChecked = !isChecked;
|
||||
widget.onChanged(isChecked);
|
||||
setState(() {});
|
||||
},
|
||||
iconPadding: EdgeInsets.zero,
|
||||
icon: icon,
|
||||
width: 23,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
import 'package:flutter_quill/flutter_quill.dart';
|
||||
import 'package:flutter_quill/models/documents/style.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'toolbar_icon_button.dart';
|
||||
|
||||
class FlowyCheckListButton extends StatefulWidget {
|
||||
const FlowyCheckListButton({
|
||||
required this.controller,
|
||||
required this.attribute,
|
||||
required this.tooltipText,
|
||||
this.iconSize = defaultIconSize,
|
||||
this.fillColor,
|
||||
this.childBuilder = defaultToggleStyleButtonBuilder,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
final double iconSize;
|
||||
|
||||
final Color? fillColor;
|
||||
|
||||
final QuillController controller;
|
||||
|
||||
final ToggleStyleButtonBuilder childBuilder;
|
||||
|
||||
final Attribute attribute;
|
||||
|
||||
final String tooltipText;
|
||||
|
||||
@override
|
||||
FlowyCheckListButtonState createState() => FlowyCheckListButtonState();
|
||||
}
|
||||
|
||||
class FlowyCheckListButtonState extends State<FlowyCheckListButton> {
|
||||
bool? _isToggled;
|
||||
|
||||
Style get _selectionStyle => widget.controller.getSelectionStyle();
|
||||
|
||||
void _didChangeEditingValue() {
|
||||
setState(() {
|
||||
_isToggled =
|
||||
_getIsToggled(widget.controller.getSelectionStyle().attributes);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_isToggled = _getIsToggled(_selectionStyle.attributes);
|
||||
widget.controller.addListener(_didChangeEditingValue);
|
||||
}
|
||||
|
||||
bool _getIsToggled(Map<String, Attribute> attrs) {
|
||||
if (widget.attribute.key == Attribute.list.key) {
|
||||
final attribute = attrs[widget.attribute.key];
|
||||
if (attribute == null) {
|
||||
return false;
|
||||
}
|
||||
return attribute.value == widget.attribute.value ||
|
||||
attribute.value == Attribute.checked.value;
|
||||
}
|
||||
return attrs.containsKey(widget.attribute.key);
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant FlowyCheckListButton oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.controller != widget.controller) {
|
||||
oldWidget.controller.removeListener(_didChangeEditingValue);
|
||||
widget.controller.addListener(_didChangeEditingValue);
|
||||
_isToggled = _getIsToggled(_selectionStyle.attributes);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
widget.controller.removeListener(_didChangeEditingValue);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ToolbarIconButton(
|
||||
onPressed: _toggleAttribute,
|
||||
width: widget.iconSize * kIconButtonFactor,
|
||||
iconName: 'editor/checkbox',
|
||||
isToggled: _isToggled ?? false,
|
||||
tooltipText: widget.tooltipText,
|
||||
);
|
||||
}
|
||||
|
||||
void _toggleAttribute() {
|
||||
widget.controller.formatSelection(_isToggled!
|
||||
? Attribute.clone(Attribute.unchecked, null)
|
||||
: Attribute.unchecked);
|
||||
}
|
||||
}
|
||||
@@ -1,280 +0,0 @@
|
||||
import 'package:flowy_infra_ui/widget/dialog/styled_dialogs.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_quill/flutter_quill.dart';
|
||||
import 'package:flutter_quill/models/documents/style.dart';
|
||||
import 'package:flutter_quill/utils/color.dart';
|
||||
import 'package:app_flowy/generated/locale_keys.g.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'toolbar_icon_button.dart';
|
||||
|
||||
class FlowyColorButton extends StatefulWidget {
|
||||
const FlowyColorButton({
|
||||
required this.icon,
|
||||
required this.controller,
|
||||
required this.background,
|
||||
this.iconSize = defaultIconSize,
|
||||
this.iconTheme,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
final IconData icon;
|
||||
final double iconSize;
|
||||
final bool background;
|
||||
final QuillController controller;
|
||||
final QuillIconTheme? iconTheme;
|
||||
|
||||
@override
|
||||
FlowyColorButtonState createState() => FlowyColorButtonState();
|
||||
}
|
||||
|
||||
class FlowyColorButtonState extends State<FlowyColorButton> {
|
||||
late bool _isToggledColor;
|
||||
late bool _isToggledBackground;
|
||||
late bool _isWhite;
|
||||
late bool _isWhitebackground;
|
||||
|
||||
Style get _selectionStyle => widget.controller.getSelectionStyle();
|
||||
|
||||
void _didChangeEditingValue() {
|
||||
setState(() {
|
||||
_isToggledColor =
|
||||
_getIsToggledColor(widget.controller.getSelectionStyle().attributes);
|
||||
_isToggledBackground = _getIsToggledBackground(
|
||||
widget.controller.getSelectionStyle().attributes);
|
||||
_isWhite = _isToggledColor &&
|
||||
_selectionStyle.attributes['color']!.value == '#ffffff';
|
||||
_isWhitebackground = _isToggledBackground &&
|
||||
_selectionStyle.attributes['background']!.value == '#ffffff';
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_isToggledColor = _getIsToggledColor(_selectionStyle.attributes);
|
||||
_isToggledBackground = _getIsToggledBackground(_selectionStyle.attributes);
|
||||
_isWhite = _isToggledColor &&
|
||||
_selectionStyle.attributes['color']!.value == '#ffffff';
|
||||
_isWhitebackground = _isToggledBackground &&
|
||||
_selectionStyle.attributes['background']!.value == '#ffffff';
|
||||
widget.controller.addListener(_didChangeEditingValue);
|
||||
}
|
||||
|
||||
bool _getIsToggledColor(Map<String, Attribute> attrs) {
|
||||
return attrs.containsKey(Attribute.color.key);
|
||||
}
|
||||
|
||||
bool _getIsToggledBackground(Map<String, Attribute> attrs) {
|
||||
return attrs.containsKey(Attribute.background.key);
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant FlowyColorButton oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.controller != widget.controller) {
|
||||
oldWidget.controller.removeListener(_didChangeEditingValue);
|
||||
widget.controller.addListener(_didChangeEditingValue);
|
||||
_isToggledColor = _getIsToggledColor(_selectionStyle.attributes);
|
||||
_isToggledBackground =
|
||||
_getIsToggledBackground(_selectionStyle.attributes);
|
||||
_isWhite = _isToggledColor &&
|
||||
_selectionStyle.attributes['color']!.value == '#ffffff';
|
||||
_isWhitebackground = _isToggledBackground &&
|
||||
_selectionStyle.attributes['background']!.value == '#ffffff';
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
widget.controller.removeListener(_didChangeEditingValue);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
final fillColor = _isToggledColor && !widget.background && _isWhite
|
||||
? stringToColor('#ffffff')
|
||||
: (widget.iconTheme?.iconUnselectedFillColor ?? theme.canvasColor);
|
||||
final fillColorBackground =
|
||||
_isToggledBackground && widget.background && _isWhitebackground
|
||||
? stringToColor('#ffffff')
|
||||
: (widget.iconTheme?.iconUnselectedFillColor ?? theme.canvasColor);
|
||||
|
||||
return Tooltip(
|
||||
message: LocaleKeys.toolbar_highlight.tr(),
|
||||
showDuration: Duration.zero,
|
||||
child: QuillIconButton(
|
||||
highlightElevation: 0,
|
||||
hoverElevation: 0,
|
||||
size: widget.iconSize * kIconButtonFactor,
|
||||
icon: Icon(widget.icon,
|
||||
size: widget.iconSize, color: theme.iconTheme.color),
|
||||
fillColor: widget.background ? fillColorBackground : fillColor,
|
||||
onPressed: _showColorPicker,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _changeColor(BuildContext context, Color color) {
|
||||
var hex = color.value.toRadixString(16);
|
||||
if (hex.startsWith('ff')) {
|
||||
hex = hex.substring(2);
|
||||
}
|
||||
hex = '#$hex';
|
||||
widget.controller.formatSelection(
|
||||
widget.background ? BackgroundAttribute(hex) : ColorAttribute(hex));
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
|
||||
void _showColorPicker() {
|
||||
final style = widget.controller.getSelectionStyle();
|
||||
final values = style.values
|
||||
.where((v) => v.key == Attribute.background.key)
|
||||
.map((v) => v.value);
|
||||
int initialColor = 0;
|
||||
if (values.isNotEmpty) {
|
||||
assert(values.length == 1);
|
||||
initialColor = stringToHex(values.first);
|
||||
}
|
||||
|
||||
StyledDialog(
|
||||
child: SingleChildScrollView(
|
||||
child: FlowyColorPicker(
|
||||
onColorChanged: (color) {
|
||||
if (color == null) {
|
||||
widget.controller.formatSelection(BackgroundAttribute(null));
|
||||
Navigator.of(context).pop();
|
||||
} else {
|
||||
_changeColor(context, color);
|
||||
}
|
||||
},
|
||||
initialColor: initialColor,
|
||||
),
|
||||
),
|
||||
).show(context);
|
||||
}
|
||||
}
|
||||
|
||||
int stringToHex(String code) {
|
||||
return int.parse(code.substring(1, 7), radix: 16) + 0xFF000000;
|
||||
}
|
||||
|
||||
class FlowyColorPicker extends StatefulWidget {
|
||||
final List<int> colors = [
|
||||
0xffe8e0ff,
|
||||
0xffffe7fd,
|
||||
0xffffe7ee,
|
||||
0xffffefe3,
|
||||
0xfffff2cd,
|
||||
0xfff5ffdc,
|
||||
0xffddffd6,
|
||||
0xffdefff1,
|
||||
];
|
||||
final Function(Color?) onColorChanged;
|
||||
final int initialColor;
|
||||
FlowyColorPicker(
|
||||
{Key? key, required this.onColorChanged, this.initialColor = 0})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<FlowyColorPicker> createState() => _FlowyColorPickerState();
|
||||
}
|
||||
|
||||
// if (shrinkWrap) {
|
||||
// innerContent = IntrinsicWidth(child: IntrinsicHeight(child: innerContent));
|
||||
// }
|
||||
class _FlowyColorPickerState extends State<FlowyColorPicker> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
const double width = 480;
|
||||
const int crossAxisCount = 6;
|
||||
const double mainAxisSpacing = 10;
|
||||
const double crossAxisSpacing = 10;
|
||||
final numberOfRows = (widget.colors.length / crossAxisCount).ceil();
|
||||
|
||||
const perRowHeight =
|
||||
((width - ((crossAxisCount - 1) * mainAxisSpacing)) / crossAxisCount);
|
||||
final totalHeight =
|
||||
numberOfRows * perRowHeight + numberOfRows * crossAxisSpacing;
|
||||
|
||||
return Container(
|
||||
constraints: BoxConstraints.tightFor(width: width, height: totalHeight),
|
||||
child: CustomScrollView(
|
||||
scrollDirection: Axis.vertical,
|
||||
controller: ScrollController(),
|
||||
physics: const ClampingScrollPhysics(),
|
||||
slivers: [
|
||||
SliverGrid(
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: crossAxisCount,
|
||||
mainAxisSpacing: mainAxisSpacing,
|
||||
crossAxisSpacing: crossAxisSpacing,
|
||||
childAspectRatio: 1.0,
|
||||
),
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(BuildContext context, int index) {
|
||||
if (widget.colors.length > index) {
|
||||
final isSelected =
|
||||
widget.colors[index] == widget.initialColor;
|
||||
return ColorItem(
|
||||
color: Color(widget.colors[index]),
|
||||
onPressed: widget.onColorChanged,
|
||||
isSelected: isSelected,
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
childCount: widget.colors.length,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ColorItem extends StatelessWidget {
|
||||
final Function(Color?) onPressed;
|
||||
final bool isSelected;
|
||||
final Color color;
|
||||
const ColorItem({
|
||||
Key? key,
|
||||
required this.color,
|
||||
required this.onPressed,
|
||||
this.isSelected = false,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!isSelected) {
|
||||
return RawMaterialButton(
|
||||
onPressed: () {
|
||||
onPressed(color);
|
||||
},
|
||||
elevation: 0,
|
||||
hoverElevation: 0.6,
|
||||
fillColor: color,
|
||||
shape: const CircleBorder(),
|
||||
);
|
||||
} else {
|
||||
return RawMaterialButton(
|
||||
shape: const CircleBorder(
|
||||
side: BorderSide(color: Colors.white, width: 8)) +
|
||||
CircleBorder(side: BorderSide(color: color, width: 4)),
|
||||
onPressed: () {
|
||||
if (isSelected) {
|
||||
onPressed(null);
|
||||
} else {
|
||||
onPressed(color);
|
||||
}
|
||||
},
|
||||
elevation: 1.0,
|
||||
hoverElevation: 0.6,
|
||||
fillColor: color,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
import 'package:flutter_quill/flutter_quill.dart';
|
||||
import 'package:flutter_quill/models/documents/style.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:app_flowy/generated/locale_keys.g.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'toolbar_icon_button.dart';
|
||||
|
||||
class FlowyHeaderStyleButton extends StatefulWidget {
|
||||
const FlowyHeaderStyleButton({
|
||||
required this.controller,
|
||||
this.iconSize = defaultIconSize,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
final QuillController controller;
|
||||
final double iconSize;
|
||||
|
||||
@override
|
||||
FlowyHeaderStyleButtonState createState() => FlowyHeaderStyleButtonState();
|
||||
}
|
||||
|
||||
class FlowyHeaderStyleButtonState extends State<FlowyHeaderStyleButton> {
|
||||
Attribute? _value;
|
||||
|
||||
Style get _selectionStyle => widget.controller.getSelectionStyle();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
setState(() {
|
||||
_value =
|
||||
_selectionStyle.attributes[Attribute.header.key] ?? Attribute.header;
|
||||
});
|
||||
widget.controller.addListener(_didChangeEditingValue);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final valueToText = <Attribute, String>{
|
||||
Attribute.h1: 'H1',
|
||||
Attribute.h2: 'H2',
|
||||
Attribute.h3: 'H3',
|
||||
};
|
||||
|
||||
final valueAttribute = <Attribute>[
|
||||
Attribute.h1,
|
||||
Attribute.h2,
|
||||
Attribute.h3
|
||||
];
|
||||
final valueString = <String>['H1', 'H2', 'H3'];
|
||||
final attributeImageName = <String>['editor/H1', 'editor/H2', 'editor/H3'];
|
||||
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: List.generate(3, (index) {
|
||||
// final child =
|
||||
// _valueToText[_value] == _valueString[index] ? svg('editor/H1', color: Colors.white) : svg('editor/H1');
|
||||
|
||||
final headerTitle = "${LocaleKeys.toolbar_header.tr()} ${index + 1}";
|
||||
final isToggled = valueToText[_value] == valueString[index];
|
||||
return ToolbarIconButton(
|
||||
onPressed: () {
|
||||
if (isToggled) {
|
||||
widget.controller.formatSelection(Attribute.header);
|
||||
} else {
|
||||
widget.controller.formatSelection(valueAttribute[index]);
|
||||
}
|
||||
},
|
||||
width: widget.iconSize * kIconButtonFactor,
|
||||
iconName: attributeImageName[index],
|
||||
isToggled: isToggled,
|
||||
tooltipText: headerTitle,
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
void _didChangeEditingValue() {
|
||||
setState(() {
|
||||
_value =
|
||||
_selectionStyle.attributes[Attribute.header.key] ?? Attribute.header;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant FlowyHeaderStyleButton oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.controller != widget.controller) {
|
||||
oldWidget.controller.removeListener(_didChangeEditingValue);
|
||||
widget.controller.addListener(_didChangeEditingValue);
|
||||
_value =
|
||||
_selectionStyle.attributes[Attribute.header.key] ?? Attribute.header;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
widget.controller.removeListener(_didChangeEditingValue);
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_quill/flutter_quill.dart';
|
||||
|
||||
class FlowyHistoryButton extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final double iconSize;
|
||||
final bool undo;
|
||||
final QuillController controller;
|
||||
final String tooltipText;
|
||||
|
||||
const FlowyHistoryButton({
|
||||
required this.icon,
|
||||
required this.controller,
|
||||
required this.undo,
|
||||
required this.tooltipText,
|
||||
required this.iconSize,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Tooltip(
|
||||
message: tooltipText,
|
||||
showDuration: Duration.zero,
|
||||
child: HistoryButton(
|
||||
icon: icon,
|
||||
iconSize: iconSize,
|
||||
controller: controller,
|
||||
undo: undo,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
import 'package:flutter_quill/flutter_quill.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'toolbar_icon_button.dart';
|
||||
|
||||
class FlowyImageButton extends StatelessWidget {
|
||||
const FlowyImageButton({
|
||||
required this.controller,
|
||||
required this.tooltipText,
|
||||
this.iconSize = defaultIconSize,
|
||||
this.onImagePickCallback,
|
||||
this.fillColor,
|
||||
this.filePickImpl,
|
||||
this.webImagePickImpl,
|
||||
this.mediaPickSettingSelector,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
final double iconSize;
|
||||
|
||||
final Color? fillColor;
|
||||
|
||||
final QuillController controller;
|
||||
|
||||
final OnImagePickCallback? onImagePickCallback;
|
||||
|
||||
final WebImagePickImpl? webImagePickImpl;
|
||||
|
||||
final FilePickImpl? filePickImpl;
|
||||
|
||||
final MediaPickSettingSelector? mediaPickSettingSelector;
|
||||
|
||||
final String tooltipText;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ToolbarIconButton(
|
||||
iconName: 'editor/image',
|
||||
width: iconSize * 1.77,
|
||||
onPressed: () => _onPressedHandler(context),
|
||||
isToggled: false,
|
||||
tooltipText: tooltipText,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onPressedHandler(BuildContext context) async {
|
||||
// if (onImagePickCallback != null) {
|
||||
// final selector = mediaPickSettingSelector ?? ImageVideoUtils.selectMediaPickSetting;
|
||||
// final source = await selector(context);
|
||||
// if (source != null) {
|
||||
// if (source == MediaPickSetting.Gallery) {
|
||||
// _pickImage(context);
|
||||
// } else {
|
||||
// _typeLink(context);
|
||||
// }
|
||||
// }
|
||||
// } else {
|
||||
// _typeLink(context);
|
||||
// }
|
||||
}
|
||||
|
||||
// void _pickImage(BuildContext context) => ImageVideoUtils.handleImageButtonTap(
|
||||
// context,
|
||||
// controller,
|
||||
// ImageSource.gallery,
|
||||
// onImagePickCallback!,
|
||||
// filePickImpl: filePickImpl,
|
||||
// webImagePickImpl: webImagePickImpl,
|
||||
// );
|
||||
|
||||
// void _typeLink(BuildContext context) {
|
||||
// TextFieldDialog(
|
||||
// title: 'URL',
|
||||
// value: "",
|
||||
// confirm: (newValue) {
|
||||
// if (newValue.isEmpty) {
|
||||
// return;
|
||||
// }
|
||||
// final index = controller.selection.baseOffset;
|
||||
// final length = controller.selection.extentOffset - index;
|
||||
|
||||
// controller.replaceText(index, length, BlockEmbed.image(newValue), null);
|
||||
// },
|
||||
// ).show(context);
|
||||
// }
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
import 'package:app_flowy/workspace/presentation/widgets/dialogs.dart';
|
||||
import 'package:flutter_quill/flutter_quill.dart';
|
||||
import 'package:flowy_infra/image.dart';
|
||||
import 'package:flowy_infra/theme.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'toolbar_icon_button.dart';
|
||||
|
||||
class FlowyLinkStyleButton extends StatefulWidget {
|
||||
const FlowyLinkStyleButton({
|
||||
required this.controller,
|
||||
this.iconSize = defaultIconSize,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
final QuillController controller;
|
||||
final double iconSize;
|
||||
|
||||
@override
|
||||
FlowyLinkStyleButtonState createState() => FlowyLinkStyleButtonState();
|
||||
}
|
||||
|
||||
class FlowyLinkStyleButtonState extends State<FlowyLinkStyleButton> {
|
||||
void _didChangeSelection() {
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
widget.controller.addListener(_didChangeSelection);
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant FlowyLinkStyleButton oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.controller != widget.controller) {
|
||||
oldWidget.controller.removeListener(_didChangeSelection);
|
||||
widget.controller.addListener(_didChangeSelection);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
widget.controller.removeListener(_didChangeSelection);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = context.watch<AppTheme>();
|
||||
final isEnabled = !widget.controller.selection.isCollapsed;
|
||||
final pressedHandler = isEnabled ? () => _openLinkDialog(context) : null;
|
||||
final icon = isEnabled
|
||||
? svgWidget(
|
||||
'editor/share',
|
||||
color: theme.iconColor,
|
||||
)
|
||||
: svgWidget(
|
||||
'editor/share',
|
||||
color: theme.disableIconColor,
|
||||
);
|
||||
|
||||
return FlowyIconButton(
|
||||
onPressed: pressedHandler,
|
||||
iconPadding: const EdgeInsets.symmetric(horizontal: 4, vertical: 4),
|
||||
icon: icon,
|
||||
fillColor: theme.shader6,
|
||||
hoverColor: theme.shader5,
|
||||
width: widget.iconSize * kIconButtonFactor,
|
||||
);
|
||||
}
|
||||
|
||||
void _openLinkDialog(BuildContext context) {
|
||||
final style = widget.controller.getSelectionStyle();
|
||||
final values = style.values
|
||||
.where((v) => v.key == Attribute.link.key)
|
||||
.map((v) => v.value);
|
||||
String value = "";
|
||||
if (values.isNotEmpty) {
|
||||
assert(values.length == 1);
|
||||
value = values.first;
|
||||
}
|
||||
|
||||
NavigatorTextFieldDialog(
|
||||
title: 'URL',
|
||||
value: value,
|
||||
confirm: (newValue) {
|
||||
if (newValue.isEmpty) {
|
||||
return;
|
||||
}
|
||||
widget.controller.formatSelection(LinkAttribute(newValue));
|
||||
},
|
||||
).show(context);
|
||||
}
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
import 'package:flutter_quill/flutter_quill.dart';
|
||||
import 'package:flutter_quill/models/documents/style.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'toolbar_icon_button.dart';
|
||||
|
||||
class FlowyToggleStyleButton extends StatefulWidget {
|
||||
final Attribute attribute;
|
||||
final String normalIcon;
|
||||
final double iconSize;
|
||||
final QuillController controller;
|
||||
final String tooltipText;
|
||||
|
||||
const FlowyToggleStyleButton({
|
||||
required this.attribute,
|
||||
required this.normalIcon,
|
||||
required this.controller,
|
||||
required this.tooltipText,
|
||||
this.iconSize = defaultIconSize,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
ToggleStyleButtonState createState() => ToggleStyleButtonState();
|
||||
}
|
||||
|
||||
class ToggleStyleButtonState extends State<FlowyToggleStyleButton> {
|
||||
bool? _isToggled;
|
||||
Style get _selectionStyle => widget.controller.getSelectionStyle();
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_isToggled = _getIsToggled(_selectionStyle.attributes);
|
||||
widget.controller.addListener(_didChangeEditingValue);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ToolbarIconButton(
|
||||
onPressed: _toggleAttribute,
|
||||
width: widget.iconSize * kIconButtonFactor,
|
||||
isToggled: _isToggled ?? false,
|
||||
iconName: widget.normalIcon,
|
||||
tooltipText: widget.tooltipText,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant FlowyToggleStyleButton oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.controller != widget.controller) {
|
||||
oldWidget.controller.removeListener(_didChangeEditingValue);
|
||||
widget.controller.addListener(_didChangeEditingValue);
|
||||
_isToggled = _getIsToggled(_selectionStyle.attributes);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
widget.controller.removeListener(_didChangeEditingValue);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _didChangeEditingValue() {
|
||||
setState(() => _isToggled = _getIsToggled(_selectionStyle.attributes));
|
||||
}
|
||||
|
||||
bool _getIsToggled(Map<String, Attribute> attrs) {
|
||||
if (widget.attribute.key == Attribute.list.key) {
|
||||
final attribute = attrs[widget.attribute.key];
|
||||
if (attribute == null) {
|
||||
return false;
|
||||
}
|
||||
return attribute.value == widget.attribute.value;
|
||||
}
|
||||
return attrs.containsKey(widget.attribute.key);
|
||||
}
|
||||
|
||||
void _toggleAttribute() {
|
||||
widget.controller.formatSelection(_isToggled!
|
||||
? Attribute.clone(widget.attribute, null)
|
||||
: widget.attribute);
|
||||
}
|
||||
}
|
||||
@@ -1,308 +0,0 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:app_flowy/workspace/presentation/widgets/emoji_picker/emoji_picker.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter_quill/flutter_quill.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'check_button.dart';
|
||||
import 'color_picker.dart';
|
||||
import 'header_button.dart';
|
||||
import 'history_button.dart';
|
||||
import 'link_button.dart';
|
||||
import 'toggle_button.dart';
|
||||
import 'toolbar_icon_button.dart';
|
||||
import 'package:app_flowy/generated/locale_keys.g.dart';
|
||||
|
||||
class EditorToolbar extends StatelessWidget implements PreferredSizeWidget {
|
||||
final List<Widget> children;
|
||||
final double toolBarHeight;
|
||||
final Color? color;
|
||||
|
||||
const EditorToolbar({
|
||||
required this.children,
|
||||
this.toolBarHeight = 46,
|
||||
this.color,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
color: Theme.of(context).canvasColor,
|
||||
constraints: BoxConstraints.tightFor(height: preferredSize.height),
|
||||
child: ToolbarButtonList(buttons: children)
|
||||
.padding(horizontal: 4, vertical: 4),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Size get preferredSize => Size.fromHeight(toolBarHeight);
|
||||
|
||||
factory EditorToolbar.basic({
|
||||
required QuillController controller,
|
||||
double toolbarIconSize = defaultIconSize,
|
||||
OnImagePickCallback? onImagePickCallback,
|
||||
OnVideoPickCallback? onVideoPickCallback,
|
||||
MediaPickSettingSelector? mediaPickSettingSelector,
|
||||
FilePickImpl? filePickImpl,
|
||||
WebImagePickImpl? webImagePickImpl,
|
||||
WebVideoPickImpl? webVideoPickImpl,
|
||||
Key? key,
|
||||
}) {
|
||||
return EditorToolbar(
|
||||
key: key,
|
||||
toolBarHeight: toolbarIconSize * 2,
|
||||
children: [
|
||||
FlowyHistoryButton(
|
||||
icon: Icons.undo_outlined,
|
||||
iconSize: toolbarIconSize,
|
||||
controller: controller,
|
||||
undo: true,
|
||||
tooltipText: LocaleKeys.toolbar_undo.tr(),
|
||||
),
|
||||
FlowyHistoryButton(
|
||||
icon: Icons.redo_outlined,
|
||||
iconSize: toolbarIconSize,
|
||||
controller: controller,
|
||||
undo: false,
|
||||
tooltipText: LocaleKeys.toolbar_redo.tr(),
|
||||
),
|
||||
FlowyToggleStyleButton(
|
||||
attribute: Attribute.bold,
|
||||
normalIcon: 'editor/bold',
|
||||
iconSize: toolbarIconSize,
|
||||
controller: controller,
|
||||
tooltipText: LocaleKeys.toolbar_bold.tr(),
|
||||
),
|
||||
FlowyToggleStyleButton(
|
||||
attribute: Attribute.italic,
|
||||
normalIcon: 'editor/italic',
|
||||
iconSize: toolbarIconSize,
|
||||
controller: controller,
|
||||
tooltipText: LocaleKeys.toolbar_italic.tr(),
|
||||
),
|
||||
FlowyToggleStyleButton(
|
||||
attribute: Attribute.underline,
|
||||
normalIcon: 'editor/underline',
|
||||
iconSize: toolbarIconSize,
|
||||
controller: controller,
|
||||
tooltipText: LocaleKeys.toolbar_underline.tr(),
|
||||
),
|
||||
FlowyToggleStyleButton(
|
||||
attribute: Attribute.strikeThrough,
|
||||
normalIcon: 'editor/strikethrough',
|
||||
iconSize: toolbarIconSize,
|
||||
controller: controller,
|
||||
tooltipText: LocaleKeys.toolbar_strike.tr(),
|
||||
),
|
||||
FlowyColorButton(
|
||||
icon: Icons.format_color_fill,
|
||||
iconSize: toolbarIconSize,
|
||||
controller: controller,
|
||||
background: true,
|
||||
),
|
||||
// FlowyImageButton(
|
||||
// iconSize: toolbarIconSize,
|
||||
// controller: controller,
|
||||
// onImagePickCallback: onImagePickCallback,
|
||||
// filePickImpl: filePickImpl,
|
||||
// webImagePickImpl: webImagePickImpl,
|
||||
// mediaPickSettingSelector: mediaPickSettingSelector,
|
||||
// ),
|
||||
FlowyHeaderStyleButton(
|
||||
controller: controller,
|
||||
iconSize: toolbarIconSize,
|
||||
),
|
||||
FlowyToggleStyleButton(
|
||||
attribute: Attribute.ol,
|
||||
controller: controller,
|
||||
normalIcon: 'editor/numbers',
|
||||
iconSize: toolbarIconSize,
|
||||
tooltipText: LocaleKeys.toolbar_numList.tr(),
|
||||
),
|
||||
FlowyToggleStyleButton(
|
||||
attribute: Attribute.ul,
|
||||
controller: controller,
|
||||
normalIcon: 'editor/bullet_list',
|
||||
iconSize: toolbarIconSize,
|
||||
tooltipText: LocaleKeys.toolbar_bulletList.tr(),
|
||||
),
|
||||
FlowyCheckListButton(
|
||||
attribute: Attribute.unchecked,
|
||||
controller: controller,
|
||||
iconSize: toolbarIconSize,
|
||||
tooltipText: LocaleKeys.toolbar_checkList.tr(),
|
||||
),
|
||||
FlowyToggleStyleButton(
|
||||
attribute: Attribute.inlineCode,
|
||||
controller: controller,
|
||||
normalIcon: 'editor/inline_block',
|
||||
iconSize: toolbarIconSize,
|
||||
tooltipText: LocaleKeys.toolbar_inlineCode.tr(),
|
||||
),
|
||||
FlowyToggleStyleButton(
|
||||
attribute: Attribute.blockQuote,
|
||||
controller: controller,
|
||||
normalIcon: 'editor/quote',
|
||||
iconSize: toolbarIconSize,
|
||||
tooltipText: LocaleKeys.toolbar_quote.tr(),
|
||||
),
|
||||
FlowyLinkStyleButton(
|
||||
controller: controller,
|
||||
iconSize: toolbarIconSize,
|
||||
),
|
||||
FlowyEmojiStyleButton(
|
||||
normalIcon: 'editor/insert_emoticon',
|
||||
controller: controller,
|
||||
tooltipText: "Emoji Picker",
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ToolbarButtonList extends StatefulWidget {
|
||||
const ToolbarButtonList({required this.buttons, Key? key}) : super(key: key);
|
||||
|
||||
final List<Widget> buttons;
|
||||
|
||||
@override
|
||||
ToolbarButtonListState createState() => ToolbarButtonListState();
|
||||
}
|
||||
|
||||
class ToolbarButtonListState extends State<ToolbarButtonList>
|
||||
with WidgetsBindingObserver {
|
||||
final ScrollController _controller = ScrollController();
|
||||
bool _showLeftArrow = false;
|
||||
bool _showRightArrow = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller.addListener(_handleScroll);
|
||||
|
||||
// Listening to the WidgetsBinding instance is necessary so that we can
|
||||
// hide the arrows when the window gets a new size and thus the toolbar
|
||||
// becomes scrollable/unscrollable.
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
|
||||
// Workaround to allow the scroll controller attach to our ListView so that
|
||||
// we can detect if overflow arrows need to be shown on init.
|
||||
Timer.run(_handleScroll);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(
|
||||
builder: (BuildContext context, BoxConstraints constraints) {
|
||||
List<Widget> children = [];
|
||||
double width =
|
||||
(widget.buttons.length + 2) * defaultIconSize * kIconButtonFactor;
|
||||
final isFit = constraints.maxWidth > width;
|
||||
if (!isFit) {
|
||||
children.add(_buildLeftArrow());
|
||||
width = width + 18;
|
||||
}
|
||||
|
||||
children.add(_buildScrollableList(constraints, isFit));
|
||||
|
||||
if (!isFit) {
|
||||
children.add(_buildRightArrow());
|
||||
width = width + 18;
|
||||
}
|
||||
|
||||
return SizedBox(
|
||||
width: min(constraints.maxWidth, width),
|
||||
child: Row(
|
||||
children: children,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeMetrics() => _handleScroll();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _handleScroll() {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_showLeftArrow =
|
||||
_controller.position.minScrollExtent != _controller.position.pixels;
|
||||
_showRightArrow =
|
||||
_controller.position.maxScrollExtent != _controller.position.pixels;
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildLeftArrow() {
|
||||
return SizedBox(
|
||||
width: 8,
|
||||
child: Transform.translate(
|
||||
// Move the icon a few pixels to center it
|
||||
offset: const Offset(-5, 0),
|
||||
child: _showLeftArrow ? const Icon(Icons.arrow_left, size: 18) : null,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// [[sliver: https://medium.com/flutter/slivers-demystified-6ff68ab0296f]]
|
||||
Widget _buildScrollableList(BoxConstraints constraints, bool isFit) {
|
||||
Widget child = Expanded(
|
||||
child: CustomScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
controller: _controller,
|
||||
physics: const ClampingScrollPhysics(),
|
||||
slivers: [
|
||||
SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(BuildContext context, int index) {
|
||||
return widget.buttons[index];
|
||||
},
|
||||
childCount: widget.buttons.length,
|
||||
addAutomaticKeepAlives: false,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (!isFit) {
|
||||
child = ScrollConfiguration(
|
||||
// Remove the glowing effect, as we already have the arrow indicators
|
||||
behavior: _NoGlowBehavior(),
|
||||
// The CustomScrollView is necessary so that the children are not
|
||||
// stretched to the height of the toolbar, https://bit.ly/3uC3bjI
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
return child;
|
||||
}
|
||||
|
||||
Widget _buildRightArrow() {
|
||||
return SizedBox(
|
||||
width: 8,
|
||||
child: Transform.translate(
|
||||
// Move the icon a few pixels to center it
|
||||
offset: const Offset(-5, 0),
|
||||
child: _showRightArrow ? const Icon(Icons.arrow_right, size: 18) : null,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _NoGlowBehavior extends ScrollBehavior {
|
||||
@override
|
||||
Widget buildViewportChrome(BuildContext _, Widget child, AxisDirection __) {
|
||||
return child;
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
import 'package:flowy_infra/image.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flowy_infra/theme.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
const double defaultIconSize = 18;
|
||||
|
||||
class ToolbarIconButton extends StatelessWidget {
|
||||
final double width;
|
||||
final VoidCallback? onPressed;
|
||||
final bool isToggled;
|
||||
final String iconName;
|
||||
final String tooltipText;
|
||||
|
||||
const ToolbarIconButton({
|
||||
Key? key,
|
||||
required this.onPressed,
|
||||
required this.isToggled,
|
||||
required this.width,
|
||||
required this.iconName,
|
||||
required this.tooltipText,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = context.watch<AppTheme>();
|
||||
return FlowyIconButton(
|
||||
iconPadding: const EdgeInsets.symmetric(horizontal: 4, vertical: 4),
|
||||
onPressed: onPressed,
|
||||
width: width,
|
||||
icon: isToggled == true ? svgWidget(iconName, color: Colors.white) : svgWidget(iconName, color: theme.iconColor),
|
||||
fillColor: isToggled == true ? theme.main1 : theme.shader6,
|
||||
hoverColor: isToggled == true ? theme.main1 : theme.hover,
|
||||
tooltipText: tooltipText,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,133 +0,0 @@
|
||||
import 'package:app_flowy/plugins/doc/presentation/style_widgets.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_quill/flutter_quill.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
import 'package:flowy_infra/theme.dart';
|
||||
|
||||
DefaultStyles customStyles(BuildContext context) {
|
||||
const baseSpacing = Tuple2<double, double>(6, 0);
|
||||
|
||||
final theme = context.watch<AppTheme>();
|
||||
final themeData = theme.themeData;
|
||||
final fontFamily = makeFontFamily(themeData);
|
||||
|
||||
final defaultTextStyle = DefaultTextStyle.of(context);
|
||||
final baseStyle = defaultTextStyle.style.copyWith(
|
||||
fontSize: 18,
|
||||
height: 1.3,
|
||||
fontWeight: FontWeight.w300,
|
||||
letterSpacing: 0.6,
|
||||
fontFamily: fontFamily,
|
||||
);
|
||||
|
||||
return DefaultStyles(
|
||||
h1: DefaultTextBlockStyle(
|
||||
defaultTextStyle.style.copyWith(
|
||||
fontSize: 34,
|
||||
color: defaultTextStyle.style.color!.withOpacity(0.70),
|
||||
height: 1.15,
|
||||
fontWeight: FontWeight.w300,
|
||||
),
|
||||
const Tuple2(16, 0),
|
||||
const Tuple2(0, 0),
|
||||
null),
|
||||
h2: DefaultTextBlockStyle(
|
||||
defaultTextStyle.style.copyWith(
|
||||
fontSize: 24,
|
||||
color: defaultTextStyle.style.color!.withOpacity(0.70),
|
||||
height: 1.15,
|
||||
fontWeight: FontWeight.normal,
|
||||
),
|
||||
const Tuple2(8, 0),
|
||||
const Tuple2(0, 0),
|
||||
null),
|
||||
h3: DefaultTextBlockStyle(
|
||||
defaultTextStyle.style.copyWith(
|
||||
fontSize: 20,
|
||||
color: defaultTextStyle.style.color!.withOpacity(0.70),
|
||||
height: 1.25,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
const Tuple2(8, 0),
|
||||
const Tuple2(0, 0),
|
||||
null),
|
||||
paragraph: DefaultTextBlockStyle(
|
||||
baseStyle, const Tuple2(10, 0), const Tuple2(0, 0), null),
|
||||
bold: const TextStyle(fontWeight: FontWeight.bold),
|
||||
italic: const TextStyle(fontStyle: FontStyle.italic),
|
||||
small: const TextStyle(fontSize: 12, color: Colors.black45),
|
||||
underline: const TextStyle(decoration: TextDecoration.underline),
|
||||
strikeThrough: const TextStyle(decoration: TextDecoration.lineThrough),
|
||||
inlineCode: TextStyle(
|
||||
color: Colors.blue.shade900.withOpacity(0.9),
|
||||
fontFamily: fontFamily,
|
||||
fontSize: 13,
|
||||
),
|
||||
link: TextStyle(
|
||||
color: themeData.colorScheme.secondary,
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
color: theme.textColor,
|
||||
placeHolder: DefaultTextBlockStyle(
|
||||
defaultTextStyle.style.copyWith(
|
||||
fontSize: 20,
|
||||
height: 1.5,
|
||||
color: Colors.grey.withOpacity(0.6),
|
||||
),
|
||||
const Tuple2(0, 0),
|
||||
const Tuple2(0, 0),
|
||||
null),
|
||||
lists: DefaultListBlockStyle(baseStyle, baseSpacing, const Tuple2(0, 6),
|
||||
null, StyleWidgetBuilder.checkbox(theme)),
|
||||
quote: DefaultTextBlockStyle(
|
||||
TextStyle(color: baseStyle.color!.withOpacity(0.6)),
|
||||
baseSpacing,
|
||||
const Tuple2(6, 2),
|
||||
BoxDecoration(
|
||||
border: Border(
|
||||
left: BorderSide(width: 4, color: theme.shader5),
|
||||
),
|
||||
)),
|
||||
code: DefaultTextBlockStyle(
|
||||
TextStyle(
|
||||
color: Colors.blue.shade900.withOpacity(0.9),
|
||||
fontFamily: fontFamily,
|
||||
fontSize: 13,
|
||||
height: 1.15,
|
||||
),
|
||||
baseSpacing,
|
||||
const Tuple2(0, 0),
|
||||
BoxDecoration(
|
||||
color: Colors.grey.shade50,
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
)),
|
||||
indent: DefaultTextBlockStyle(
|
||||
baseStyle, baseSpacing, const Tuple2(0, 6), null),
|
||||
align: DefaultTextBlockStyle(
|
||||
baseStyle, const Tuple2(0, 0), const Tuple2(0, 0), null),
|
||||
leading: DefaultTextBlockStyle(
|
||||
baseStyle, const Tuple2(0, 0), const Tuple2(0, 0), null),
|
||||
sizeSmall: const TextStyle(fontSize: 10),
|
||||
sizeLarge: const TextStyle(fontSize: 18),
|
||||
sizeHuge: const TextStyle(fontSize: 22));
|
||||
}
|
||||
|
||||
String makeFontFamily(ThemeData themeData) {
|
||||
String fontFamily;
|
||||
switch (themeData.platform) {
|
||||
case TargetPlatform.iOS:
|
||||
case TargetPlatform.macOS:
|
||||
fontFamily = 'Mulish';
|
||||
break;
|
||||
case TargetPlatform.android:
|
||||
case TargetPlatform.fuchsia:
|
||||
case TargetPlatform.windows:
|
||||
case TargetPlatform.linux:
|
||||
fontFamily = 'Roboto Mono';
|
||||
break;
|
||||
default:
|
||||
throw UnimplementedError();
|
||||
}
|
||||
return fontFamily;
|
||||
}
|
||||
+17
-15
@@ -1,7 +1,7 @@
|
||||
import 'dart:convert';
|
||||
import 'package:app_flowy/plugins/trash/application/trash_service.dart';
|
||||
import 'package:app_flowy/workspace/application/view/view_listener.dart';
|
||||
import 'package:app_flowy/plugins/doc/application/doc_service.dart';
|
||||
import 'package:app_flowy/plugins/document/application/doc_service.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart'
|
||||
show EditorState, Document, Transaction;
|
||||
import 'package:flowy_sdk/protobuf/flowy-folder/trash.pb.dart';
|
||||
@@ -17,19 +17,19 @@ part 'doc_bloc.freezed.dart';
|
||||
|
||||
class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
|
||||
final ViewPB view;
|
||||
final DocumentService service;
|
||||
final DocumentService _documentService;
|
||||
|
||||
final ViewListener listener;
|
||||
final TrashService trashService;
|
||||
final ViewListener _listener;
|
||||
final TrashService _trashService;
|
||||
late EditorState editorState;
|
||||
StreamSubscription? _subscription;
|
||||
|
||||
DocumentBloc({
|
||||
required this.view,
|
||||
required this.service,
|
||||
required this.listener,
|
||||
required this.trashService,
|
||||
}) : super(DocumentState.initial()) {
|
||||
}) : _documentService = DocumentService(),
|
||||
_listener = ViewListener(view: view),
|
||||
_trashService = TrashService(),
|
||||
super(DocumentState.initial()) {
|
||||
on<DocumentEvent>((event, emit) async {
|
||||
await event.map(
|
||||
initial: (Initial value) async {
|
||||
@@ -43,7 +43,7 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
|
||||
emit(state.copyWith(isDeleted: false));
|
||||
},
|
||||
deletePermanently: (DeletePermanently value) async {
|
||||
final result = await trashService
|
||||
final result = await _trashService
|
||||
.deleteViews([Tuple2(view.id, TrashType.TrashView)]);
|
||||
|
||||
final newState = result.fold(
|
||||
@@ -51,7 +51,7 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
|
||||
emit(newState);
|
||||
},
|
||||
restorePage: (RestorePage value) async {
|
||||
final result = await trashService.putback(view.id);
|
||||
final result = await _trashService.putback(view.id);
|
||||
final newState = result.fold(
|
||||
(l) => state.copyWith(isDeleted: false), (r) => state);
|
||||
emit(newState);
|
||||
@@ -62,18 +62,18 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
await listener.stop();
|
||||
await _listener.stop();
|
||||
|
||||
if (_subscription != null) {
|
||||
await _subscription?.cancel();
|
||||
}
|
||||
|
||||
await service.closeDocument(docId: view.id);
|
||||
await _documentService.closeDocument(docId: view.id);
|
||||
return super.close();
|
||||
}
|
||||
|
||||
Future<void> _initial(Initial value, Emitter<DocumentState> emit) async {
|
||||
final result = await service.openDocument(view: view);
|
||||
final result = await _documentService.openDocument(view: view);
|
||||
result.fold(
|
||||
(block) {
|
||||
final document = Document.fromJson(jsonDecode(block.snapshot));
|
||||
@@ -96,7 +96,7 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
|
||||
}
|
||||
|
||||
void _listenOnViewChange() {
|
||||
listener.start(
|
||||
_listener.start(
|
||||
onViewDeleted: (result) {
|
||||
result.fold(
|
||||
(view) => add(const DocumentEvent.deleted()),
|
||||
@@ -115,7 +115,9 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
|
||||
void _listenOnDocumentChange() {
|
||||
_subscription = editorState.transactionStream.listen((transaction) {
|
||||
final json = jsonEncode(TransactionAdaptor(transaction).toJson());
|
||||
service.applyEdit(docId: view.id, operations: json).then((result) {
|
||||
_documentService
|
||||
.applyEdit(docId: view.id, operations: json)
|
||||
.then((result) {
|
||||
result.fold(
|
||||
(l) => null,
|
||||
(err) => Log.error(err),
|
||||
File renamed without changes.
File renamed without changes.
+3
-3
@@ -1,14 +1,14 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:app_flowy/plugins/doc/application/share_service.dart';
|
||||
import 'package:app_flowy/workspace/application/markdown/document_markdown.dart';
|
||||
import 'package:app_flowy/plugins/document/application/share_service.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-document/entities.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart' show Document;
|
||||
import 'package:appflowy_editor/appflowy_editor.dart'
|
||||
show Document, documentToMarkdown;
|
||||
part 'share_bloc.freezed.dart';
|
||||
|
||||
class DocShareBloc extends Bloc<DocShareEvent, DocShareState> {
|
||||
File renamed without changes.
+12
-27
@@ -4,8 +4,7 @@ import 'package:app_flowy/generated/locale_keys.g.dart';
|
||||
import 'package:app_flowy/plugins/util.dart';
|
||||
import 'package:app_flowy/startup/plugin/plugin.dart';
|
||||
import 'package:app_flowy/startup/startup.dart';
|
||||
import 'package:app_flowy/workspace/application/appearance.dart';
|
||||
import 'package:app_flowy/plugins/doc/application/share_bloc.dart';
|
||||
import 'package:app_flowy/plugins/document/application/share_bloc.dart';
|
||||
import 'package:app_flowy/workspace/presentation/home/home_stack.dart';
|
||||
import 'package:app_flowy/workspace/presentation/home/toast.dart';
|
||||
import 'package:app_flowy/workspace/presentation/widgets/left_bar_item.dart';
|
||||
@@ -15,8 +14,6 @@ import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:clipboard/clipboard.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flowy_infra/size.dart';
|
||||
import 'package:flowy_infra/theme.dart';
|
||||
import 'package:flowy_infra_ui/widget/rounded_button.dart';
|
||||
import 'package:flowy_sdk/log.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
|
||||
@@ -24,7 +21,6 @@ import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-document/entities.pb.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'document_page.dart';
|
||||
|
||||
@@ -41,6 +37,9 @@ class DocumentPluginBuilder extends PluginBuilder {
|
||||
@override
|
||||
String get menuName => LocaleKeys.document_menuName.tr();
|
||||
|
||||
@override
|
||||
String get menuIcon => "editor/documents";
|
||||
|
||||
@override
|
||||
PluginType get pluginType => PluginType.editor;
|
||||
|
||||
@@ -129,23 +128,13 @@ class DocumentShareButton extends StatelessWidget {
|
||||
);
|
||||
},
|
||||
child: BlocBuilder<DocShareBloc, DocShareState>(
|
||||
builder: (context, state) {
|
||||
return ChangeNotifierProvider.value(
|
||||
value: Provider.of<AppearanceSetting>(context, listen: true),
|
||||
child: Selector<AppearanceSetting, Locale>(
|
||||
selector: (ctx, notifier) => notifier.locale,
|
||||
builder: (ctx, _, child) => ConstrainedBox(
|
||||
constraints: const BoxConstraints.expand(
|
||||
height: 30,
|
||||
width: 100,
|
||||
),
|
||||
child: ShareActionList(
|
||||
view: view,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
builder: (context, state) => ConstrainedBox(
|
||||
constraints: const BoxConstraints.expand(
|
||||
height: 30,
|
||||
width: 100,
|
||||
),
|
||||
child: ShareActionList(view: view),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -177,7 +166,6 @@ class ShareActionList extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = context.watch<AppTheme>();
|
||||
final docShareBloc = context.read<DocShareBloc>();
|
||||
return PopoverActionList<ShareActionWrapper>(
|
||||
direction: PopoverDirection.bottomWithCenterAligned,
|
||||
@@ -187,9 +175,6 @@ class ShareActionList extends StatelessWidget {
|
||||
buildChild: (controller) {
|
||||
return RoundedTextButton(
|
||||
title: LocaleKeys.shareAction_buttonText.tr(),
|
||||
fontSize: 12,
|
||||
borderRadius: Corners.s6Border,
|
||||
color: theme.main1,
|
||||
onPressed: () => controller.show(),
|
||||
);
|
||||
},
|
||||
@@ -228,7 +213,7 @@ class ShareActionWrapper extends ActionCell {
|
||||
ShareActionWrapper(this.inner);
|
||||
|
||||
@override
|
||||
Widget? icon(Color iconColor) => null;
|
||||
Widget? leftIcon(Color iconColor) => null;
|
||||
|
||||
@override
|
||||
String get name => inner.name;
|
||||
+22
-8
@@ -1,7 +1,7 @@
|
||||
import 'package:app_flowy/plugins/doc/editor_styles.dart';
|
||||
import 'package:app_flowy/plugins/doc/presentation/plugins/horizontal_rule_node_widget.dart';
|
||||
import 'package:app_flowy/plugins/document/editor_styles.dart';
|
||||
import 'package:app_flowy/plugins/document/presentation/plugins/horizontal_rule_node_widget.dart';
|
||||
import 'package:app_flowy/startup/startup.dart';
|
||||
import 'package:app_flowy/plugins/doc/presentation/banner.dart';
|
||||
import 'package:app_flowy/plugins/document/presentation/banner.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flowy_infra_ui/widget/error_page.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart';
|
||||
@@ -30,7 +30,7 @@ class _DocumentPageState extends State<DocumentPage> {
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
// The appflowy editor use Intl as locatization, set the default language as fallback.
|
||||
// The appflowy editor use Intl as localization, set the default language as fallback.
|
||||
Intl.defaultLocale = 'en_US';
|
||||
documentBloc = getIt<DocumentBloc>(param1: super.widget.view)
|
||||
..add(const DocumentEvent.initial());
|
||||
@@ -77,7 +77,9 @@ class _DocumentPageState extends State<DocumentPage> {
|
||||
children: [
|
||||
if (state.isDeleted) _renderBanner(context),
|
||||
// AppFlowy Editor
|
||||
_renderAppFlowyEditor(context.read<DocumentBloc>().editorState),
|
||||
_renderAppFlowyEditor(
|
||||
context.read<DocumentBloc>().editorState,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -93,19 +95,31 @@ class _DocumentPageState extends State<DocumentPage> {
|
||||
}
|
||||
|
||||
Widget _renderAppFlowyEditor(EditorState editorState) {
|
||||
final theme = Theme.of(context);
|
||||
final editorMaxWidth = MediaQuery.of(context).size.width * 0.6;
|
||||
final editor = AppFlowyEditor(
|
||||
editorState: editorState,
|
||||
editorStyle: customEditorStyle(context),
|
||||
autoFocus: editorState.document.isEmpty,
|
||||
customBuilders: {
|
||||
'horizontal_rule': HorizontalRuleWidgetBuilder(),
|
||||
},
|
||||
shortcutEvents: [
|
||||
insertHorizontalRule,
|
||||
],
|
||||
themeData: theme.copyWith(extensions: [
|
||||
...theme.extensions.values,
|
||||
customEditorTheme(context),
|
||||
...customPluginTheme(context),
|
||||
]),
|
||||
);
|
||||
return Expanded(
|
||||
child: SizedBox.expand(
|
||||
child: editor,
|
||||
child: Center(
|
||||
child: Container(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: editorMaxWidth,
|
||||
),
|
||||
child: editor,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
const _baseFontSize = 14.0;
|
||||
|
||||
EditorStyle customEditorTheme(BuildContext context) {
|
||||
var editorStyle = Theme.of(context).brightness == Brightness.dark
|
||||
? EditorStyle.dark
|
||||
: EditorStyle.light;
|
||||
editorStyle = editorStyle.copyWith(
|
||||
padding: const EdgeInsets.all(0),
|
||||
textStyle: editorStyle.textStyle?.copyWith(
|
||||
fontFamily: 'poppins',
|
||||
fontSize: _baseFontSize,
|
||||
),
|
||||
placeholderTextStyle: editorStyle.placeholderTextStyle?.copyWith(
|
||||
fontFamily: 'poppins',
|
||||
fontSize: _baseFontSize,
|
||||
),
|
||||
bold: editorStyle.bold?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
);
|
||||
return editorStyle;
|
||||
}
|
||||
|
||||
Iterable<ThemeExtension<dynamic>> customPluginTheme(BuildContext context) {
|
||||
const basePadding = 12.0;
|
||||
var headingPluginStyle = Theme.of(context).brightness == Brightness.dark
|
||||
? HeadingPluginStyle.dark
|
||||
: HeadingPluginStyle.light;
|
||||
headingPluginStyle = headingPluginStyle.copyWith(
|
||||
textStyle: (EditorState editorState, Node node) {
|
||||
final headingToFontSize = {
|
||||
'h1': _baseFontSize + 12,
|
||||
'h2': _baseFontSize + 8,
|
||||
'h3': _baseFontSize + 4,
|
||||
'h4': _baseFontSize,
|
||||
'h5': _baseFontSize,
|
||||
'h6': _baseFontSize,
|
||||
};
|
||||
final fontSize =
|
||||
headingToFontSize[node.attributes.heading] ?? _baseFontSize;
|
||||
return TextStyle(fontSize: fontSize, fontWeight: FontWeight.w600);
|
||||
},
|
||||
padding: (EditorState editorState, Node node) {
|
||||
final headingToPadding = {
|
||||
'h1': basePadding + 6,
|
||||
'h2': basePadding + 4,
|
||||
'h3': basePadding + 2,
|
||||
'h4': basePadding,
|
||||
'h5': basePadding,
|
||||
'h6': basePadding,
|
||||
};
|
||||
final padding = headingToPadding[node.attributes.heading] ?? basePadding;
|
||||
return EdgeInsets.only(bottom: padding);
|
||||
},
|
||||
);
|
||||
final pluginTheme = Theme.of(context).brightness == Brightness.dark
|
||||
? darkPlguinStyleExtension
|
||||
: lightPlguinStyleExtension;
|
||||
return pluginTheme.toList()
|
||||
..removeWhere((element) => element is HeadingPluginStyle)
|
||||
..add(headingPluginStyle);
|
||||
}
|
||||
+13
-14
@@ -1,11 +1,9 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/size.dart';
|
||||
import 'package:flowy_infra/theme.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flowy_infra_ui/widget/buttons/base_styled_button.dart';
|
||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:app_flowy/generated/locale_keys.g.dart';
|
||||
|
||||
class DocumentBanner extends StatelessWidget {
|
||||
@@ -17,12 +15,11 @@ class DocumentBanner extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = context.watch<AppTheme>();
|
||||
return ConstrainedBox(
|
||||
constraints: const BoxConstraints(minHeight: 60),
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
color: theme.main1,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
child: FittedBox(
|
||||
alignment: Alignment.center,
|
||||
fit: BoxFit.scaleDown,
|
||||
@@ -36,30 +33,32 @@ class DocumentBanner extends StatelessWidget {
|
||||
minHeight: 40,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
bgColor: Colors.transparent,
|
||||
hoverColor: theme.main2,
|
||||
downColor: theme.main1,
|
||||
hoverColor: Theme.of(context).colorScheme.primary,
|
||||
downColor: Theme.of(context).colorScheme.primaryContainer,
|
||||
outlineColor: Colors.white,
|
||||
borderRadius: Corners.s8Border,
|
||||
onPressed: onRestore,
|
||||
child: FlowyText.medium(
|
||||
LocaleKeys.deletePagePrompt_restore.tr(),
|
||||
color: Colors.white,
|
||||
fontSize: 14)),
|
||||
LocaleKeys.deletePagePrompt_restore.tr(),
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
fontSize: 14,
|
||||
)),
|
||||
const HSpace(20),
|
||||
BaseStyledButton(
|
||||
minWidth: 220,
|
||||
minHeight: 40,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
bgColor: Colors.transparent,
|
||||
hoverColor: theme.main2,
|
||||
downColor: theme.main1,
|
||||
hoverColor: Theme.of(context).colorScheme.primaryContainer,
|
||||
downColor: Theme.of(context).colorScheme.primary,
|
||||
outlineColor: Colors.white,
|
||||
borderRadius: Corners.s8Border,
|
||||
onPressed: onDelete,
|
||||
child: FlowyText.medium(
|
||||
LocaleKeys.deletePagePrompt_deletePermanent.tr(),
|
||||
color: Colors.white,
|
||||
fontSize: 14)),
|
||||
LocaleKeys.deletePagePrompt_deletePermanent.tr(),
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
fontSize: 14,
|
||||
)),
|
||||
],
|
||||
),
|
||||
),
|
||||
+4
-2
@@ -38,9 +38,11 @@ ShortcutEventHandler _insertHorzaontalRule = (editorState, event) {
|
||||
|
||||
SelectionMenuItem horizontalRuleMenuItem = SelectionMenuItem(
|
||||
name: () => 'Horizontal rule',
|
||||
icon: const Icon(
|
||||
icon: (editorState, onSelected) => Icon(
|
||||
Icons.horizontal_rule,
|
||||
color: Colors.black,
|
||||
color: onSelected
|
||||
? editorState.editorStyle.selectionMenuItemSelectedIconColor
|
||||
: editorState.editorStyle.selectionMenuItemIconColor,
|
||||
size: 18.0,
|
||||
),
|
||||
keywords: ['horizontal rule'],
|
||||
@@ -13,7 +13,7 @@ class GridBlockCache {
|
||||
late GridRowCache _rowCache;
|
||||
late GridBlockListener _listener;
|
||||
|
||||
List<RowInfo> get rows => _rowCache.rows;
|
||||
List<RowInfo> get rows => _rowCache.visibleRows;
|
||||
GridRowCache get rowCache => _rowCache;
|
||||
|
||||
GridBlockCache({
|
||||
@@ -30,7 +30,7 @@ class GridBlockCache {
|
||||
_listener = GridBlockListener(blockId: block.id);
|
||||
_listener.start((result) {
|
||||
result.fold(
|
||||
(changesets) => _rowCache.applyChangesets(changesets),
|
||||
(changeset) => _rowCache.applyChangesets(changeset),
|
||||
(err) => Log.error(err),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -7,11 +7,12 @@ import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/block_entities.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/dart_notification.pb.dart';
|
||||
|
||||
typedef GridBlockUpdateNotifierValue = Either<List<GridBlockChangesetPB>, FlowyError>;
|
||||
typedef GridBlockUpdateNotifierValue = Either<GridBlockChangesetPB, FlowyError>;
|
||||
|
||||
class GridBlockListener {
|
||||
final String blockId;
|
||||
PublishNotifier<GridBlockUpdateNotifierValue>? _rowsUpdateNotifier = PublishNotifier();
|
||||
PublishNotifier<GridBlockUpdateNotifierValue>? _rowsUpdateNotifier =
|
||||
PublishNotifier();
|
||||
GridNotificationListener? _listener;
|
||||
|
||||
GridBlockListener({required this.blockId});
|
||||
@@ -29,11 +30,12 @@ class GridBlockListener {
|
||||
_rowsUpdateNotifier?.addPublishListener(onBlockChanged);
|
||||
}
|
||||
|
||||
void _handler(GridNotification ty, Either<Uint8List, FlowyError> result) {
|
||||
void _handler(GridDartNotification ty, Either<Uint8List, FlowyError> result) {
|
||||
switch (ty) {
|
||||
case GridNotification.DidUpdateGridBlock:
|
||||
case GridDartNotification.DidUpdateGridBlock:
|
||||
result.fold(
|
||||
(payload) => _rowsUpdateNotifier?.value = left([GridBlockChangesetPB.fromBuffer(payload)]),
|
||||
(payload) => _rowsUpdateNotifier?.value =
|
||||
left(GridBlockChangesetPB.fromBuffer(payload)),
|
||||
(error) => _rowsUpdateNotifier?.value = right(error),
|
||||
);
|
||||
break;
|
||||
|
||||
@@ -22,9 +22,9 @@ class CellListener {
|
||||
objectId: "$rowId:$fieldId", handler: _handler);
|
||||
}
|
||||
|
||||
void _handler(GridNotification ty, Either<Uint8List, FlowyError> result) {
|
||||
void _handler(GridDartNotification ty, Either<Uint8List, FlowyError> result) {
|
||||
switch (ty) {
|
||||
case GridNotification.DidUpdateCell:
|
||||
case GridDartNotification.DidUpdateCell:
|
||||
result.fold(
|
||||
(payload) => _updateCellNotifier?.value = left(unit),
|
||||
(error) => _updateCellNotifier?.value = right(error),
|
||||
|
||||
+23
-20
@@ -150,22 +150,22 @@ class IGridCellController<T, D> extends Equatable {
|
||||
_fieldNotifier = fieldNotifier,
|
||||
_fieldService = FieldService(
|
||||
gridId: cellId.gridId,
|
||||
fieldId: cellId.fieldContext.id,
|
||||
fieldId: cellId.fieldInfo.id,
|
||||
),
|
||||
_cacheKey = GridCellCacheKey(
|
||||
rowId: cellId.rowId,
|
||||
fieldId: cellId.fieldContext.id,
|
||||
fieldId: cellId.fieldInfo.id,
|
||||
);
|
||||
|
||||
String get gridId => cellId.gridId;
|
||||
|
||||
String get rowId => cellId.rowId;
|
||||
|
||||
String get fieldId => cellId.fieldContext.id;
|
||||
String get fieldId => cellId.fieldInfo.id;
|
||||
|
||||
GridFieldContext get fieldContext => cellId.fieldContext;
|
||||
FieldInfo get fieldInfo => cellId.fieldInfo;
|
||||
|
||||
FieldType get fieldType => cellId.fieldContext.fieldType;
|
||||
FieldType get fieldType => cellId.fieldInfo.fieldType;
|
||||
|
||||
VoidCallback? startListening({
|
||||
required void Function(T?) onCellChanged,
|
||||
@@ -179,7 +179,7 @@ class IGridCellController<T, D> extends Equatable {
|
||||
|
||||
_cellDataNotifier = ValueNotifier(_cellsCache.get(_cacheKey));
|
||||
_cellListener =
|
||||
CellListener(rowId: cellId.rowId, fieldId: cellId.fieldContext.id);
|
||||
CellListener(rowId: cellId.rowId, fieldId: cellId.fieldInfo.id);
|
||||
|
||||
/// 1.Listen on user edit event and load the new cell data if needed.
|
||||
/// For example:
|
||||
@@ -234,7 +234,7 @@ class IGridCellController<T, D> extends Equatable {
|
||||
return data;
|
||||
}
|
||||
|
||||
/// Return the FieldTypeOptionDataPB that can be parsed into corresponding class using the [parser].
|
||||
/// Return the TypeOptionPB that can be parsed into corresponding class using the [parser].
|
||||
/// [PD] is the type that the parser return.
|
||||
Future<Either<PD, FlowyError>>
|
||||
getFieldTypeOption<PD, P extends TypeOptionDataParser>(P parser) {
|
||||
@@ -290,50 +290,53 @@ class IGridCellController<T, D> extends Equatable {
|
||||
});
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
Future<void> dispose() async {
|
||||
if (_isDispose) {
|
||||
Log.error("$this should only dispose once");
|
||||
return;
|
||||
}
|
||||
_isDispose = true;
|
||||
_cellListener?.stop();
|
||||
await _cellListener?.stop();
|
||||
_loadDataOperation?.cancel();
|
||||
_saveDataOperation?.cancel();
|
||||
_cellDataNotifier = null;
|
||||
|
||||
if (_onFieldChangedFn != null) {
|
||||
_fieldNotifier.unregister(_cacheKey, _onFieldChangedFn!);
|
||||
_fieldNotifier.dispose();
|
||||
await _fieldNotifier.dispose();
|
||||
_onFieldChangedFn = null;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object> get props =>
|
||||
[_cellsCache.get(_cacheKey) ?? "", cellId.rowId + cellId.fieldContext.id];
|
||||
[_cellsCache.get(_cacheKey) ?? "", cellId.rowId + cellId.fieldInfo.id];
|
||||
}
|
||||
|
||||
class GridCellFieldNotifierImpl extends IGridCellFieldNotifier {
|
||||
final GridFieldController _cache;
|
||||
OnChangeset? _onChangesetFn;
|
||||
final GridFieldController _fieldController;
|
||||
OnReceiveUpdateFields? _onChangesetFn;
|
||||
|
||||
GridCellFieldNotifierImpl(GridFieldController cache) : _cache = cache;
|
||||
GridCellFieldNotifierImpl(GridFieldController cache)
|
||||
: _fieldController = cache;
|
||||
|
||||
@override
|
||||
void onCellDispose() {
|
||||
if (_onChangesetFn != null) {
|
||||
_cache.removeListener(onChangesetListener: _onChangesetFn!);
|
||||
_fieldController.removeListener(onChangesetListener: _onChangesetFn!);
|
||||
_onChangesetFn = null;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void onCellFieldChanged(void Function(FieldPB p1) callback) {
|
||||
_onChangesetFn = (FieldChangesetPB changeset) {
|
||||
for (final updatedField in changeset.updatedFields) {
|
||||
callback(updatedField);
|
||||
void onCellFieldChanged(void Function(FieldInfo) callback) {
|
||||
_onChangesetFn = (List<FieldInfo> filedInfos) {
|
||||
for (final field in filedInfos) {
|
||||
callback(field);
|
||||
}
|
||||
};
|
||||
_cache.addListener(onChangeset: _onChangesetFn);
|
||||
_fieldController.addListener(
|
||||
onFieldsUpdated: _onChangesetFn,
|
||||
);
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -25,7 +25,7 @@ class GridCellDataLoader<T> {
|
||||
final fut = service.getCell(cellId: cellId);
|
||||
return fut.then(
|
||||
(result) => result.fold(
|
||||
(GridCellPB cell) {
|
||||
(CellPB cell) {
|
||||
try {
|
||||
return parser.parserData(cell.data);
|
||||
} catch (e, s) {
|
||||
|
||||
+8
-5
@@ -29,10 +29,12 @@ class CellDataPersistence implements IGridCellDataPersistence<String> {
|
||||
|
||||
@freezed
|
||||
class CalendarData with _$CalendarData {
|
||||
const factory CalendarData({required DateTime date, String? time}) = _CalendarData;
|
||||
const factory CalendarData({required DateTime date, String? time}) =
|
||||
_CalendarData;
|
||||
}
|
||||
|
||||
class DateCellDataPersistence implements IGridCellDataPersistence<CalendarData> {
|
||||
class DateCellDataPersistence
|
||||
implements IGridCellDataPersistence<CalendarData> {
|
||||
final GridCellIdentifier cellId;
|
||||
DateCellDataPersistence({
|
||||
required this.cellId,
|
||||
@@ -40,10 +42,11 @@ class DateCellDataPersistence implements IGridCellDataPersistence<CalendarData>
|
||||
|
||||
@override
|
||||
Future<Option<FlowyError>> save(CalendarData data) {
|
||||
var payload = DateChangesetPayloadPB.create()..cellIdentifier = _makeCellIdPayload(cellId);
|
||||
var payload = DateChangesetPB.create()..cellPath = _makeCellPath(cellId);
|
||||
|
||||
final date = (data.date.millisecondsSinceEpoch ~/ 1000).toString();
|
||||
payload.date = date;
|
||||
payload.isUtc = data.date.isUtc;
|
||||
|
||||
if (data.time != null) {
|
||||
payload.time = data.time!;
|
||||
@@ -58,8 +61,8 @@ class DateCellDataPersistence implements IGridCellDataPersistence<CalendarData>
|
||||
}
|
||||
}
|
||||
|
||||
GridCellIdPB _makeCellIdPayload(GridCellIdentifier cellId) {
|
||||
return GridCellIdPB.create()
|
||||
CellPathPB _makeCellPath(GridCellIdentifier cellId) {
|
||||
return CellPathPB.create()
|
||||
..gridId = cellId.gridId
|
||||
..fieldId = cellId.fieldId
|
||||
..rowId = cellId.rowId;
|
||||
|
||||
+2
-2
@@ -1,10 +1,10 @@
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
|
||||
import 'package:app_flowy/plugins/grid/application/field/field_controller.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import 'cell_service.dart';
|
||||
|
||||
abstract class IGridCellFieldNotifier {
|
||||
void onCellFieldChanged(void Function(FieldPB) callback);
|
||||
void onCellFieldChanged(void Function(FieldInfo) callback);
|
||||
void onCellDispose();
|
||||
}
|
||||
|
||||
|
||||
@@ -42,10 +42,10 @@ class CellService {
|
||||
return GridEventUpdateCell(payload).send();
|
||||
}
|
||||
|
||||
Future<Either<GridCellPB, FlowyError>> getCell({
|
||||
Future<Either<CellPB, FlowyError>> getCell({
|
||||
required GridCellIdentifier cellId,
|
||||
}) {
|
||||
final payload = GridCellIdPB.create()
|
||||
final payload = CellPathPB.create()
|
||||
..gridId = cellId.gridId
|
||||
..fieldId = cellId.fieldId
|
||||
..rowId = cellId.rowId;
|
||||
@@ -60,17 +60,17 @@ class GridCellIdentifier with _$GridCellIdentifier {
|
||||
const factory GridCellIdentifier({
|
||||
required String gridId,
|
||||
required String rowId,
|
||||
required GridFieldContext fieldContext,
|
||||
required FieldInfo fieldInfo,
|
||||
}) = _GridCellIdentifier;
|
||||
|
||||
// ignore: unused_element
|
||||
const GridCellIdentifier._();
|
||||
|
||||
String get fieldId => fieldContext.id;
|
||||
String get fieldId => fieldInfo.id;
|
||||
|
||||
FieldType get fieldType => fieldContext.fieldType;
|
||||
FieldType get fieldType => fieldInfo.fieldType;
|
||||
|
||||
ValueKey key() {
|
||||
return ValueKey("$rowId$fieldId${fieldContext.fieldType}");
|
||||
return ValueKey("$rowId$fieldId${fieldInfo.fieldType}");
|
||||
}
|
||||
}
|
||||
@@ -37,7 +37,7 @@ class CheckboxCellBloc extends Bloc<CheckboxCellEvent, CheckboxCellState> {
|
||||
_onCellChangedFn = null;
|
||||
}
|
||||
|
||||
cellController.dispose();
|
||||
await cellController.dispose();
|
||||
return super.close();
|
||||
}
|
||||
|
||||
|
||||
@@ -139,7 +139,7 @@ class DateCalBloc extends Bloc<DateCalEvent, DateCalState> {
|
||||
cellController.removeListener(_onCellChangedFn!);
|
||||
_onCellChangedFn = null;
|
||||
}
|
||||
cellController.dispose();
|
||||
await cellController.dispose();
|
||||
return super.close();
|
||||
}
|
||||
|
||||
@@ -176,7 +176,7 @@ class DateCalBloc extends Bloc<DateCalEvent, DateCalState> {
|
||||
|
||||
final result = await FieldService.updateFieldTypeOption(
|
||||
gridId: cellController.gridId,
|
||||
fieldId: cellController.fieldContext.id,
|
||||
fieldId: cellController.fieldInfo.id,
|
||||
typeOptionData: newDateTypeOption.writeToBuffer(),
|
||||
);
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ class DateCellBloc extends Bloc<DateCellEvent, DateCellState> {
|
||||
cellController.removeListener(_onCellChangedFn!);
|
||||
_onCellChangedFn = null;
|
||||
}
|
||||
cellController.dispose();
|
||||
await cellController.dispose();
|
||||
return super.close();
|
||||
}
|
||||
|
||||
@@ -58,14 +58,14 @@ class DateCellState with _$DateCellState {
|
||||
const factory DateCellState({
|
||||
required DateCellDataPB? data,
|
||||
required String dateStr,
|
||||
required GridFieldContext fieldContext,
|
||||
required FieldInfo fieldInfo,
|
||||
}) = _DateCellState;
|
||||
|
||||
factory DateCellState.initial(GridDateCellController context) {
|
||||
final cellData = context.getCellData();
|
||||
|
||||
return DateCellState(
|
||||
fieldContext: context.fieldContext,
|
||||
fieldInfo: context.fieldInfo,
|
||||
data: cellData,
|
||||
dateStr: _dateStrFromCellData(cellData),
|
||||
);
|
||||
|
||||
@@ -46,7 +46,7 @@ class NumberCellBloc extends Bloc<NumberCellEvent, NumberCellState> {
|
||||
cellController.removeListener(_onCellChangedFn!);
|
||||
_onCellChangedFn = null;
|
||||
}
|
||||
cellController.dispose();
|
||||
await cellController.dispose();
|
||||
return super.close();
|
||||
}
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ class SelectOptionCellBloc
|
||||
cellController.removeListener(_onCellChangedFn!);
|
||||
_onCellChangedFn = null;
|
||||
}
|
||||
cellController.dispose();
|
||||
await cellController.dispose();
|
||||
return super.close();
|
||||
}
|
||||
|
||||
|
||||
@@ -81,7 +81,7 @@ class SelectOptionCellEditorBloc
|
||||
@override
|
||||
Future<void> close() async {
|
||||
_delayOperation?.cancel();
|
||||
cellController.dispose();
|
||||
await cellController.dispose();
|
||||
return super.close();
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ class SelectOptionService {
|
||||
SelectOptionService({required this.cellId});
|
||||
|
||||
String get gridId => cellId.gridId;
|
||||
String get fieldId => cellId.fieldContext.id;
|
||||
String get fieldId => cellId.fieldInfo.id;
|
||||
String get rowId => cellId.rowId;
|
||||
|
||||
Future<Either<Unit, FlowyError>> create({required String name}) {
|
||||
@@ -21,11 +21,11 @@ class SelectOptionService {
|
||||
(result) {
|
||||
return result.fold(
|
||||
(option) {
|
||||
final cellIdentifier = GridCellIdPB.create()
|
||||
final cellIdentifier = CellPathPB.create()
|
||||
..gridId = gridId
|
||||
..fieldId = fieldId
|
||||
..rowId = rowId;
|
||||
final payload = SelectOptionChangesetPayloadPB.create()
|
||||
final payload = SelectOptionChangesetPB.create()
|
||||
..insertOptions.add(option)
|
||||
..cellIdentifier = cellIdentifier;
|
||||
return GridEventUpdateSelectOption(payload).send();
|
||||
@@ -39,7 +39,7 @@ class SelectOptionService {
|
||||
Future<Either<Unit, FlowyError>> update({
|
||||
required SelectOptionPB option,
|
||||
}) {
|
||||
final payload = SelectOptionChangesetPayloadPB.create()
|
||||
final payload = SelectOptionChangesetPB.create()
|
||||
..updateOptions.add(option)
|
||||
..cellIdentifier = _cellIdentifier();
|
||||
return GridEventUpdateSelectOption(payload).send();
|
||||
@@ -47,7 +47,7 @@ class SelectOptionService {
|
||||
|
||||
Future<Either<Unit, FlowyError>> delete(
|
||||
{required Iterable<SelectOptionPB> options}) {
|
||||
final payload = SelectOptionChangesetPayloadPB.create()
|
||||
final payload = SelectOptionChangesetPB.create()
|
||||
..deleteOptions.addAll(options)
|
||||
..cellIdentifier = _cellIdentifier();
|
||||
|
||||
@@ -55,7 +55,7 @@ class SelectOptionService {
|
||||
}
|
||||
|
||||
Future<Either<SelectOptionCellDataPB, FlowyError>> getOptionContext() {
|
||||
final payload = GridCellIdPB.create()
|
||||
final payload = CellPathPB.create()
|
||||
..gridId = gridId
|
||||
..fieldId = fieldId
|
||||
..rowId = rowId;
|
||||
@@ -65,7 +65,7 @@ class SelectOptionService {
|
||||
|
||||
Future<Either<void, FlowyError>> select(
|
||||
{required Iterable<String> optionIds}) {
|
||||
final payload = SelectOptionCellChangesetPayloadPB.create()
|
||||
final payload = SelectOptionCellChangesetPB.create()
|
||||
..cellIdentifier = _cellIdentifier()
|
||||
..insertOptionIds.addAll(optionIds);
|
||||
return GridEventUpdateSelectOptionCell(payload).send();
|
||||
@@ -73,14 +73,14 @@ class SelectOptionService {
|
||||
|
||||
Future<Either<void, FlowyError>> unSelect(
|
||||
{required Iterable<String> optionIds}) {
|
||||
final payload = SelectOptionCellChangesetPayloadPB.create()
|
||||
final payload = SelectOptionCellChangesetPB.create()
|
||||
..cellIdentifier = _cellIdentifier()
|
||||
..deleteOptionIds.addAll(optionIds);
|
||||
return GridEventUpdateSelectOptionCell(payload).send();
|
||||
}
|
||||
|
||||
GridCellIdPB _cellIdentifier() {
|
||||
return GridCellIdPB.create()
|
||||
CellPathPB _cellIdentifier() {
|
||||
return CellPathPB.create()
|
||||
..gridId = gridId
|
||||
..fieldId = fieldId
|
||||
..rowId = rowId;
|
||||
|
||||
@@ -35,7 +35,7 @@ class TextCellBloc extends Bloc<TextCellEvent, TextCellState> {
|
||||
cellController.removeListener(_onCellChangedFn!);
|
||||
_onCellChangedFn = null;
|
||||
}
|
||||
cellController.dispose();
|
||||
await cellController.dispose();
|
||||
return super.close();
|
||||
}
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ class URLCellBloc extends Bloc<URLCellEvent, URLCellState> {
|
||||
cellController.removeListener(_onCellChangedFn!);
|
||||
_onCellChangedFn = null;
|
||||
}
|
||||
cellController.dispose();
|
||||
await cellController.dispose();
|
||||
return super.close();
|
||||
}
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ class URLCellEditorBloc extends Bloc<URLCellEditorEvent, URLCellEditorState> {
|
||||
cellController.removeListener(_onCellChangedFn!);
|
||||
_onCellChangedFn = null;
|
||||
}
|
||||
cellController.dispose();
|
||||
await cellController.dispose();
|
||||
return super.close();
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ class FieldActionSheetBloc
|
||||
),
|
||||
super(
|
||||
FieldActionSheetState.initial(
|
||||
FieldTypeOptionDataPB.create()..field_2 = fieldCellContext.field,
|
||||
TypeOptionPB.create()..field_2 = fieldCellContext.field,
|
||||
),
|
||||
) {
|
||||
on<FieldActionSheetEvent>(
|
||||
@@ -85,12 +85,12 @@ class FieldActionSheetEvent with _$FieldActionSheetEvent {
|
||||
@freezed
|
||||
class FieldActionSheetState with _$FieldActionSheetState {
|
||||
const factory FieldActionSheetState({
|
||||
required FieldTypeOptionDataPB fieldTypeOptionData,
|
||||
required TypeOptionPB fieldTypeOptionData,
|
||||
required String errorText,
|
||||
required String fieldName,
|
||||
}) = _FieldActionSheetState;
|
||||
|
||||
factory FieldActionSheetState.initial(FieldTypeOptionDataPB data) =>
|
||||
factory FieldActionSheetState.initial(TypeOptionPB data) =>
|
||||
FieldActionSheetState(
|
||||
fieldTypeOptionData: data,
|
||||
errorText: '',
|
||||
|
||||
@@ -1,22 +1,26 @@
|
||||
import 'dart:collection';
|
||||
import 'package:app_flowy/plugins/grid/application/field/grid_listener.dart';
|
||||
import 'package:app_flowy/plugins/grid/application/filter/filter_listener.dart';
|
||||
import 'package:app_flowy/plugins/grid/application/filter/filter_service.dart';
|
||||
import 'package:app_flowy/plugins/grid/application/grid_service.dart';
|
||||
import 'package:app_flowy/plugins/grid/application/setting/setting_listener.dart';
|
||||
import 'package:app_flowy/plugins/grid/application/setting/setting_service.dart';
|
||||
import 'package:app_flowy/plugins/grid/presentation/widgets/filter/filter_info.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:flowy_sdk/log.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/group.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/setting_entities.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/util.pb.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import '../row/row_cache.dart';
|
||||
|
||||
class _GridFieldNotifier extends ChangeNotifier {
|
||||
List<GridFieldContext> _fieldContexts = [];
|
||||
List<FieldInfo> _fieldInfos = [];
|
||||
|
||||
set fieldContexts(List<GridFieldContext> fieldContexts) {
|
||||
_fieldContexts = fieldContexts;
|
||||
set fieldInfos(List<FieldInfo> fieldInfos) {
|
||||
_fieldInfos = fieldInfos;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@@ -24,91 +28,225 @@ class _GridFieldNotifier extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
List<GridFieldContext> get fieldContexts => _fieldContexts;
|
||||
List<FieldInfo> get fieldInfos => _fieldInfos;
|
||||
}
|
||||
|
||||
typedef OnChangeset = void Function(FieldChangesetPB);
|
||||
typedef OnReceiveFields = void Function(List<GridFieldContext>);
|
||||
class _GridFilterNotifier extends ChangeNotifier {
|
||||
List<FilterInfo> _filters = [];
|
||||
|
||||
set filters(List<FilterInfo> filters) {
|
||||
_filters = filters;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void notify() {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
List<FilterInfo> get filters => _filters;
|
||||
}
|
||||
|
||||
typedef OnReceiveUpdateFields = void Function(List<FieldInfo>);
|
||||
typedef OnReceiveFields = void Function(List<FieldInfo>);
|
||||
typedef OnReceiveFilters = void Function(List<FilterInfo>);
|
||||
|
||||
class GridFieldController {
|
||||
final String gridId;
|
||||
// Listeners
|
||||
final GridFieldsListener _fieldListener;
|
||||
final SettingListener _settingListener;
|
||||
final Map<OnReceiveFields, VoidCallback> _fieldCallbackMap = {};
|
||||
final Map<OnChangeset, OnChangeset> _changesetCallbackMap = {};
|
||||
final FiltersListener _filterListener;
|
||||
|
||||
// FFI services
|
||||
final GridFFIService _gridFFIService;
|
||||
final SettingFFIService _settingFFIService;
|
||||
final FilterFFIService _filterFFIService;
|
||||
|
||||
// Field callbacks
|
||||
final Map<OnReceiveFields, VoidCallback> _fieldCallbacks = {};
|
||||
_GridFieldNotifier? _fieldNotifier = _GridFieldNotifier();
|
||||
final Map<String, GridGroupConfigurationPB> _configurationByFieldId = {};
|
||||
|
||||
List<GridFieldContext> get fieldContexts =>
|
||||
[..._fieldNotifier?.fieldContexts ?? []];
|
||||
// Field updated callbacks
|
||||
final Map<OnReceiveUpdateFields, void Function(List<FieldInfo>)>
|
||||
_updatedFieldCallbacks = {};
|
||||
|
||||
// Group callbacks
|
||||
final Map<String, GroupConfigurationPB> _groupConfigurationByFieldId = {};
|
||||
|
||||
// Filter callbacks
|
||||
final Map<OnReceiveFilters, VoidCallback> _filterCallbacks = {};
|
||||
_GridFilterNotifier? _filterNotifier = _GridFilterNotifier();
|
||||
final Map<String, FilterPB> _filterPBByFieldId = {};
|
||||
|
||||
// Getters
|
||||
List<FieldInfo> get fieldInfos => [..._fieldNotifier?.fieldInfos ?? []];
|
||||
List<FilterInfo> get filterInfos => [..._filterNotifier?.filters ?? []];
|
||||
FieldInfo? getField(String fieldId) {
|
||||
final fields = _fieldNotifier?.fieldInfos
|
||||
.where((element) => element.id == fieldId)
|
||||
.toList() ??
|
||||
[];
|
||||
if (fields.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
assert(fields.length == 1);
|
||||
return fields.first;
|
||||
}
|
||||
|
||||
FilterInfo? getFilter(String filterId) {
|
||||
final filters = _filterNotifier?.filters
|
||||
.where((element) => element.filter.id == filterId)
|
||||
.toList() ??
|
||||
[];
|
||||
if (filters.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
assert(filters.length == 1);
|
||||
return filters.first;
|
||||
}
|
||||
|
||||
GridFieldController({required this.gridId})
|
||||
: _fieldListener = GridFieldsListener(gridId: gridId),
|
||||
_settingListener = SettingListener(gridId: gridId),
|
||||
_filterListener = FiltersListener(viewId: gridId),
|
||||
_gridFFIService = GridFFIService(gridId: gridId),
|
||||
_settingFFIService = SettingFFIService(viewId: gridId),
|
||||
_settingListener = SettingListener(gridId: gridId) {
|
||||
_filterFFIService = FilterFFIService(viewId: gridId),
|
||||
_settingFFIService = SettingFFIService(viewId: gridId) {
|
||||
//Listen on field's changes
|
||||
_listenOnFieldChanges();
|
||||
|
||||
//Listen on setting changes
|
||||
_listenOnSettingChanges();
|
||||
|
||||
//Listen on the fitler changes
|
||||
_listenOnFilterChanges();
|
||||
|
||||
_settingFFIService.getSetting().then((result) {
|
||||
result.fold(
|
||||
(setting) => _updateSettingConfiguration(setting),
|
||||
(err) => Log.error(err),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void _listenOnFilterChanges() {
|
||||
//Listen on the fitler changes
|
||||
_filterListener.start(onFilterChanged: (result) {
|
||||
result.fold(
|
||||
(changeset) {
|
||||
final List<FilterInfo> filters = filterInfos;
|
||||
// Deletes the filters
|
||||
final deleteFilterIds =
|
||||
changeset.deleteFilters.map((e) => e.id).toList();
|
||||
if (deleteFilterIds.isNotEmpty) {
|
||||
filters.retainWhere(
|
||||
(element) => !deleteFilterIds.contains(element.filter.id),
|
||||
);
|
||||
|
||||
_filterPBByFieldId.removeWhere(
|
||||
(key, value) => deleteFilterIds.contains(value.id));
|
||||
}
|
||||
|
||||
// Inserts the new filter if it's not exist
|
||||
for (final newFilter in changeset.insertFilters) {
|
||||
final filterIndex = filters
|
||||
.indexWhere((element) => element.filter.id == newFilter.id);
|
||||
if (filterIndex == -1) {
|
||||
final fieldInfo = _findFieldInfoForFilter(fieldInfos, newFilter);
|
||||
if (fieldInfo != null) {
|
||||
_filterPBByFieldId[fieldInfo.id] = newFilter;
|
||||
filters.add(FilterInfo(gridId, newFilter, fieldInfo));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (final updatedFilter in changeset.updateFilters) {
|
||||
final filterIndex = filters.indexWhere(
|
||||
(element) => element.filter.id == updatedFilter.filterId,
|
||||
);
|
||||
// Remove the old filter
|
||||
if (filterIndex != -1) {
|
||||
filters.removeAt(filterIndex);
|
||||
_filterPBByFieldId.removeWhere(
|
||||
(key, value) => value.id == updatedFilter.filterId);
|
||||
}
|
||||
|
||||
// Insert the filter if there is a fitler and its field info is
|
||||
// not null
|
||||
if (updatedFilter.hasFilter()) {
|
||||
final fieldInfo = _findFieldInfoForFilter(
|
||||
fieldInfos,
|
||||
updatedFilter.filter,
|
||||
);
|
||||
|
||||
if (fieldInfo != null) {
|
||||
// Insert the filter with the position: filterIndex, otherwise,
|
||||
// append it to the end of the list.
|
||||
final filterInfo =
|
||||
FilterInfo(gridId, updatedFilter.filter, fieldInfo);
|
||||
if (filterIndex != -1) {
|
||||
filters.insert(filterIndex, filterInfo);
|
||||
} else {
|
||||
filters.add(filterInfo);
|
||||
}
|
||||
_filterPBByFieldId[fieldInfo.id] = updatedFilter.filter;
|
||||
}
|
||||
}
|
||||
}
|
||||
_updateFieldInfos();
|
||||
_filterNotifier?.filters = filters;
|
||||
},
|
||||
(err) => Log.error(err),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void _listenOnSettingChanges() {
|
||||
//Listen on setting changes
|
||||
_settingListener.start(onSettingUpdated: (result) {
|
||||
result.fold(
|
||||
(setting) => _updateSettingConfiguration(setting),
|
||||
(r) => Log.error(r),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void _listenOnFieldChanges() {
|
||||
//Listen on field's changes
|
||||
_fieldListener.start(onFieldsChanged: (result) {
|
||||
result.fold(
|
||||
(changeset) {
|
||||
_deleteFields(changeset.deletedFields);
|
||||
_insertFields(changeset.insertedFields);
|
||||
_updateFields(changeset.updatedFields);
|
||||
for (final listener in _changesetCallbackMap.values) {
|
||||
listener(changeset);
|
||||
|
||||
final updateFields = _updateFields(changeset.updatedFields);
|
||||
for (final listener in _updatedFieldCallbacks.values) {
|
||||
listener(updateFields);
|
||||
}
|
||||
},
|
||||
(err) => Log.error(err),
|
||||
);
|
||||
});
|
||||
|
||||
//Listen on setting changes
|
||||
_settingListener.start(onSettingUpdated: (result) {
|
||||
result.fold(
|
||||
(setting) => _updateGroupConfiguration(setting),
|
||||
(r) => Log.error(r),
|
||||
);
|
||||
});
|
||||
|
||||
_settingFFIService.getSetting().then((result) {
|
||||
result.fold(
|
||||
(setting) => _updateGroupConfiguration(setting),
|
||||
(err) => Log.error(err),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
GridFieldContext? getField(String fieldId) {
|
||||
final fields = _fieldNotifier?.fieldContexts
|
||||
.where(
|
||||
(element) => element.id == fieldId,
|
||||
)
|
||||
.toList();
|
||||
if (fields?.isEmpty ?? true) {
|
||||
return null;
|
||||
}
|
||||
return fields!.first;
|
||||
}
|
||||
|
||||
void _updateGroupConfiguration(GridSettingPB setting) {
|
||||
_configurationByFieldId.clear();
|
||||
void _updateSettingConfiguration(GridSettingPB setting) {
|
||||
_groupConfigurationByFieldId.clear();
|
||||
for (final configuration in setting.groupConfigurations.items) {
|
||||
_configurationByFieldId[configuration.fieldId] = configuration;
|
||||
_groupConfigurationByFieldId[configuration.fieldId] = configuration;
|
||||
}
|
||||
_updateFieldContexts();
|
||||
|
||||
for (final configuration in setting.filters.items) {
|
||||
_filterPBByFieldId[configuration.fieldId] = configuration;
|
||||
}
|
||||
|
||||
_updateFieldInfos();
|
||||
}
|
||||
|
||||
void _updateFieldContexts() {
|
||||
void _updateFieldInfos() {
|
||||
if (_fieldNotifier != null) {
|
||||
for (var field in _fieldNotifier!.fieldContexts) {
|
||||
if (_configurationByFieldId[field.id] != null) {
|
||||
field._isGroupField = true;
|
||||
} else {
|
||||
field._isGroupField = false;
|
||||
}
|
||||
for (var field in _fieldNotifier!.fieldInfos) {
|
||||
field._isGroupField = _groupConfigurationByFieldId[field.id] != null;
|
||||
field._hasFilter = _filterPBByFieldId[field.id] != null;
|
||||
}
|
||||
_fieldNotifier?.notify();
|
||||
}
|
||||
@@ -116,20 +254,33 @@ class GridFieldController {
|
||||
|
||||
Future<void> dispose() async {
|
||||
await _fieldListener.stop();
|
||||
await _filterListener.stop();
|
||||
await _settingListener.stop();
|
||||
|
||||
for (final callback in _fieldCallbacks.values) {
|
||||
_fieldNotifier?.removeListener(callback);
|
||||
}
|
||||
_fieldNotifier?.dispose();
|
||||
_fieldNotifier = null;
|
||||
|
||||
for (final callback in _filterCallbacks.values) {
|
||||
_filterNotifier?.removeListener(callback);
|
||||
}
|
||||
_filterNotifier?.dispose();
|
||||
_filterNotifier = null;
|
||||
}
|
||||
|
||||
Future<Either<Unit, FlowyError>> loadFields(
|
||||
{required List<FieldIdPB> fieldIds}) async {
|
||||
Future<Either<Unit, FlowyError>> loadFields({
|
||||
required List<FieldIdPB> fieldIds,
|
||||
}) async {
|
||||
final result = await _gridFFIService.getFields(fieldIds: fieldIds);
|
||||
return Future(
|
||||
() => result.fold(
|
||||
(newFields) {
|
||||
_fieldNotifier?.fieldContexts = newFields.items
|
||||
.map((field) => GridFieldContext(field: field))
|
||||
.toList();
|
||||
_updateFieldContexts();
|
||||
_fieldNotifier?.fieldInfos =
|
||||
newFields.map((field) => FieldInfo(field: field)).toList();
|
||||
_loadFilters();
|
||||
_updateFieldInfos();
|
||||
return left(unit);
|
||||
},
|
||||
(err) => right(err),
|
||||
@@ -137,20 +288,43 @@ class GridFieldController {
|
||||
);
|
||||
}
|
||||
|
||||
Future<Either<Unit, FlowyError>> _loadFilters() async {
|
||||
return _filterFFIService.getAllFilters().then((result) {
|
||||
return result.fold(
|
||||
(filterPBs) {
|
||||
final List<FilterInfo> filters = [];
|
||||
for (final filterPB in filterPBs) {
|
||||
final fieldInfo = _findFieldInfoForFilter(fieldInfos, filterPB);
|
||||
if (fieldInfo != null) {
|
||||
final filterInfo = FilterInfo(gridId, filterPB, fieldInfo);
|
||||
filters.add(filterInfo);
|
||||
}
|
||||
}
|
||||
|
||||
_updateFieldInfos();
|
||||
_filterNotifier?.filters = filters;
|
||||
return left(unit);
|
||||
},
|
||||
(err) => right(err),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void addListener({
|
||||
OnReceiveFields? onFields,
|
||||
OnChangeset? onChangeset,
|
||||
OnReceiveUpdateFields? onFieldsUpdated,
|
||||
OnReceiveFilters? onFilters,
|
||||
bool Function()? listenWhen,
|
||||
}) {
|
||||
if (onChangeset != null) {
|
||||
callback(c) {
|
||||
if (onFieldsUpdated != null) {
|
||||
callback(List<FieldInfo> updateFields) {
|
||||
if (listenWhen != null && listenWhen() == false) {
|
||||
return;
|
||||
}
|
||||
onChangeset(c);
|
||||
onFieldsUpdated(updateFields);
|
||||
}
|
||||
|
||||
_changesetCallbackMap[onChangeset] = callback;
|
||||
_updatedFieldCallbacks[onFieldsUpdated] = callback;
|
||||
}
|
||||
|
||||
if (onFields != null) {
|
||||
@@ -158,27 +332,42 @@ class GridFieldController {
|
||||
if (listenWhen != null && listenWhen() == false) {
|
||||
return;
|
||||
}
|
||||
onFields(fieldContexts);
|
||||
onFields(fieldInfos);
|
||||
}
|
||||
|
||||
_fieldCallbackMap[onFields] = callback;
|
||||
_fieldCallbacks[onFields] = callback;
|
||||
_fieldNotifier?.addListener(callback);
|
||||
}
|
||||
|
||||
if (onFilters != null) {
|
||||
callback() {
|
||||
if (listenWhen != null && listenWhen() == false) {
|
||||
return;
|
||||
}
|
||||
onFilters(filterInfos);
|
||||
}
|
||||
|
||||
_filterCallbacks[onFilters] = callback;
|
||||
_filterNotifier?.addListener(callback);
|
||||
}
|
||||
}
|
||||
|
||||
void removeListener({
|
||||
OnReceiveFields? onFieldsListener,
|
||||
OnChangeset? onChangesetListener,
|
||||
OnReceiveFilters? onFiltersListener,
|
||||
OnReceiveUpdateFields? onChangesetListener,
|
||||
}) {
|
||||
if (onFieldsListener != null) {
|
||||
final callback = _fieldCallbackMap.remove(onFieldsListener);
|
||||
final callback = _fieldCallbacks.remove(onFieldsListener);
|
||||
if (callback != null) {
|
||||
_fieldNotifier?.removeListener(callback);
|
||||
}
|
||||
}
|
||||
|
||||
if (onChangesetListener != null) {
|
||||
_changesetCallbackMap.remove(onChangesetListener);
|
||||
if (onFiltersListener != null) {
|
||||
final callback = _filterCallbacks.remove(onFiltersListener);
|
||||
if (callback != null) {
|
||||
_filterNotifier?.removeListener(callback);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,58 +375,65 @@ class GridFieldController {
|
||||
if (deletedFields.isEmpty) {
|
||||
return;
|
||||
}
|
||||
final List<GridFieldContext> newFields = fieldContexts;
|
||||
final List<FieldInfo> newFields = fieldInfos;
|
||||
final Map<String, FieldIdPB> deletedFieldMap = {
|
||||
for (var fieldOrder in deletedFields) fieldOrder.fieldId: fieldOrder
|
||||
};
|
||||
|
||||
newFields.retainWhere((field) => (deletedFieldMap[field.id] == null));
|
||||
_fieldNotifier?.fieldContexts = newFields;
|
||||
_fieldNotifier?.fieldInfos = newFields;
|
||||
}
|
||||
|
||||
void _insertFields(List<IndexFieldPB> insertedFields) {
|
||||
if (insertedFields.isEmpty) {
|
||||
return;
|
||||
}
|
||||
final List<GridFieldContext> newFields = fieldContexts;
|
||||
final List<FieldInfo> newFields = fieldInfos;
|
||||
for (final indexField in insertedFields) {
|
||||
final gridField = GridFieldContext(field: indexField.field_1);
|
||||
final gridField = FieldInfo(field: indexField.field_1);
|
||||
if (newFields.length > indexField.index) {
|
||||
newFields.insert(indexField.index, gridField);
|
||||
} else {
|
||||
newFields.add(gridField);
|
||||
}
|
||||
}
|
||||
_fieldNotifier?.fieldContexts = newFields;
|
||||
_fieldNotifier?.fieldInfos = newFields;
|
||||
}
|
||||
|
||||
void _updateFields(List<FieldPB> updatedFields) {
|
||||
if (updatedFields.isEmpty) {
|
||||
return;
|
||||
List<FieldInfo> _updateFields(List<FieldPB> updatedFieldPBs) {
|
||||
if (updatedFieldPBs.isEmpty) {
|
||||
return [];
|
||||
}
|
||||
final List<GridFieldContext> newFields = fieldContexts;
|
||||
for (final updatedField in updatedFields) {
|
||||
|
||||
final List<FieldInfo> newFields = fieldInfos;
|
||||
final List<FieldInfo> updatedFields = [];
|
||||
for (final updatedFieldPB in updatedFieldPBs) {
|
||||
final index =
|
||||
newFields.indexWhere((field) => field.id == updatedField.id);
|
||||
newFields.indexWhere((field) => field.id == updatedFieldPB.id);
|
||||
if (index != -1) {
|
||||
newFields.removeAt(index);
|
||||
final gridField = GridFieldContext(field: updatedField);
|
||||
newFields.insert(index, gridField);
|
||||
final fieldInfo = FieldInfo(field: updatedFieldPB);
|
||||
newFields.insert(index, fieldInfo);
|
||||
updatedFields.add(fieldInfo);
|
||||
}
|
||||
}
|
||||
_fieldNotifier?.fieldContexts = newFields;
|
||||
|
||||
if (updatedFields.isNotEmpty) {
|
||||
_fieldNotifier?.fieldInfos = newFields;
|
||||
}
|
||||
return updatedFields;
|
||||
}
|
||||
}
|
||||
|
||||
class GridRowFieldNotifierImpl extends IGridRowFieldNotifier {
|
||||
final GridFieldController _cache;
|
||||
OnChangeset? _onChangesetFn;
|
||||
OnReceiveUpdateFields? _onChangesetFn;
|
||||
OnReceiveFields? _onFieldFn;
|
||||
GridRowFieldNotifierImpl(GridFieldController cache) : _cache = cache;
|
||||
|
||||
@override
|
||||
UnmodifiableListView<GridFieldContext> get fields =>
|
||||
UnmodifiableListView(_cache.fieldContexts);
|
||||
UnmodifiableListView<FieldInfo> get fields =>
|
||||
UnmodifiableListView(_cache.fieldInfos);
|
||||
|
||||
@override
|
||||
void onRowFieldsChanged(VoidCallback callback) {
|
||||
@@ -246,14 +442,14 @@ class GridRowFieldNotifierImpl extends IGridRowFieldNotifier {
|
||||
}
|
||||
|
||||
@override
|
||||
void onRowFieldChanged(void Function(FieldPB) callback) {
|
||||
_onChangesetFn = (FieldChangesetPB changeset) {
|
||||
for (final updatedField in changeset.updatedFields) {
|
||||
void onRowFieldChanged(void Function(FieldInfo) callback) {
|
||||
_onChangesetFn = (List<FieldInfo> fieldInfos) {
|
||||
for (final updatedField in fieldInfos) {
|
||||
callback(updatedField);
|
||||
}
|
||||
};
|
||||
|
||||
_cache.addListener(onChangeset: _onChangesetFn);
|
||||
_cache.addListener(onFieldsUpdated: _onChangesetFn);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -270,10 +466,25 @@ class GridRowFieldNotifierImpl extends IGridRowFieldNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
class GridFieldContext {
|
||||
FieldInfo? _findFieldInfoForFilter(
|
||||
List<FieldInfo> fieldInfos, FilterPB filter) {
|
||||
final fieldIndex = fieldInfos.indexWhere((element) {
|
||||
return element.id == filter.fieldId &&
|
||||
element.fieldType == filter.fieldType;
|
||||
});
|
||||
if (fieldIndex != -1) {
|
||||
return fieldInfos[fieldIndex];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
class FieldInfo {
|
||||
final FieldPB _field;
|
||||
bool _isGroupField = false;
|
||||
|
||||
bool _hasFilter = false;
|
||||
|
||||
String get id => _field.id;
|
||||
|
||||
FieldType get fieldType => _field.fieldType;
|
||||
@@ -290,6 +501,8 @@ class GridFieldContext {
|
||||
|
||||
bool get isGroupField => _isGroupField;
|
||||
|
||||
bool get hasFilter => _hasFilter;
|
||||
|
||||
bool get canGroup {
|
||||
switch (_field.fieldType) {
|
||||
case FieldType.Checkbox:
|
||||
@@ -311,5 +524,19 @@ class GridFieldContext {
|
||||
return false;
|
||||
}
|
||||
|
||||
GridFieldContext({required FieldPB field}) : _field = field;
|
||||
bool get canCreateFilter {
|
||||
if (hasFilter) return false;
|
||||
|
||||
switch (_field.fieldType) {
|
||||
case FieldType.Checkbox:
|
||||
// case FieldType.MultiSelect:
|
||||
case FieldType.RichText:
|
||||
case FieldType.SingleSelect:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
FieldInfo({required FieldPB field}) : _field = field;
|
||||
}
|
||||
@@ -57,6 +57,9 @@ class FieldEditorBloc extends Bloc<FieldEditorEvent, FieldEditorState> {
|
||||
},
|
||||
);
|
||||
},
|
||||
switchToField: (FieldType fieldType) async {
|
||||
await dataController.switchToField(fieldType);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -73,6 +76,8 @@ class FieldEditorEvent with _$FieldEditorEvent {
|
||||
const factory FieldEditorEvent.initial() = _InitialField;
|
||||
const factory FieldEditorEvent.updateName(String name) = _UpdateName;
|
||||
const factory FieldEditorEvent.deleteField() = _DeleteField;
|
||||
const factory FieldEditorEvent.switchToField(FieldType fieldType) =
|
||||
_SwitchToField;
|
||||
const factory FieldEditorEvent.didReceiveFieldChanged(FieldPB field) =
|
||||
_DidReceiveFieldChanged;
|
||||
}
|
||||
|
||||
@@ -27,11 +27,11 @@ class SingleFieldListener {
|
||||
}
|
||||
|
||||
void _handler(
|
||||
GridNotification ty,
|
||||
GridDartNotification ty,
|
||||
Either<Uint8List, FlowyError> result,
|
||||
) {
|
||||
switch (ty) {
|
||||
case GridNotification.DidUpdateField:
|
||||
case GridDartNotification.DidUpdateField:
|
||||
result.fold(
|
||||
(payload) =>
|
||||
_updateFieldNotifier?.value = left(FieldPB.fromBuffer(payload)),
|
||||
|
||||
@@ -34,9 +34,8 @@ class FieldService {
|
||||
bool? frozen,
|
||||
bool? visibility,
|
||||
double? width,
|
||||
List<int>? typeOptionData,
|
||||
}) {
|
||||
var payload = FieldChangesetPayloadPB.create()
|
||||
var payload = FieldChangesetPB.create()
|
||||
..gridId = gridId
|
||||
..fieldId = fieldId;
|
||||
|
||||
@@ -60,10 +59,6 @@ class FieldService {
|
||||
payload.width = width.toInt();
|
||||
}
|
||||
|
||||
if (typeOptionData != null) {
|
||||
payload.typeOptionData = typeOptionData;
|
||||
}
|
||||
|
||||
return GridEventUpdateField(payload).send();
|
||||
}
|
||||
|
||||
@@ -72,7 +67,7 @@ class FieldService {
|
||||
required String fieldId,
|
||||
required List<int> typeOptionData,
|
||||
}) {
|
||||
var payload = UpdateFieldTypeOptionPayloadPB.create()
|
||||
var payload = TypeOptionChangesetPB.create()
|
||||
..gridId = gridId
|
||||
..fieldId = fieldId
|
||||
..typeOptionData = typeOptionData;
|
||||
@@ -96,10 +91,10 @@ class FieldService {
|
||||
return GridEventDuplicateField(payload).send();
|
||||
}
|
||||
|
||||
Future<Either<FieldTypeOptionDataPB, FlowyError>> getFieldTypeOptionData({
|
||||
Future<Either<TypeOptionPB, FlowyError>> getFieldTypeOptionData({
|
||||
required FieldType fieldType,
|
||||
}) {
|
||||
final payload = FieldTypeOptionIdPB.create()
|
||||
final payload = TypeOptionPathPB.create()
|
||||
..gridId = gridId
|
||||
..fieldId = fieldId
|
||||
..fieldType = fieldType;
|
||||
|
||||
+8
-2
@@ -25,6 +25,9 @@ class FieldTypeOptionEditBloc
|
||||
didReceiveFieldUpdated: (field) {
|
||||
emit(state.copyWith(field: field));
|
||||
},
|
||||
switchToField: (FieldType fieldType) async {
|
||||
await _dataController.switchToField(fieldType);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -42,6 +45,8 @@ class FieldTypeOptionEditBloc
|
||||
@freezed
|
||||
class FieldTypeOptionEditEvent with _$FieldTypeOptionEditEvent {
|
||||
const factory FieldTypeOptionEditEvent.initial() = _Initial;
|
||||
const factory FieldTypeOptionEditEvent.switchToField(FieldType fieldType) =
|
||||
_SwitchToField;
|
||||
const factory FieldTypeOptionEditEvent.didReceiveFieldUpdated(FieldPB field) =
|
||||
_DidReceiveFieldUpdated;
|
||||
}
|
||||
@@ -53,8 +58,9 @@ class FieldTypeOptionEditState with _$FieldTypeOptionEditState {
|
||||
}) = _FieldTypeOptionEditState;
|
||||
|
||||
factory FieldTypeOptionEditState.initial(
|
||||
TypeOptionDataController fieldContext) =>
|
||||
TypeOptionDataController typeOptionDataController,
|
||||
) =>
|
||||
FieldTypeOptionEditState(
|
||||
field: fieldContext.field,
|
||||
field: typeOptionDataController.field,
|
||||
);
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import 'dart:async';
|
||||
import 'dart:typed_data';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
|
||||
|
||||
typedef UpdateFieldNotifiedValue = Either<FieldChangesetPB, FlowyError>;
|
||||
typedef UpdateFieldNotifiedValue = Either<GridFieldChangesetPB, FlowyError>;
|
||||
|
||||
class GridFieldsListener {
|
||||
final String gridId;
|
||||
@@ -25,12 +25,12 @@ class GridFieldsListener {
|
||||
);
|
||||
}
|
||||
|
||||
void _handler(GridNotification ty, Either<Uint8List, FlowyError> result) {
|
||||
void _handler(GridDartNotification ty, Either<Uint8List, FlowyError> result) {
|
||||
switch (ty) {
|
||||
case GridNotification.DidUpdateGridField:
|
||||
case GridDartNotification.DidUpdateGridField:
|
||||
result.fold(
|
||||
(payload) => updateFieldsNotifier?.value =
|
||||
left(FieldChangesetPB.fromBuffer(payload)),
|
||||
left(GridFieldChangesetPB.fromBuffer(payload)),
|
||||
(error) => updateFieldsNotifier?.value = right(error),
|
||||
);
|
||||
break;
|
||||
|
||||
+7
-7
@@ -143,11 +143,11 @@ abstract class TypeOptionFieldDelegate {
|
||||
|
||||
abstract class IFieldTypeOptionLoader {
|
||||
String get gridId;
|
||||
Future<Either<FieldTypeOptionDataPB, FlowyError>> load();
|
||||
Future<Either<TypeOptionPB, FlowyError>> load();
|
||||
|
||||
Future<Either<Unit, FlowyError>> switchToField(
|
||||
String fieldId, FieldType fieldType) {
|
||||
final payload = EditFieldPayloadPB.create()
|
||||
final payload = EditFieldChangesetPB.create()
|
||||
..gridId = gridId
|
||||
..fieldId = fieldId
|
||||
..fieldType = fieldType;
|
||||
@@ -158,7 +158,7 @@ abstract class IFieldTypeOptionLoader {
|
||||
|
||||
/// Uses when creating a new field
|
||||
class NewFieldTypeOptionLoader extends IFieldTypeOptionLoader {
|
||||
FieldTypeOptionDataPB? fieldTypeOption;
|
||||
TypeOptionPB? fieldTypeOption;
|
||||
|
||||
@override
|
||||
final String gridId;
|
||||
@@ -169,9 +169,9 @@ class NewFieldTypeOptionLoader extends IFieldTypeOptionLoader {
|
||||
/// Creates the field type option if the fieldTypeOption is null.
|
||||
/// Otherwise, it loads the type option data from the backend.
|
||||
@override
|
||||
Future<Either<FieldTypeOptionDataPB, FlowyError>> load() {
|
||||
Future<Either<TypeOptionPB, FlowyError>> load() {
|
||||
if (fieldTypeOption != null) {
|
||||
final payload = FieldTypeOptionIdPB.create()
|
||||
final payload = TypeOptionPathPB.create()
|
||||
..gridId = gridId
|
||||
..fieldId = fieldTypeOption!.field_2.id
|
||||
..fieldType = fieldTypeOption!.field_2.fieldType;
|
||||
@@ -207,8 +207,8 @@ class FieldTypeOptionLoader extends IFieldTypeOptionLoader {
|
||||
});
|
||||
|
||||
@override
|
||||
Future<Either<FieldTypeOptionDataPB, FlowyError>> load() {
|
||||
final payload = FieldTypeOptionIdPB.create()
|
||||
Future<Either<TypeOptionPB, FlowyError>> load() {
|
||||
final payload = TypeOptionPathPB.create()
|
||||
..gridId = gridId
|
||||
..fieldId = field.id
|
||||
..fieldType = field.fieldType;
|
||||
|
||||
+23
-18
@@ -4,7 +4,7 @@ import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
|
||||
import 'package:app_flowy/plugins/grid/application/field/field_service.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:protobuf/protobuf.dart';
|
||||
import 'package:protobuf/protobuf.dart' hide FieldInfo;
|
||||
import 'package:flowy_sdk/log.dart';
|
||||
|
||||
import 'type_option_context.dart';
|
||||
@@ -12,18 +12,24 @@ import 'type_option_context.dart';
|
||||
class TypeOptionDataController {
|
||||
final String gridId;
|
||||
final IFieldTypeOptionLoader loader;
|
||||
late FieldTypeOptionDataPB _data;
|
||||
late TypeOptionPB _data;
|
||||
final PublishNotifier<FieldPB> _fieldNotifier = PublishNotifier();
|
||||
|
||||
/// Returns a [TypeOptionDataController] used to modify the specified
|
||||
/// [FieldPB]'s data
|
||||
///
|
||||
/// Should call [loadTypeOptionData] if the passed-in [FieldInfo]
|
||||
/// is null
|
||||
///
|
||||
TypeOptionDataController({
|
||||
required this.gridId,
|
||||
required this.loader,
|
||||
GridFieldContext? fieldContext,
|
||||
FieldInfo? fieldInfo,
|
||||
}) {
|
||||
if (fieldContext != null) {
|
||||
_data = FieldTypeOptionDataPB.create()
|
||||
if (fieldInfo != null) {
|
||||
_data = TypeOptionPB.create()
|
||||
..gridId = gridId
|
||||
..field_2 = fieldContext.field;
|
||||
..field_2 = fieldInfo.field;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,18 +83,17 @@ class TypeOptionDataController {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> switchToField(FieldType newFieldType) {
|
||||
return loader.switchToField(field.id, newFieldType).then((result) {
|
||||
return result.fold(
|
||||
(_) {
|
||||
// Should load the type-option data after switching to a new field.
|
||||
// After loading the type-option data, the editor widget that uses
|
||||
// the type-option data will be rebuild.
|
||||
loadTypeOptionData();
|
||||
},
|
||||
(err) => Log.error(err),
|
||||
);
|
||||
});
|
||||
Future<void> switchToField(FieldType newFieldType) async {
|
||||
final result = await loader.switchToField(field.id, newFieldType);
|
||||
await result.fold(
|
||||
(_) {
|
||||
// Should load the type-option data after switching to a new field.
|
||||
// After loading the type-option data, the editor widget that uses
|
||||
// the type-option data will be rebuild.
|
||||
loadTypeOptionData();
|
||||
},
|
||||
(err) => Future(() => Log.error(err)),
|
||||
);
|
||||
}
|
||||
|
||||
void Function() addFieldListener(void Function(FieldPB) callback) {
|
||||
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
import 'package:app_flowy/plugins/grid/presentation/widgets/filter/filter_info.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/checkbox_filter.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/util.pb.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'dart:async';
|
||||
import 'filter_listener.dart';
|
||||
import 'filter_service.dart';
|
||||
|
||||
part 'checkbox_filter_editor_bloc.freezed.dart';
|
||||
|
||||
class CheckboxFilterEditorBloc
|
||||
extends Bloc<CheckboxFilterEditorEvent, CheckboxFilterEditorState> {
|
||||
final FilterInfo filterInfo;
|
||||
final FilterFFIService _ffiService;
|
||||
final FilterListener _listener;
|
||||
|
||||
CheckboxFilterEditorBloc({required this.filterInfo})
|
||||
: _ffiService = FilterFFIService(viewId: filterInfo.viewId),
|
||||
_listener = FilterListener(
|
||||
viewId: filterInfo.viewId,
|
||||
filterId: filterInfo.filter.id,
|
||||
),
|
||||
super(CheckboxFilterEditorState.initial(filterInfo)) {
|
||||
on<CheckboxFilterEditorEvent>(
|
||||
(event, emit) async {
|
||||
event.when(
|
||||
initial: () async {
|
||||
_startListening();
|
||||
},
|
||||
updateCondition: (CheckboxFilterCondition condition) {
|
||||
_ffiService.insertCheckboxFilter(
|
||||
filterId: filterInfo.filter.id,
|
||||
fieldId: filterInfo.field.id,
|
||||
condition: condition,
|
||||
);
|
||||
},
|
||||
delete: () {
|
||||
_ffiService.deleteFilter(
|
||||
fieldId: filterInfo.field.id,
|
||||
filterId: filterInfo.filter.id,
|
||||
fieldType: filterInfo.field.fieldType,
|
||||
);
|
||||
},
|
||||
didReceiveFilter: (FilterPB filter) {
|
||||
final filterInfo = state.filterInfo.copyWith(filter: filter);
|
||||
final checkboxFilter = filterInfo.checkboxFilter()!;
|
||||
emit(state.copyWith(
|
||||
filterInfo: filterInfo,
|
||||
filter: checkboxFilter,
|
||||
));
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _startListening() {
|
||||
_listener.start(
|
||||
onDeleted: () {
|
||||
if (!isClosed) add(const CheckboxFilterEditorEvent.delete());
|
||||
},
|
||||
onUpdated: (filter) {
|
||||
if (!isClosed) add(CheckboxFilterEditorEvent.didReceiveFilter(filter));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
await _listener.stop();
|
||||
return super.close();
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class CheckboxFilterEditorEvent with _$CheckboxFilterEditorEvent {
|
||||
const factory CheckboxFilterEditorEvent.initial() = _Initial;
|
||||
const factory CheckboxFilterEditorEvent.didReceiveFilter(FilterPB filter) =
|
||||
_DidReceiveFilter;
|
||||
const factory CheckboxFilterEditorEvent.updateCondition(
|
||||
CheckboxFilterCondition condition) = _UpdateCondition;
|
||||
const factory CheckboxFilterEditorEvent.delete() = _Delete;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class CheckboxFilterEditorState with _$CheckboxFilterEditorState {
|
||||
const factory CheckboxFilterEditorState({
|
||||
required FilterInfo filterInfo,
|
||||
required CheckboxFilterPB filter,
|
||||
}) = _GridFilterState;
|
||||
|
||||
factory CheckboxFilterEditorState.initial(FilterInfo filterInfo) {
|
||||
return CheckboxFilterEditorState(
|
||||
filterInfo: filterInfo,
|
||||
filter: filterInfo.checkboxFilter()!,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
import 'package:app_flowy/plugins/grid/application/field/field_controller.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-error/errors.pbserver.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/checkbox_filter.pbenum.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/date_filter.pbenum.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/number_filter.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/select_option_filter.pbenum.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/text_filter.pb.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'dart:async';
|
||||
import 'filter_service.dart';
|
||||
|
||||
part 'filter_create_bloc.freezed.dart';
|
||||
|
||||
class GridCreateFilterBloc
|
||||
extends Bloc<GridCreateFilterEvent, GridCreateFilterState> {
|
||||
final String viewId;
|
||||
final FilterFFIService _ffiService;
|
||||
final GridFieldController fieldController;
|
||||
void Function(List<FieldInfo>)? _onFieldFn;
|
||||
GridCreateFilterBloc({required this.viewId, required this.fieldController})
|
||||
: _ffiService = FilterFFIService(viewId: viewId),
|
||||
super(GridCreateFilterState.initial(fieldController.fieldInfos)) {
|
||||
on<GridCreateFilterEvent>(
|
||||
(event, emit) async {
|
||||
event.when(
|
||||
initial: () async {
|
||||
_startListening();
|
||||
},
|
||||
didReceiveFields: (List<FieldInfo> fields) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
allFields: fields,
|
||||
creatableFields: _filterFields(fields, state.filterText),
|
||||
),
|
||||
);
|
||||
},
|
||||
didReceiveFilterText: (String text) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
filterText: text,
|
||||
creatableFields: _filterFields(state.allFields, text),
|
||||
),
|
||||
);
|
||||
},
|
||||
createDefaultFilter: (FieldInfo field) {
|
||||
emit(state.copyWith(didCreateFilter: true));
|
||||
_createDefaultFilter(field);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
List<FieldInfo> _filterFields(
|
||||
List<FieldInfo> fields,
|
||||
String filterText,
|
||||
) {
|
||||
final List<FieldInfo> allFields = List.from(fields);
|
||||
final keyword = filterText.toLowerCase();
|
||||
allFields.retainWhere((field) {
|
||||
if (field.canCreateFilter) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (filterText.isNotEmpty) {
|
||||
return field.name.toLowerCase().contains(keyword);
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
return allFields;
|
||||
}
|
||||
|
||||
void _startListening() {
|
||||
_onFieldFn = (fields) {
|
||||
fields.retainWhere((field) => field.canCreateFilter);
|
||||
add(GridCreateFilterEvent.didReceiveFields(fields));
|
||||
};
|
||||
fieldController.addListener(onFields: _onFieldFn);
|
||||
}
|
||||
|
||||
Future<Either<Unit, FlowyError>> _createDefaultFilter(FieldInfo field) async {
|
||||
final fieldId = field.id;
|
||||
switch (field.fieldType) {
|
||||
case FieldType.Checkbox:
|
||||
return _ffiService.insertCheckboxFilter(
|
||||
fieldId: fieldId,
|
||||
condition: CheckboxFilterCondition.IsChecked,
|
||||
);
|
||||
case FieldType.DateTime:
|
||||
final timestamp = DateTime.now().millisecondsSinceEpoch ~/ 1000;
|
||||
return _ffiService.insertDateFilter(
|
||||
fieldId: fieldId,
|
||||
condition: DateFilterCondition.DateIs,
|
||||
timestamp: timestamp,
|
||||
);
|
||||
case FieldType.MultiSelect:
|
||||
return _ffiService.insertSelectOptionFilter(
|
||||
fieldId: fieldId,
|
||||
condition: SelectOptionCondition.OptionIs,
|
||||
fieldType: FieldType.MultiSelect,
|
||||
);
|
||||
case FieldType.Number:
|
||||
return _ffiService.insertNumberFilter(
|
||||
fieldId: fieldId,
|
||||
condition: NumberFilterCondition.Equal,
|
||||
content: "",
|
||||
);
|
||||
case FieldType.RichText:
|
||||
return _ffiService.insertTextFilter(
|
||||
fieldId: fieldId,
|
||||
condition: TextFilterCondition.Contains,
|
||||
content: '',
|
||||
);
|
||||
case FieldType.SingleSelect:
|
||||
return _ffiService.insertSelectOptionFilter(
|
||||
fieldId: fieldId,
|
||||
condition: SelectOptionCondition.OptionIs,
|
||||
fieldType: FieldType.SingleSelect,
|
||||
);
|
||||
case FieldType.URL:
|
||||
return _ffiService.insertURLFilter(
|
||||
fieldId: fieldId,
|
||||
condition: TextFilterCondition.Contains,
|
||||
);
|
||||
}
|
||||
|
||||
return left(unit);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
if (_onFieldFn != null) {
|
||||
fieldController.removeListener(onFieldsListener: _onFieldFn);
|
||||
_onFieldFn = null;
|
||||
}
|
||||
return super.close();
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class GridCreateFilterEvent with _$GridCreateFilterEvent {
|
||||
const factory GridCreateFilterEvent.initial() = _Initial;
|
||||
const factory GridCreateFilterEvent.didReceiveFields(List<FieldInfo> fields) =
|
||||
_DidReceiveFields;
|
||||
|
||||
const factory GridCreateFilterEvent.createDefaultFilter(FieldInfo field) =
|
||||
_CreateDefaultFilter;
|
||||
|
||||
const factory GridCreateFilterEvent.didReceiveFilterText(String text) =
|
||||
_DidReceiveFilterText;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class GridCreateFilterState with _$GridCreateFilterState {
|
||||
const factory GridCreateFilterState({
|
||||
required String filterText,
|
||||
required List<FieldInfo> creatableFields,
|
||||
required List<FieldInfo> allFields,
|
||||
required bool didCreateFilter,
|
||||
}) = _GridFilterState;
|
||||
|
||||
factory GridCreateFilterState.initial(List<FieldInfo> fields) {
|
||||
return GridCreateFilterState(
|
||||
filterText: "",
|
||||
creatableFields: getCreatableFilter(fields),
|
||||
allFields: fields,
|
||||
didCreateFilter: false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
List<FieldInfo> getCreatableFilter(List<FieldInfo> fieldInfos) {
|
||||
final List<FieldInfo> creatableFields = List.from(fieldInfos);
|
||||
creatableFields.retainWhere((element) => element.canCreateFilter);
|
||||
return creatableFields;
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:app_flowy/core/grid_notification.dart';
|
||||
import 'package:flowy_infra/notifier.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/dart_notification.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/filter_changeset.pb.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/util.pb.dart';
|
||||
|
||||
typedef UpdateFilterNotifiedValue
|
||||
= Either<FilterChangesetNotificationPB, FlowyError>;
|
||||
|
||||
class FiltersListener {
|
||||
final String viewId;
|
||||
|
||||
PublishNotifier<UpdateFilterNotifiedValue>? _filterNotifier =
|
||||
PublishNotifier();
|
||||
GridNotificationListener? _listener;
|
||||
FiltersListener({required this.viewId});
|
||||
|
||||
void start({
|
||||
required void Function(UpdateFilterNotifiedValue) onFilterChanged,
|
||||
}) {
|
||||
_filterNotifier?.addPublishListener(onFilterChanged);
|
||||
_listener = GridNotificationListener(
|
||||
objectId: viewId,
|
||||
handler: _handler,
|
||||
);
|
||||
}
|
||||
|
||||
void _handler(
|
||||
GridDartNotification ty,
|
||||
Either<Uint8List, FlowyError> result,
|
||||
) {
|
||||
switch (ty) {
|
||||
case GridDartNotification.DidUpdateFilter:
|
||||
result.fold(
|
||||
(payload) => _filterNotifier?.value =
|
||||
left(FilterChangesetNotificationPB.fromBuffer(payload)),
|
||||
(error) => _filterNotifier?.value = right(error),
|
||||
);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> stop() async {
|
||||
await _listener?.stop();
|
||||
_filterNotifier?.dispose();
|
||||
_filterNotifier = null;
|
||||
}
|
||||
}
|
||||
|
||||
class FilterListener {
|
||||
final String viewId;
|
||||
final String filterId;
|
||||
|
||||
PublishNotifier<FilterPB>? _onDeleteNotifier = PublishNotifier();
|
||||
PublishNotifier<FilterPB>? _onUpdateNotifier = PublishNotifier();
|
||||
|
||||
GridNotificationListener? _listener;
|
||||
FilterListener({required this.viewId, required this.filterId});
|
||||
|
||||
void start({
|
||||
void Function()? onDeleted,
|
||||
void Function(FilterPB)? onUpdated,
|
||||
}) {
|
||||
_onDeleteNotifier?.addPublishListener((_) {
|
||||
onDeleted?.call();
|
||||
});
|
||||
|
||||
_onUpdateNotifier?.addPublishListener((filter) {
|
||||
onUpdated?.call(filter);
|
||||
});
|
||||
|
||||
_listener = GridNotificationListener(
|
||||
objectId: viewId,
|
||||
handler: _handler,
|
||||
);
|
||||
}
|
||||
|
||||
void handleChangeset(FilterChangesetNotificationPB changeset) {
|
||||
// check the delete filter
|
||||
final deletedIndex = changeset.deleteFilters.indexWhere(
|
||||
(element) => element.id == filterId,
|
||||
);
|
||||
if (deletedIndex != -1) {
|
||||
_onDeleteNotifier?.value = changeset.deleteFilters[deletedIndex];
|
||||
}
|
||||
|
||||
// check the updated filter
|
||||
final updatedIndex = changeset.updateFilters.indexWhere(
|
||||
(element) => element.filter.id == filterId,
|
||||
);
|
||||
if (updatedIndex != -1) {
|
||||
_onUpdateNotifier?.value = changeset.updateFilters[updatedIndex].filter;
|
||||
}
|
||||
}
|
||||
|
||||
void _handler(
|
||||
GridDartNotification ty,
|
||||
Either<Uint8List, FlowyError> result,
|
||||
) {
|
||||
switch (ty) {
|
||||
case GridDartNotification.DidUpdateFilter:
|
||||
result.fold(
|
||||
(payload) => handleChangeset(
|
||||
FilterChangesetNotificationPB.fromBuffer(payload)),
|
||||
(error) {},
|
||||
);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> stop() async {
|
||||
await _listener?.stop();
|
||||
_onDeleteNotifier?.dispose();
|
||||
_onDeleteNotifier = null;
|
||||
|
||||
_onUpdateNotifier?.dispose();
|
||||
_onUpdateNotifier = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
import 'package:app_flowy/plugins/grid/application/field/field_controller.dart';
|
||||
import 'package:app_flowy/plugins/grid/presentation/widgets/filter/filter_info.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'dart:async';
|
||||
|
||||
part 'filter_menu_bloc.freezed.dart';
|
||||
|
||||
class GridFilterMenuBloc
|
||||
extends Bloc<GridFilterMenuEvent, GridFilterMenuState> {
|
||||
final String viewId;
|
||||
final GridFieldController fieldController;
|
||||
void Function(List<FilterInfo>)? _onFilterFn;
|
||||
void Function(List<FieldInfo>)? _onFieldFn;
|
||||
|
||||
GridFilterMenuBloc({required this.viewId, required this.fieldController})
|
||||
: super(GridFilterMenuState.initial(
|
||||
viewId,
|
||||
fieldController.filterInfos,
|
||||
fieldController.fieldInfos,
|
||||
)) {
|
||||
on<GridFilterMenuEvent>(
|
||||
(event, emit) async {
|
||||
event.when(
|
||||
initial: () {
|
||||
_startListening();
|
||||
},
|
||||
didReceiveFilters: (filters) {
|
||||
emit(state.copyWith(filters: filters));
|
||||
},
|
||||
toggleMenu: () {
|
||||
final isVisible = !state.isVisible;
|
||||
emit(state.copyWith(isVisible: isVisible));
|
||||
},
|
||||
didReceiveFields: (List<FieldInfo> fields) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
fields: fields,
|
||||
creatableFields: getCreatableFilter(fields),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _startListening() {
|
||||
_onFilterFn = (filters) {
|
||||
add(GridFilterMenuEvent.didReceiveFilters(filters));
|
||||
};
|
||||
|
||||
_onFieldFn = (fields) {
|
||||
add(GridFilterMenuEvent.didReceiveFields(fields));
|
||||
};
|
||||
|
||||
fieldController.addListener(
|
||||
onFilters: (filters) {
|
||||
_onFilterFn?.call(filters);
|
||||
},
|
||||
onFields: (fields) {
|
||||
_onFieldFn?.call(fields);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() {
|
||||
if (_onFilterFn != null) {
|
||||
fieldController.removeListener(onFiltersListener: _onFilterFn!);
|
||||
_onFilterFn = null;
|
||||
}
|
||||
if (_onFieldFn != null) {
|
||||
fieldController.removeListener(onFieldsListener: _onFieldFn!);
|
||||
_onFieldFn = null;
|
||||
}
|
||||
return super.close();
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class GridFilterMenuEvent with _$GridFilterMenuEvent {
|
||||
const factory GridFilterMenuEvent.initial() = _Initial;
|
||||
const factory GridFilterMenuEvent.didReceiveFilters(
|
||||
List<FilterInfo> filters) = _DidReceiveFilters;
|
||||
const factory GridFilterMenuEvent.didReceiveFields(List<FieldInfo> fields) =
|
||||
_DidReceiveFields;
|
||||
const factory GridFilterMenuEvent.toggleMenu() = _SetMenuVisibility;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class GridFilterMenuState with _$GridFilterMenuState {
|
||||
const factory GridFilterMenuState({
|
||||
required String viewId,
|
||||
required List<FilterInfo> filters,
|
||||
required List<FieldInfo> fields,
|
||||
required List<FieldInfo> creatableFields,
|
||||
required bool isVisible,
|
||||
}) = _GridFilterMenuState;
|
||||
|
||||
factory GridFilterMenuState.initial(
|
||||
String viewId,
|
||||
List<FilterInfo> filterInfos,
|
||||
List<FieldInfo> fields,
|
||||
) =>
|
||||
GridFilterMenuState(
|
||||
viewId: viewId,
|
||||
filters: filterInfos,
|
||||
fields: fields,
|
||||
creatableFields: getCreatableFilter(fields),
|
||||
isVisible: false,
|
||||
);
|
||||
}
|
||||
|
||||
List<FieldInfo> getCreatableFilter(List<FieldInfo> fieldInfos) {
|
||||
final List<FieldInfo> creatableFields = List.from(fieldInfos);
|
||||
creatableFields.retainWhere((element) => element.canCreateFilter);
|
||||
return creatableFields;
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:flowy_sdk/dispatch/dispatch.dart';
|
||||
import 'package:flowy_sdk/log.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/checkbox_filter.pbserver.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/date_filter.pbserver.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/grid_entities.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/number_filter.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/select_option_filter.pbserver.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/setting_entities.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/text_filter.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/util.pb.dart';
|
||||
import 'package:fixnum/fixnum.dart' as $fixnum;
|
||||
|
||||
class FilterFFIService {
|
||||
final String viewId;
|
||||
const FilterFFIService({required this.viewId});
|
||||
|
||||
Future<Either<List<FilterPB>, FlowyError>> getAllFilters() {
|
||||
final payload = GridIdPB()..value = viewId;
|
||||
|
||||
return GridEventGetAllFilters(payload).send().then((result) {
|
||||
return result.fold(
|
||||
(repeated) => left(repeated.items),
|
||||
(r) => right(r),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Future<Either<Unit, FlowyError>> insertTextFilter({
|
||||
required String fieldId,
|
||||
String? filterId,
|
||||
required TextFilterCondition condition,
|
||||
required String content,
|
||||
}) {
|
||||
final filter = TextFilterPB()
|
||||
..condition = condition
|
||||
..content = content;
|
||||
|
||||
return insertFilter(
|
||||
fieldId: fieldId,
|
||||
filterId: filterId,
|
||||
fieldType: FieldType.RichText,
|
||||
data: filter.writeToBuffer(),
|
||||
);
|
||||
}
|
||||
|
||||
Future<Either<Unit, FlowyError>> insertCheckboxFilter({
|
||||
required String fieldId,
|
||||
String? filterId,
|
||||
required CheckboxFilterCondition condition,
|
||||
}) {
|
||||
final filter = CheckboxFilterPB()..condition = condition;
|
||||
|
||||
return insertFilter(
|
||||
fieldId: fieldId,
|
||||
filterId: filterId,
|
||||
fieldType: FieldType.Checkbox,
|
||||
data: filter.writeToBuffer(),
|
||||
);
|
||||
}
|
||||
|
||||
Future<Either<Unit, FlowyError>> insertNumberFilter({
|
||||
required String fieldId,
|
||||
String? filterId,
|
||||
required NumberFilterCondition condition,
|
||||
String content = "",
|
||||
}) {
|
||||
final filter = NumberFilterPB()
|
||||
..condition = condition
|
||||
..content = content;
|
||||
|
||||
return insertFilter(
|
||||
fieldId: fieldId,
|
||||
filterId: filterId,
|
||||
fieldType: FieldType.Number,
|
||||
data: filter.writeToBuffer(),
|
||||
);
|
||||
}
|
||||
|
||||
Future<Either<Unit, FlowyError>> insertDateFilter({
|
||||
required String fieldId,
|
||||
String? filterId,
|
||||
required DateFilterCondition condition,
|
||||
int? start,
|
||||
int? end,
|
||||
int? timestamp,
|
||||
}) {
|
||||
var filter = DateFilterPB();
|
||||
if (timestamp != null) {
|
||||
filter.timestamp = $fixnum.Int64(timestamp);
|
||||
} else {
|
||||
if (start != null && end != null) {
|
||||
filter.start = $fixnum.Int64(start);
|
||||
filter.end = $fixnum.Int64(end);
|
||||
} else {
|
||||
throw Exception(
|
||||
"Start and end should not be null if the timestamp is null");
|
||||
}
|
||||
}
|
||||
|
||||
return insertFilter(
|
||||
fieldId: fieldId,
|
||||
filterId: filterId,
|
||||
fieldType: FieldType.DateTime,
|
||||
data: filter.writeToBuffer(),
|
||||
);
|
||||
}
|
||||
|
||||
Future<Either<Unit, FlowyError>> insertURLFilter({
|
||||
required String fieldId,
|
||||
String? filterId,
|
||||
required TextFilterCondition condition,
|
||||
String content = "",
|
||||
}) {
|
||||
final filter = TextFilterPB()
|
||||
..condition = condition
|
||||
..content = content;
|
||||
|
||||
return insertFilter(
|
||||
fieldId: fieldId,
|
||||
filterId: filterId,
|
||||
fieldType: FieldType.URL,
|
||||
data: filter.writeToBuffer(),
|
||||
);
|
||||
}
|
||||
|
||||
Future<Either<Unit, FlowyError>> insertSelectOptionFilter({
|
||||
required String fieldId,
|
||||
required FieldType fieldType,
|
||||
required SelectOptionCondition condition,
|
||||
String? filterId,
|
||||
List<String> optionIds = const [],
|
||||
}) {
|
||||
final filter = SelectOptionFilterPB()
|
||||
..condition = condition
|
||||
..optionIds.addAll(optionIds);
|
||||
|
||||
return insertFilter(
|
||||
fieldId: fieldId,
|
||||
filterId: filterId,
|
||||
fieldType: fieldType,
|
||||
data: filter.writeToBuffer(),
|
||||
);
|
||||
}
|
||||
|
||||
Future<Either<Unit, FlowyError>> insertFilter({
|
||||
required String fieldId,
|
||||
String? filterId,
|
||||
required FieldType fieldType,
|
||||
required List<int> data,
|
||||
}) {
|
||||
var insertFilterPayload = AlterFilterPayloadPB.create()
|
||||
..fieldId = fieldId
|
||||
..fieldType = fieldType
|
||||
..data = data;
|
||||
|
||||
if (filterId != null) {
|
||||
insertFilterPayload.filterId = filterId;
|
||||
}
|
||||
|
||||
final payload = GridSettingChangesetPB.create()
|
||||
..gridId = viewId
|
||||
..insertFilter = insertFilterPayload;
|
||||
return GridEventUpdateGridSetting(payload).send().then((result) {
|
||||
return result.fold(
|
||||
(l) => left(l),
|
||||
(err) {
|
||||
Log.error(err);
|
||||
return right(err);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Future<Either<Unit, FlowyError>> deleteFilter({
|
||||
required String fieldId,
|
||||
required String filterId,
|
||||
required FieldType fieldType,
|
||||
}) {
|
||||
TextFilterCondition.DoesNotContain.value;
|
||||
|
||||
final deleteFilterPayload = DeleteFilterPayloadPB.create()
|
||||
..fieldId = fieldId
|
||||
..filterId = filterId
|
||||
..fieldType = fieldType;
|
||||
|
||||
final payload = GridSettingChangesetPB.create()
|
||||
..gridId = viewId
|
||||
..deleteFilter = deleteFilterPayload;
|
||||
|
||||
return GridEventUpdateGridSetting(payload).send().then((result) {
|
||||
return result.fold(
|
||||
(l) => left(l),
|
||||
(err) {
|
||||
Log.error(err);
|
||||
return right(err);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
+148
@@ -0,0 +1,148 @@
|
||||
import 'package:app_flowy/plugins/grid/application/field/type_option/type_option_context.dart';
|
||||
import 'package:app_flowy/plugins/grid/presentation/widgets/filter/filter_info.dart';
|
||||
import 'package:app_flowy/plugins/grid/presentation/widgets/header/type_option/builder.dart';
|
||||
import 'package:flowy_sdk/log.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/select_option_filter.pbserver.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/util.pb.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'dart:async';
|
||||
import 'filter_listener.dart';
|
||||
import 'filter_service.dart';
|
||||
|
||||
part 'select_option_filter_bloc.freezed.dart';
|
||||
|
||||
class SelectOptionFilterEditorBloc
|
||||
extends Bloc<SelectOptionFilterEditorEvent, SelectOptionFilterEditorState> {
|
||||
final FilterInfo filterInfo;
|
||||
final FilterFFIService _ffiService;
|
||||
final FilterListener _listener;
|
||||
final SingleSelectTypeOptionContext typeOptionContext;
|
||||
|
||||
SelectOptionFilterEditorBloc({required this.filterInfo})
|
||||
: _ffiService = FilterFFIService(viewId: filterInfo.viewId),
|
||||
_listener = FilterListener(
|
||||
viewId: filterInfo.viewId,
|
||||
filterId: filterInfo.filter.id,
|
||||
),
|
||||
typeOptionContext = makeSingleSelectTypeOptionContext(
|
||||
gridId: filterInfo.viewId,
|
||||
fieldPB: filterInfo.field.field,
|
||||
),
|
||||
super(SelectOptionFilterEditorState.initial(filterInfo)) {
|
||||
on<SelectOptionFilterEditorEvent>(
|
||||
(event, emit) async {
|
||||
event.when(
|
||||
initial: () async {
|
||||
_startListening();
|
||||
_loadOptions();
|
||||
},
|
||||
updateCondition: (SelectOptionCondition condition) {
|
||||
_ffiService.insertSelectOptionFilter(
|
||||
filterId: filterInfo.filter.id,
|
||||
fieldId: filterInfo.field.id,
|
||||
condition: condition,
|
||||
optionIds: state.filter.optionIds,
|
||||
fieldType: state.filterInfo.field.fieldType,
|
||||
);
|
||||
},
|
||||
updateContent: (List<String> optionIds) {
|
||||
_ffiService.insertSelectOptionFilter(
|
||||
filterId: filterInfo.filter.id,
|
||||
fieldId: filterInfo.field.id,
|
||||
condition: state.filter.condition,
|
||||
optionIds: optionIds,
|
||||
fieldType: state.filterInfo.field.fieldType,
|
||||
);
|
||||
},
|
||||
delete: () {
|
||||
_ffiService.deleteFilter(
|
||||
fieldId: filterInfo.field.id,
|
||||
filterId: filterInfo.filter.id,
|
||||
fieldType: filterInfo.field.fieldType,
|
||||
);
|
||||
},
|
||||
didReceiveFilter: (FilterPB filter) {
|
||||
final filterInfo = state.filterInfo.copyWith(filter: filter);
|
||||
final selectOptionFilter = filterInfo.selectOptionFilter()!;
|
||||
emit(state.copyWith(
|
||||
filterInfo: filterInfo,
|
||||
filter: selectOptionFilter,
|
||||
));
|
||||
},
|
||||
updateFilterDescription: (String desc) {
|
||||
emit(state.copyWith(filterDesc: desc));
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _startListening() {
|
||||
_listener.start(
|
||||
onDeleted: () {
|
||||
if (!isClosed) add(const SelectOptionFilterEditorEvent.delete());
|
||||
},
|
||||
onUpdated: (filter) {
|
||||
if (!isClosed) {
|
||||
add(SelectOptionFilterEditorEvent.didReceiveFilter(filter));
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _loadOptions() {
|
||||
typeOptionContext.loadTypeOptionData(
|
||||
onCompleted: (value) {
|
||||
if (!isClosed) {
|
||||
String filterDesc = '';
|
||||
for (final option in value.options) {
|
||||
if (state.filter.optionIds.contains(option.id)) {
|
||||
filterDesc += "${option.name} ";
|
||||
}
|
||||
}
|
||||
add(SelectOptionFilterEditorEvent.updateFilterDescription(
|
||||
filterDesc));
|
||||
}
|
||||
},
|
||||
onError: (error) => Log.error(error),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
await _listener.stop();
|
||||
return super.close();
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class SelectOptionFilterEditorEvent with _$SelectOptionFilterEditorEvent {
|
||||
const factory SelectOptionFilterEditorEvent.initial() = _Initial;
|
||||
const factory SelectOptionFilterEditorEvent.didReceiveFilter(
|
||||
FilterPB filter) = _DidReceiveFilter;
|
||||
const factory SelectOptionFilterEditorEvent.updateCondition(
|
||||
SelectOptionCondition condition) = _UpdateCondition;
|
||||
const factory SelectOptionFilterEditorEvent.updateContent(
|
||||
List<String> optionIds) = _UpdateContent;
|
||||
const factory SelectOptionFilterEditorEvent.updateFilterDescription(
|
||||
String desc) = _UpdateDesc;
|
||||
const factory SelectOptionFilterEditorEvent.delete() = _Delete;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class SelectOptionFilterEditorState with _$SelectOptionFilterEditorState {
|
||||
const factory SelectOptionFilterEditorState({
|
||||
required FilterInfo filterInfo,
|
||||
required SelectOptionFilterPB filter,
|
||||
required String filterDesc,
|
||||
}) = _GridFilterState;
|
||||
|
||||
factory SelectOptionFilterEditorState.initial(FilterInfo filterInfo) {
|
||||
return SelectOptionFilterEditorState(
|
||||
filterInfo: filterInfo,
|
||||
filter: filterInfo.selectOptionFilter()!,
|
||||
filterDesc: '',
|
||||
);
|
||||
}
|
||||
}
|
||||
Whitespace-only changes.
+156
@@ -0,0 +1,156 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:app_flowy/plugins/grid/application/field/type_option/type_option_context.dart';
|
||||
import 'package:app_flowy/plugins/grid/presentation/widgets/header/type_option/builder.dart';
|
||||
import 'package:flowy_sdk/log.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/select_type_option.pb.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'select_option_filter_list_bloc.freezed.dart';
|
||||
|
||||
class SelectOptionFilterListBloc<T>
|
||||
extends Bloc<SelectOptionFilterListEvent, SelectOptionFilterListState> {
|
||||
final SingleSelectTypeOptionContext typeOptionContext;
|
||||
SelectOptionFilterListBloc({
|
||||
required String viewId,
|
||||
required FieldPB fieldPB,
|
||||
required List<String> selectedOptionIds,
|
||||
}) : typeOptionContext = makeSingleSelectTypeOptionContext(
|
||||
gridId: viewId,
|
||||
fieldPB: fieldPB,
|
||||
),
|
||||
super(SelectOptionFilterListState.initial(selectedOptionIds)) {
|
||||
on<SelectOptionFilterListEvent>(
|
||||
(event, emit) async {
|
||||
await event.when(
|
||||
initial: () async {
|
||||
_startListening();
|
||||
_loadOptions();
|
||||
},
|
||||
selectOption: (option) {
|
||||
final selectedOptionIds = Set<String>.from(state.selectedOptionIds);
|
||||
selectedOptionIds.add(option.id);
|
||||
|
||||
_updateSelectOptions(
|
||||
selectedOptionIds: selectedOptionIds,
|
||||
emit: emit,
|
||||
);
|
||||
},
|
||||
unselectOption: (option) {
|
||||
final selectedOptionIds = Set<String>.from(state.selectedOptionIds);
|
||||
selectedOptionIds.remove(option.id);
|
||||
|
||||
_updateSelectOptions(
|
||||
selectedOptionIds: selectedOptionIds,
|
||||
emit: emit,
|
||||
);
|
||||
},
|
||||
didReceiveOptions: (newOptions) {
|
||||
List<SelectOptionPB> options = List.from(newOptions);
|
||||
options.retainWhere(
|
||||
(element) => element.name.contains(state.predicate));
|
||||
|
||||
final visibleOptions = options.map((option) {
|
||||
return VisibleSelectOption(
|
||||
option, state.selectedOptionIds.contains(option.id));
|
||||
}).toList();
|
||||
|
||||
emit(state.copyWith(
|
||||
options: options, visibleOptions: visibleOptions));
|
||||
},
|
||||
filterOption: (optionName) {
|
||||
_updateSelectOptions(predicate: optionName, emit: emit);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
return super.close();
|
||||
}
|
||||
|
||||
void _updateSelectOptions({
|
||||
String? predicate,
|
||||
Set<String>? selectedOptionIds,
|
||||
required Emitter<SelectOptionFilterListState> emit,
|
||||
}) {
|
||||
final List<VisibleSelectOption> visibleOptions = _makeVisibleOptions(
|
||||
predicate ?? state.predicate,
|
||||
selectedOptionIds ?? state.selectedOptionIds,
|
||||
);
|
||||
|
||||
emit(state.copyWith(
|
||||
predicate: predicate ?? state.predicate,
|
||||
visibleOptions: visibleOptions,
|
||||
selectedOptionIds: selectedOptionIds ?? state.selectedOptionIds,
|
||||
));
|
||||
}
|
||||
|
||||
List<VisibleSelectOption> _makeVisibleOptions(
|
||||
String predicate,
|
||||
Set<String> selectedOptionIds,
|
||||
) {
|
||||
List<SelectOptionPB> options = List.from(state.options);
|
||||
options.retainWhere((element) => element.name.contains(predicate));
|
||||
|
||||
return options.map((option) {
|
||||
return VisibleSelectOption(option, selectedOptionIds.contains(option.id));
|
||||
}).toList();
|
||||
}
|
||||
|
||||
void _loadOptions() {
|
||||
typeOptionContext.loadTypeOptionData(
|
||||
onCompleted: (value) {
|
||||
if (!isClosed) {
|
||||
add(SelectOptionFilterListEvent.didReceiveOptions(value.options));
|
||||
}
|
||||
},
|
||||
onError: (error) => Log.error(error),
|
||||
);
|
||||
}
|
||||
|
||||
void _startListening() {}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class SelectOptionFilterListEvent with _$SelectOptionFilterListEvent {
|
||||
const factory SelectOptionFilterListEvent.initial() = _Initial;
|
||||
const factory SelectOptionFilterListEvent.selectOption(
|
||||
SelectOptionPB option) = _SelectOption;
|
||||
const factory SelectOptionFilterListEvent.unselectOption(
|
||||
SelectOptionPB option) = _UnSelectOption;
|
||||
const factory SelectOptionFilterListEvent.didReceiveOptions(
|
||||
List<SelectOptionPB> options) = _DidReceiveOptions;
|
||||
const factory SelectOptionFilterListEvent.filterOption(String optionName) =
|
||||
_SelectOptionFilter;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class SelectOptionFilterListState with _$SelectOptionFilterListState {
|
||||
const factory SelectOptionFilterListState({
|
||||
required List<SelectOptionPB> options,
|
||||
required List<VisibleSelectOption> visibleOptions,
|
||||
required Set<String> selectedOptionIds,
|
||||
required String predicate,
|
||||
}) = _SelectOptionFilterListState;
|
||||
|
||||
factory SelectOptionFilterListState.initial(List<String> selectedOptionIds) {
|
||||
return SelectOptionFilterListState(
|
||||
options: [],
|
||||
predicate: '',
|
||||
visibleOptions: [],
|
||||
selectedOptionIds: selectedOptionIds.toSet(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class VisibleSelectOption {
|
||||
final SelectOptionPB optionPB;
|
||||
final bool isSelected;
|
||||
|
||||
VisibleSelectOption(this.optionPB, this.isSelected);
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
import 'package:app_flowy/plugins/grid/presentation/widgets/filter/filter_info.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/text_filter.pbserver.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/util.pb.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'dart:async';
|
||||
import 'filter_listener.dart';
|
||||
import 'filter_service.dart';
|
||||
|
||||
part 'text_filter_editor_bloc.freezed.dart';
|
||||
|
||||
class TextFilterEditorBloc
|
||||
extends Bloc<TextFilterEditorEvent, TextFilterEditorState> {
|
||||
final FilterInfo filterInfo;
|
||||
final FilterFFIService _ffiService;
|
||||
final FilterListener _listener;
|
||||
|
||||
TextFilterEditorBloc({required this.filterInfo})
|
||||
: _ffiService = FilterFFIService(viewId: filterInfo.viewId),
|
||||
_listener = FilterListener(
|
||||
viewId: filterInfo.viewId,
|
||||
filterId: filterInfo.filter.id,
|
||||
),
|
||||
super(TextFilterEditorState.initial(filterInfo)) {
|
||||
on<TextFilterEditorEvent>(
|
||||
(event, emit) async {
|
||||
event.when(
|
||||
initial: () async {
|
||||
_startListening();
|
||||
},
|
||||
updateCondition: (TextFilterCondition condition) {
|
||||
_ffiService.insertTextFilter(
|
||||
filterId: filterInfo.filter.id,
|
||||
fieldId: filterInfo.field.id,
|
||||
condition: condition,
|
||||
content: state.filter.content,
|
||||
);
|
||||
},
|
||||
updateContent: (content) {
|
||||
_ffiService.insertTextFilter(
|
||||
filterId: filterInfo.filter.id,
|
||||
fieldId: filterInfo.field.id,
|
||||
condition: state.filter.condition,
|
||||
content: content,
|
||||
);
|
||||
},
|
||||
delete: () {
|
||||
_ffiService.deleteFilter(
|
||||
fieldId: filterInfo.field.id,
|
||||
filterId: filterInfo.filter.id,
|
||||
fieldType: filterInfo.field.fieldType,
|
||||
);
|
||||
},
|
||||
didReceiveFilter: (FilterPB filter) {
|
||||
final filterInfo = state.filterInfo.copyWith(filter: filter);
|
||||
final textFilter = filterInfo.textFilter()!;
|
||||
emit(state.copyWith(
|
||||
filterInfo: filterInfo,
|
||||
filter: textFilter,
|
||||
));
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _startListening() {
|
||||
_listener.start(
|
||||
onDeleted: () {
|
||||
if (!isClosed) add(const TextFilterEditorEvent.delete());
|
||||
},
|
||||
onUpdated: (filter) {
|
||||
if (!isClosed) add(TextFilterEditorEvent.didReceiveFilter(filter));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
await _listener.stop();
|
||||
return super.close();
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class TextFilterEditorEvent with _$TextFilterEditorEvent {
|
||||
const factory TextFilterEditorEvent.initial() = _Initial;
|
||||
const factory TextFilterEditorEvent.didReceiveFilter(FilterPB filter) =
|
||||
_DidReceiveFilter;
|
||||
const factory TextFilterEditorEvent.updateCondition(
|
||||
TextFilterCondition condition) = _UpdateCondition;
|
||||
const factory TextFilterEditorEvent.updateContent(String content) =
|
||||
_UpdateContent;
|
||||
const factory TextFilterEditorEvent.delete() = _Delete;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class TextFilterEditorState with _$TextFilterEditorState {
|
||||
const factory TextFilterEditorState({
|
||||
required FilterInfo filterInfo,
|
||||
required TextFilterPB filter,
|
||||
}) = _GridFilterState;
|
||||
|
||||
factory TextFilterEditorState.initial(FilterInfo filterInfo) {
|
||||
return TextFilterEditorState(
|
||||
filterInfo: filterInfo,
|
||||
filter: filterInfo.textFilter()!,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -16,25 +16,24 @@ import 'row/row_service.dart';
|
||||
part 'grid_bloc.freezed.dart';
|
||||
|
||||
class GridBloc extends Bloc<GridEvent, GridState> {
|
||||
final GridDataController dataController;
|
||||
final GridController gridController;
|
||||
void Function()? _createRowOperation;
|
||||
|
||||
GridBloc({required ViewPB view})
|
||||
: dataController = GridDataController(view: view),
|
||||
super(GridState.initial(view.id)) {
|
||||
GridBloc({required ViewPB view, required this.gridController})
|
||||
: super(GridState.initial(view.id)) {
|
||||
on<GridEvent>(
|
||||
(event, emit) async {
|
||||
await event.when(
|
||||
initial: () async {
|
||||
_startListening();
|
||||
await _loadGrid(emit);
|
||||
await _openGrid(emit);
|
||||
},
|
||||
createRow: () {
|
||||
state.loadingState.when(
|
||||
loading: () {
|
||||
_createRowOperation = () => dataController.createRow();
|
||||
_createRowOperation = () => gridController.createRow();
|
||||
},
|
||||
finish: (_) => dataController.createRow(),
|
||||
finish: (_) => gridController.createRow(),
|
||||
);
|
||||
},
|
||||
deleteRow: (rowInfo) async {
|
||||
@@ -66,17 +65,17 @@ class GridBloc extends Bloc<GridEvent, GridState> {
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
await dataController.dispose();
|
||||
await gridController.dispose();
|
||||
return super.close();
|
||||
}
|
||||
|
||||
GridRowCache? getRowCache(String blockId, String rowId) {
|
||||
final GridBlockCache? blockCache = dataController.blocks[blockId];
|
||||
final GridBlockCache? blockCache = gridController.blocks[blockId];
|
||||
return blockCache?.rowCache;
|
||||
}
|
||||
|
||||
void _startListening() {
|
||||
dataController.addListener(
|
||||
gridController.addListener(
|
||||
onGridChanged: (grid) {
|
||||
if (!isClosed) {
|
||||
add(GridEvent.didReceiveGridUpdate(grid));
|
||||
@@ -95,8 +94,8 @@ class GridBloc extends Bloc<GridEvent, GridState> {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _loadGrid(Emitter<GridState> emit) async {
|
||||
final result = await dataController.loadData();
|
||||
Future<void> _openGrid(Emitter<GridState> emit) async {
|
||||
final result = await gridController.openGrid();
|
||||
result.fold(
|
||||
(grid) {
|
||||
if (_createRowOperation != null) {
|
||||
@@ -124,7 +123,7 @@ class GridEvent with _$GridEvent {
|
||||
RowsChangedReason listState,
|
||||
) = _DidReceiveRowUpdate;
|
||||
const factory GridEvent.didReceiveFieldUpdate(
|
||||
UnmodifiableListView<GridFieldContext> fields,
|
||||
List<FieldInfo> fields,
|
||||
) = _DidReceiveFieldUpdate;
|
||||
|
||||
const factory GridEvent.didReceiveGridUpdate(
|
||||
@@ -163,9 +162,9 @@ class GridLoadingState with _$GridLoadingState {
|
||||
}
|
||||
|
||||
class GridFieldEquatable extends Equatable {
|
||||
final UnmodifiableListView<GridFieldContext> _fields;
|
||||
final List<FieldInfo> _fields;
|
||||
const GridFieldEquatable(
|
||||
UnmodifiableListView<GridFieldContext> fields,
|
||||
List<FieldInfo> fields,
|
||||
) : _fields = fields;
|
||||
|
||||
@override
|
||||
@@ -182,6 +181,5 @@ class GridFieldEquatable extends Equatable {
|
||||
];
|
||||
}
|
||||
|
||||
UnmodifiableListView<GridFieldContext> get value =>
|
||||
UnmodifiableListView(_fields);
|
||||
UnmodifiableListView<FieldInfo> get value => UnmodifiableListView(_fields);
|
||||
}
|
||||
Loaded 100 of 679 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user