mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
Merge remote-tracking branch 'origin/main' into image_selection_area
This commit is contained in:
commit
9c40a501fe
2
.github/workflows/appflowy_editor_test.yml
vendored
2
.github/workflows/appflowy_editor_test.yml
vendored
@ -4,10 +4,12 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- "main"
|
||||
- "release/*"
|
||||
|
||||
pull_request:
|
||||
branches:
|
||||
- "main"
|
||||
- "release/*"
|
||||
paths:
|
||||
- "frontend/app_flowy/packages/appflowy_editor/**"
|
||||
|
||||
|
6
.github/workflows/ci.yaml
vendored
6
.github/workflows/ci.yaml
vendored
@ -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
|
||||
@ -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
|
||||
|
4
.github/workflows/dart_lint.yml
vendored
4
.github/workflows/dart_lint.yml
vendored
@ -9,12 +9,14 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- "main"
|
||||
- "release/*"
|
||||
paths:
|
||||
- "frontend/app_flowy/**"
|
||||
|
||||
pull_request:
|
||||
branches:
|
||||
- "main"
|
||||
- "release/*"
|
||||
paths:
|
||||
- "frontend/app_flowy/**"
|
||||
|
||||
@ -73,7 +75,7 @@ jobs:
|
||||
run: |
|
||||
cargo make --profile development-linux-x86_64 flowy-sdk-dev
|
||||
|
||||
- name: Code Generation
|
||||
- name: Flutter Code Generation
|
||||
working-directory: frontend/app_flowy
|
||||
run: |
|
||||
flutter packages pub run easy_localization:generate -f keys -o locale_keys.g.dart -S assets/translations -s en.json
|
||||
|
15
.github/workflows/dart_test.yml
vendored
15
.github/workflows/dart_test.yml
vendored
@ -1,15 +1,17 @@
|
||||
name: Unit test(Flutter)
|
||||
name: Frontend test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "main"
|
||||
- "release/*"
|
||||
paths:
|
||||
- "frontend/app_flowy/**"
|
||||
|
||||
pull_request:
|
||||
branches:
|
||||
- "main"
|
||||
- "release/*"
|
||||
paths:
|
||||
- "frontend/app_flowy/**"
|
||||
|
||||
@ -21,7 +23,6 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: "stable-2022-04-07"
|
||||
@ -53,10 +54,6 @@ jobs:
|
||||
working-directory: frontend
|
||||
run: |
|
||||
cargo install cargo-make
|
||||
|
||||
- name: Cargo make flowy dev
|
||||
working-directory: frontend
|
||||
run: |
|
||||
cargo make flowy_dev
|
||||
|
||||
- name: Flutter Deps
|
||||
@ -64,12 +61,12 @@ jobs:
|
||||
run: |
|
||||
flutter config --enable-linux-desktop
|
||||
|
||||
- name: Build FlowySDK
|
||||
- name: Build Test lib
|
||||
working-directory: frontend
|
||||
run: |
|
||||
cargo make --profile test-linux test-lib-build
|
||||
cargo make --profile test-linux build-test-lib
|
||||
|
||||
- name: Code Generation
|
||||
- name: Flutter Code Generation
|
||||
working-directory: frontend/app_flowy
|
||||
run: |
|
||||
flutter packages pub get
|
||||
|
79
.github/workflows/release.yml
vendored
79
.github/workflows/release.yml
vendored
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
|
2
.github/workflows/rust_coverage.yml
vendored
2
.github/workflows/rust_coverage.yml
vendored
@ -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/**"
|
||||
|
2
.github/workflows/rust_lint.yml
vendored
2
.github/workflows/rust_lint.yml
vendored
@ -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
.github/workflows/rust_test.yml
vendored
4
.github/workflows/rust_test.yml
vendored
@ -1,9 +1,10 @@
|
||||
name: Unit test(Rust)
|
||||
name: Backend test
|
||||
|
||||
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/**"
|
||||
|
40
CHANGELOG.md
40
CHANGELOG.md
@ -1,8 +1,24 @@
|
||||
# Release Notes
|
||||
|
||||
## Version 0.0.6.2 - 10/30/2022
|
||||
- Fix some bugs
|
||||
|
||||
## Version 0.0.6.1 - 10/26/2022
|
||||
### New features
|
||||
- Optimzie 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
|
||||
- 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
|
||||
|
||||
@ -16,7 +32,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
|
||||
|
||||
@ -27,19 +43,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
|
||||
@ -49,7 +65,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
|
||||
@ -61,7 +77,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
|
||||
@ -84,7 +100,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
|
||||
@ -108,7 +124,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
|
||||
@ -117,12 +133,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
|
||||
|
12
README.md
12
README.md
@ -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
|
||||
|
||||
|
@ -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.5.2"
|
||||
CURRENT_APP_VERSION = "0.0.6.2"
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
|
93
frontend/app_flowy/assets/google_fonts/Poppins/OFL.txt
Normal file
93
frontend/app_flowy/assets/google_fonts/Poppins/OFL.txt
Normal file
@ -0,0 +1,93 @@
|
||||
Copyright 2020 The Poppins Project Authors (https://github.com/itfoundry/Poppins)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
BIN
frontend/app_flowy/assets/google_fonts/Poppins/Poppins-Black.ttf
Normal file
BIN
frontend/app_flowy/assets/google_fonts/Poppins/Poppins-Black.ttf
Normal file
Binary file not shown.
Binary file not shown.
BIN
frontend/app_flowy/assets/google_fonts/Poppins/Poppins-Bold.ttf
Normal file
BIN
frontend/app_flowy/assets/google_fonts/Poppins/Poppins-Bold.ttf
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
frontend/app_flowy/assets/google_fonts/Poppins/Poppins-Light.ttf
Normal file
BIN
frontend/app_flowy/assets/google_fonts/Poppins/Poppins-Light.ttf
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
frontend/app_flowy/assets/google_fonts/Poppins/Poppins-Thin.ttf
Normal file
BIN
frontend/app_flowy/assets/google_fonts/Poppins/Poppins-Thin.ttf
Normal file
Binary file not shown.
Binary file not shown.
@ -221,7 +221,7 @@
|
||||
"menuName": "Grid"
|
||||
},
|
||||
"document": {
|
||||
"menuName": "Doc",
|
||||
"menuName": "Document",
|
||||
"date": {
|
||||
"timeHintTextInTwelveHour": "01:00 PM",
|
||||
"timeHintTextInTwentyFourHour": "13:00"
|
||||
@ -232,4 +232,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": {
|
||||
|
235
frontend/app_flowy/assets/translations/sv.json
Normal file
235
frontend/app_flowy/assets/translations/sv.json
Normal file
@ -0,0 +1,235 @@
|
||||
{
|
||||
"appName": "AppFlowy",
|
||||
"defaultUsername": "Jag",
|
||||
"welcomeText": "Välkommen till @:appName",
|
||||
"githubStarText": "Stärnmärk på GitHub",
|
||||
"subscribeNewsletterText": "Prenumerera på nyhetsbrev",
|
||||
"letsGoButtonText": "Kör igång",
|
||||
"title": "Namn",
|
||||
"signUp": {
|
||||
"buttonText": "Registrera dig",
|
||||
"title": "Registrera dig på @:appName",
|
||||
"getStartedText": "Sätt igång",
|
||||
"emptyPasswordError": "Lösenordet kan inte vara tomt",
|
||||
"repeatPasswordEmptyError": "Upprepat lösenord kan inte vara tomt",
|
||||
"unmatchedPasswordError": "Upprepat lösenord är inte samma som det första",
|
||||
"alreadyHaveAnAccount": "Har du redan ett konto?",
|
||||
"emailHint": "E-post",
|
||||
"passwordHint": "Lösenord",
|
||||
"repeatPasswordHint": "Uprepa lösenordet"
|
||||
},
|
||||
"signIn": {
|
||||
"loginTitle": "Logga in till @:appName",
|
||||
"loginButtonText": "Logga in",
|
||||
"buttonText": "Registrering",
|
||||
"forgotPassword": "Glömt lösenordet?",
|
||||
"emailHint": "E-post",
|
||||
"passwordHint": "Lösenord",
|
||||
"dontHaveAnAccount": "Har du inget konto?",
|
||||
"repeatPasswordEmptyError": "Upprepat lösenord kan inte vara tomt",
|
||||
"unmatchedPasswordError": "Upprepat lösenord är inte samma som det första"
|
||||
},
|
||||
"workspace": {
|
||||
"create": "Skapa arbetsyta",
|
||||
"hint": "Arbetsyta",
|
||||
"notFoundError": "Hittade ingen arbetsyta"
|
||||
},
|
||||
"shareAction": {
|
||||
"buttonText": "Dela",
|
||||
"workInProgress": "Kommer snart",
|
||||
"markdown": "Markdown",
|
||||
"copyLink": "Kopiera länk"
|
||||
},
|
||||
"disclosureAction": {
|
||||
"rename": "Byt namn",
|
||||
"delete": "Ta bort",
|
||||
"duplicate": "Klona"
|
||||
},
|
||||
"blankPageTitle": "Tom sida",
|
||||
"newPageText": "Ny sida",
|
||||
"trash": {
|
||||
"text": "Skräp",
|
||||
"restoreAll": "Återställ alla",
|
||||
"deleteAll": "Ta bort alla",
|
||||
"pageHeader": {
|
||||
"fileName": "Filnamn",
|
||||
"lastModified": "Ändrad",
|
||||
"created": "Skapad"
|
||||
}
|
||||
},
|
||||
"deletePagePrompt": {
|
||||
"text": "Denna sida är i skräpmappen",
|
||||
"restore": "Återställ sida",
|
||||
"deletePermanent": "Radera permanent"
|
||||
},
|
||||
"dialogCreatePageNameHint": "Sidnamn",
|
||||
"questionBubble": {
|
||||
"whatsNew": "Vad nytt?",
|
||||
"help": "Hjälp & Support",
|
||||
"debug": {
|
||||
"name": "Felsökningsinfo",
|
||||
"success": "Kopierade felsökningsinfo till urklipp!",
|
||||
"fail": "Kunde inte kopiera felsökningsinfo till urklipp"
|
||||
}
|
||||
},
|
||||
"menuAppHeader": {
|
||||
"addPageTooltip": "Lägg snabbt till en sida inuti",
|
||||
"defaultNewPageName": "Namnlös",
|
||||
"renameDialog": "Byt namn"
|
||||
},
|
||||
"toolbar": {
|
||||
"undo": "Ångra",
|
||||
"redo": "Upprepa",
|
||||
"bold": "Fet",
|
||||
"italic": "Kursiv",
|
||||
"underline": "Understruken",
|
||||
"strike": "Genomstruken",
|
||||
"numList": "Numrerad lista",
|
||||
"bulletList": "Punktlista",
|
||||
"checkList": "Checklista",
|
||||
"inlineCode": "Infogad kod",
|
||||
"quote": "Citatblock",
|
||||
"header": "Rubrik",
|
||||
"highlight": "Färgmarkera"
|
||||
},
|
||||
"tooltip": {
|
||||
"lightMode": "Växla till ljust läge",
|
||||
"darkMode": "Växla till mörkt läge",
|
||||
"openAsPage": "Öppna som sida",
|
||||
"addNewRow": "Lägg till ny rad",
|
||||
"openMenu": "Klicka för att öppna meny"
|
||||
},
|
||||
"sideBar": {
|
||||
"closeSidebar": "Stäng sidofältet",
|
||||
"openSidebar": "Öppna sidofältet"
|
||||
},
|
||||
"notifications": {
|
||||
"export": {
|
||||
"markdown": "Exporterade anteckning till Markdown",
|
||||
"path": "Dokument/flowy"
|
||||
}
|
||||
},
|
||||
"contactsPage": {
|
||||
"title": "Kontakter",
|
||||
"whatsHappening": "Vad händer denna vecka?",
|
||||
"addContact": "Lägg till kontakt",
|
||||
"editContact": "Redigera kontakt"
|
||||
},
|
||||
"button": {
|
||||
"OK": "OK",
|
||||
"Cancel": "Avbryt",
|
||||
"signIn": "Logga in",
|
||||
"signOut": "Logga ut",
|
||||
"complete": "Slutfört",
|
||||
"save": "Spara"
|
||||
},
|
||||
"label": {
|
||||
"welcome": "Välkommen!",
|
||||
"firstName": "Förnamn",
|
||||
"middleName": "Mellannamn",
|
||||
"lastName": "Efternamn",
|
||||
"stepX": "Steg {X}"
|
||||
},
|
||||
"oAuth": {
|
||||
"err": {
|
||||
"failedTitle": "Kan inte ansluta till ditt konto.",
|
||||
"failedMsg": "Tillse att du har slutfört registreringsprocessen i din webbläsare."
|
||||
},
|
||||
"google": {
|
||||
"title": "GOOGLE-inloggning",
|
||||
"instruction1": "För att kunna importera dina Google-kontakter, måste du auktorisera detta program med hjälp av din webbläsare.",
|
||||
"instruction2": "Kopiera den här koden till urklipp genom att klicka på ikonen eller genom att markera texten:",
|
||||
"instruction3": "Gå till följande länk i din webbläsare, och ange ovanstående kod:",
|
||||
"instruction4": "Tryck på nedanstående knapp när du slutfört registreringen:"
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"title": "Inställningar",
|
||||
"menu": {
|
||||
"appearance": "Utseende",
|
||||
"language": "Språk",
|
||||
"user": "Användare",
|
||||
"open": "Öppna inställningarna"
|
||||
},
|
||||
"appearance": {
|
||||
"lightLabel": "Ljust läge",
|
||||
"darkLabel": "Mörkt läge"
|
||||
}
|
||||
},
|
||||
"grid": {
|
||||
"settings": {
|
||||
"filter": "Filter",
|
||||
"sortBy": "Sortera efter",
|
||||
"Properties": "Egenskaper",
|
||||
"group": "Grupp"
|
||||
},
|
||||
"field": {
|
||||
"hide": "Dölj",
|
||||
"insertLeft": "Infoga till vänster",
|
||||
"insertRight": "Infoga till höger",
|
||||
"duplicate": "Klona",
|
||||
"delete": "Ta bort",
|
||||
"textFieldName": "Text",
|
||||
"checkboxFieldName": "Checkruta",
|
||||
"dateFieldName": "Datum",
|
||||
"numberFieldName": "Siffror",
|
||||
"singleSelectFieldName": "Välj",
|
||||
"multiSelectFieldName": "Välj flera",
|
||||
"urlFieldName": "URL",
|
||||
"numberFormat": " Sifferformat",
|
||||
"dateFormat": " Datumformat",
|
||||
"includeTime": " Inkludera tid",
|
||||
"dateFormatFriendly": "Månad Dag,År",
|
||||
"dateFormatISO": "År-Månad-Dag",
|
||||
"dateFormatLocal": "Månad/Dag/År",
|
||||
"dateFormatUS": "År/Månad/Dag",
|
||||
"timeFormat": " Tidsformat",
|
||||
"invalidTimeFormat": "Ogiltigt format",
|
||||
"timeFormatTwelveHour": "12-timmars",
|
||||
"timeFormatTwentyFourHour": "24-timmars",
|
||||
"addSelectOption": "Lägg till ett alternativ",
|
||||
"optionTitle": "Alternativ",
|
||||
"addOption": "Lägg till alternativ",
|
||||
"editProperty": "Redigera egenskap",
|
||||
"newColumn": "Ny kolumn",
|
||||
"deleteFieldPromptMessage": "Är du säker? Denna egenskap kommer att raderas."
|
||||
},
|
||||
"row": {
|
||||
"duplicate": "Klona",
|
||||
"delete": "Ta bort",
|
||||
"textPlaceholder": "Tom",
|
||||
"copyProperty": "Kopierade egenskap till urklipp",
|
||||
"count": "Antal",
|
||||
"newRow": "Ny rad"
|
||||
},
|
||||
"selectOption": {
|
||||
"create": "Skapa",
|
||||
"purpleColor": "Purpur",
|
||||
"pinkColor": "Rosa",
|
||||
"lightPinkColor": "Ljusrosa",
|
||||
"orangeColor": "Orange",
|
||||
"yellowColor": "Gul",
|
||||
"limeColor": "Lime",
|
||||
"greenColor": "Grön",
|
||||
"aquaColor": "Vatten",
|
||||
"blueColor": "Blå",
|
||||
"deleteTag": "Ta bort tagg",
|
||||
"colorPanelTitle": "Färger",
|
||||
"panelTitle": "Välj ett alternativ eller skapa ett",
|
||||
"searchOption": "Sök efter ett alternativ"
|
||||
},
|
||||
"menuName": "Tabell"
|
||||
},
|
||||
"document": {
|
||||
"menuName": "Dokument",
|
||||
"date": {
|
||||
"timeHintTextInTwelveHour": "01:00 PM",
|
||||
"timeHintTextInTwentyFourHour": "13:00"
|
||||
}
|
||||
},
|
||||
"board": {
|
||||
"column": {
|
||||
"create_new_card": "Nytt"
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
@ -284,8 +285,8 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
|
||||
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))),
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -32,7 +32,7 @@ class BoardNumberCellBloc
|
||||
cellController.removeListener(_onCellChangedFn!);
|
||||
_onCellChangedFn = null;
|
||||
}
|
||||
cellController.dispose();
|
||||
await cellController.dispose();
|
||||
return super.close();
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -66,6 +66,7 @@ class BoardCardBloc extends Bloc<BoardCardEvent, BoardCardState> {
|
||||
state.cells.map((cell) => cell.identifier.fieldContext).toList(),
|
||||
),
|
||||
rowPB: state.rowPB,
|
||||
visible: true,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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,11 +20,14 @@ class BoardPluginBuilder implements PluginBuilder {
|
||||
@override
|
||||
String get menuName => "Board";
|
||||
|
||||
@override
|
||||
String get menuIcon => "editor/board";
|
||||
|
||||
@override
|
||||
PluginType get pluginType => PluginType.board;
|
||||
|
||||
@override
|
||||
ViewDataTypePB get dataType => ViewDataTypePB.Database;
|
||||
ViewDataFormatPB get dataFormatType => ViewDataFormatPB.DatabaseFormat;
|
||||
|
||||
@override
|
||||
ViewLayoutTypePB? get layoutType => ViewLayoutTypePB.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,
|
||||
@ -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)),
|
||||
);
|
||||
@ -296,6 +287,7 @@ class _BoardContentState extends State<BoardContent> {
|
||||
gridId: gridId,
|
||||
fields: UnmodifiableListView(fieldController.fieldContexts),
|
||||
rowPB: rowPB,
|
||||
visible: true,
|
||||
);
|
||||
|
||||
final dataController = GridRowDataController(
|
||||
@ -329,15 +321,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,7 @@ 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/size.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 +95,6 @@ class _SettingItem extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = context.read<AppTheme>();
|
||||
final isSelected = context
|
||||
.read<BoardSettingBloc>()
|
||||
.state
|
||||
@ -108,16 +107,17 @@ class _SettingItem extends StatelessWidget {
|
||||
isSelected: isSelected,
|
||||
text: FlowyText.medium(
|
||||
action.title(),
|
||||
fontSize: 12,
|
||||
color: theme.textColor,
|
||||
fontSize: FontSizes.s12,
|
||||
),
|
||||
hoverColor: theme.hover,
|
||||
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,11 +1,9 @@
|
||||
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: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:flutter/material.dart';
|
||||
|
||||
import 'board_setting.dart';
|
||||
|
||||
@ -61,16 +59,17 @@ class _SettingButtonState extends State<_SettingButton> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = context.read<AppTheme>();
|
||||
return AppFlowyPopover(
|
||||
controller: popoverController,
|
||||
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: svgWidget(
|
||||
"grid/setting/setting",
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
),
|
||||
popupBuilder: (BuildContext popoverContext) {
|
||||
|
@ -1,28 +0,0 @@
|
||||
import 'dart:async';
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:flowy_sdk/dispatch/dispatch.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-document/protobuf.dart';
|
||||
|
||||
class ShareService {
|
||||
Future<Either<ExportDataPB, FlowyError>> export(
|
||||
String docId, ExportType type) {
|
||||
final request = ExportPayloadPB.create()
|
||||
..viewId = docId
|
||||
..exportType = type;
|
||||
|
||||
return DocumentEventExportDocument(request).send();
|
||||
}
|
||||
|
||||
Future<Either<ExportDataPB, FlowyError>> exportText(String docId) {
|
||||
return export(docId, ExportType.Text);
|
||||
}
|
||||
|
||||
Future<Either<ExportDataPB, FlowyError>> exportMarkdown(String docId) {
|
||||
return export(docId, ExportType.Markdown);
|
||||
}
|
||||
|
||||
Future<Either<ExportDataPB, FlowyError>> exportURL(String docId) {
|
||||
return export(docId, ExportType.Link);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
@ -1,11 +1,12 @@
|
||||
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';
|
||||
import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:flutter_quill/flutter_quill.dart' show Document, Delta;
|
||||
import 'package:flowy_sdk/log.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
@ -14,27 +15,26 @@ import 'dart:async';
|
||||
|
||||
part 'doc_bloc.freezed.dart';
|
||||
|
||||
typedef FlutterQuillDocument = Document;
|
||||
|
||||
class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
|
||||
final ViewPB view;
|
||||
final DocumentService service;
|
||||
final DocumentService _documentService;
|
||||
|
||||
final ViewListener listener;
|
||||
final TrashService trashService;
|
||||
late FlutterQuillDocument document;
|
||||
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 {
|
||||
await _initial(value, emit);
|
||||
_listenOnViewChange();
|
||||
},
|
||||
deleted: (Deleted value) async {
|
||||
emit(state.copyWith(isDeleted: true));
|
||||
@ -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,41 @@ 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 {
|
||||
listener.start(
|
||||
final result = await _documentService.openDocument(view: view);
|
||||
result.fold(
|
||||
(block) {
|
||||
final document = Document.fromJson(jsonDecode(block.snapshot));
|
||||
editorState = EditorState(document: document);
|
||||
_listenOnDocumentChange();
|
||||
emit(
|
||||
state.copyWith(
|
||||
loadingState: DocumentLoadingState.finish(left(unit)),
|
||||
),
|
||||
);
|
||||
},
|
||||
(err) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
loadingState: DocumentLoadingState.finish(right(err)),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _listenOnViewChange() {
|
||||
_listener.start(
|
||||
onViewDeleted: (result) {
|
||||
result.fold(
|
||||
(view) => add(const DocumentEvent.deleted()),
|
||||
@ -87,46 +110,20 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
|
||||
);
|
||||
},
|
||||
);
|
||||
final result = await service.openDocument(docId: view.id);
|
||||
result.fold(
|
||||
(block) {
|
||||
document = _decodeJsonToDocument(block.snapshot);
|
||||
_subscription = document.changes.listen((event) {
|
||||
final delta = event.item2;
|
||||
final documentDelta = document.toDelta();
|
||||
_composeDelta(delta, documentDelta);
|
||||
});
|
||||
emit(state.copyWith(
|
||||
loadingState: DocumentLoadingState.finish(left(unit))));
|
||||
},
|
||||
(err) {
|
||||
emit(state.copyWith(
|
||||
loadingState: DocumentLoadingState.finish(right(err))));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Document _decodeListToDocument(Uint8List data) {
|
||||
// final json = jsonDecode(utf8.decode(data));
|
||||
// final document = Document.fromJson(json);
|
||||
// return document;
|
||||
// }
|
||||
|
||||
void _composeDelta(Delta composedDelta, Delta documentDelta) async {
|
||||
final json = jsonEncode(composedDelta.toJson());
|
||||
Log.debug("doc_id: $view.id - Send json: $json");
|
||||
final result = await service.applyEdit(docId: view.id, data: json);
|
||||
|
||||
result.fold(
|
||||
(_) {},
|
||||
(r) => Log.error(r),
|
||||
);
|
||||
}
|
||||
|
||||
Document _decodeJsonToDocument(String data) {
|
||||
final json = jsonDecode(data);
|
||||
final document = Document.fromJson(json);
|
||||
return document;
|
||||
void _listenOnDocumentChange() {
|
||||
_subscription = editorState.transactionStream.listen((transaction) {
|
||||
final json = jsonEncode(TransactionAdaptor(transaction).toJson());
|
||||
_documentService
|
||||
.applyEdit(docId: view.id, operations: json)
|
||||
.then((result) {
|
||||
result.fold(
|
||||
(l) => null,
|
||||
(err) => Log.error(err),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -160,3 +157,44 @@ class DocumentLoadingState with _$DocumentLoadingState {
|
||||
const factory DocumentLoadingState.finish(
|
||||
Either<Unit, FlowyError> successOrFail) = _Finish;
|
||||
}
|
||||
|
||||
/// Uses to erase the different between appflowy editor and the backend
|
||||
class TransactionAdaptor {
|
||||
final Transaction transaction;
|
||||
TransactionAdaptor(this.transaction);
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
if (transaction.operations.isNotEmpty) {
|
||||
// The backend uses [0,0] as the beginning path, but the editor uses [0].
|
||||
// So it needs to extend the path by inserting `0` at the head for all
|
||||
// operations before passing to the backend.
|
||||
json['operations'] = transaction.operations
|
||||
.map((e) => e.copyWith(path: [0, ...e.path]).toJson())
|
||||
.toList();
|
||||
}
|
||||
if (transaction.afterSelection != null) {
|
||||
final selection = transaction.afterSelection!;
|
||||
final start = selection.start;
|
||||
final end = selection.end;
|
||||
json['after_selection'] = selection
|
||||
.copyWith(
|
||||
start: start.copyWith(path: [0, ...start.path]),
|
||||
end: end.copyWith(path: [0, ...end.path]),
|
||||
)
|
||||
.toJson();
|
||||
}
|
||||
if (transaction.beforeSelection != null) {
|
||||
final selection = transaction.beforeSelection!;
|
||||
final start = selection.start;
|
||||
final end = selection.end;
|
||||
json['before_selection'] = selection
|
||||
.copyWith(
|
||||
start: start.copyWith(path: [0, ...start.path]),
|
||||
end: end.copyWith(path: [0, ...end.path]),
|
||||
)
|
||||
.toJson();
|
||||
}
|
||||
return json;
|
||||
}
|
||||
}
|
@ -3,28 +3,35 @@ import 'package:flowy_sdk/dispatch/dispatch.dart';
|
||||
|
||||
import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-sync/document.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-document/entities.pb.dart';
|
||||
|
||||
class DocumentService {
|
||||
Future<Either<DocumentSnapshotPB, FlowyError>> openDocument({
|
||||
required String docId,
|
||||
required ViewPB view,
|
||||
}) async {
|
||||
await FolderEventSetLatestView(ViewIdPB(value: docId)).send();
|
||||
await FolderEventSetLatestView(ViewIdPB(value: view.id)).send();
|
||||
|
||||
final payload = OpenDocumentContextPB()
|
||||
..documentId = view.id
|
||||
..documentVersion = DocumentVersionPB.V1;
|
||||
// switch (view.dataFormat) {
|
||||
// case ViewDataFormatPB.DeltaFormat:
|
||||
// payload.documentVersion = DocumentVersionPB.V0;
|
||||
// break;
|
||||
// default:
|
||||
// break;
|
||||
// }
|
||||
|
||||
final payload = DocumentIdPB(value: docId);
|
||||
return DocumentEventGetDocument(payload).send();
|
||||
}
|
||||
|
||||
Future<Either<Unit, FlowyError>> applyEdit({
|
||||
required String docId,
|
||||
required String data,
|
||||
String operations = "",
|
||||
required String operations,
|
||||
}) {
|
||||
final payload = EditPayloadPB.create()
|
||||
..docId = docId
|
||||
..operations = operations
|
||||
..operationsStr = data;
|
||||
..operations = operations;
|
||||
return DocumentEventApplyEdit(payload).send();
|
||||
}
|
||||
|
@ -1,14 +1,14 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:app_flowy/startup/tasks/rust_sdk.dart';
|
||||
import 'package:app_flowy/workspace/application/markdown/delta_markdown.dart';
|
||||
import 'package:app_flowy/plugins/doc/application/share_service.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, documentToMarkdown;
|
||||
part 'share_bloc.freezed.dart';
|
||||
|
||||
class DocShareBloc extends Bloc<DocShareEvent, DocShareState> {
|
||||
@ -18,11 +18,14 @@ class DocShareBloc extends Bloc<DocShareEvent, DocShareState> {
|
||||
: super(const DocShareState.initial()) {
|
||||
on<DocShareEvent>((event, emit) async {
|
||||
await event.map(
|
||||
shareMarkdown: (ShareMarkdown value) async {
|
||||
await service.exportMarkdown(view.id).then((result) {
|
||||
shareMarkdown: (ShareMarkdown shareMarkdown) async {
|
||||
await service.exportMarkdown(view).then((result) {
|
||||
result.fold(
|
||||
(value) => emit(
|
||||
DocShareState.finish(left(_convertDeltaToMarkdown(value)))),
|
||||
DocShareState.finish(
|
||||
left(_saveMarkdown(value, shareMarkdown.path)),
|
||||
),
|
||||
),
|
||||
(error) => emit(DocShareState.finish(right(error))),
|
||||
);
|
||||
});
|
||||
@ -35,38 +38,23 @@ class DocShareBloc extends Bloc<DocShareEvent, DocShareState> {
|
||||
});
|
||||
}
|
||||
|
||||
ExportDataPB _convertDeltaToMarkdown(ExportDataPB value) {
|
||||
final result = deltaToMarkdown(value.data);
|
||||
value.data = result;
|
||||
writeFile(result);
|
||||
ExportDataPB _saveMarkdown(ExportDataPB value, String path) {
|
||||
final markdown = _convertDocumentToMarkdown(value);
|
||||
value.data = markdown;
|
||||
File(path).writeAsStringSync(markdown);
|
||||
return value;
|
||||
}
|
||||
|
||||
Future<Directory> get _exportDir async {
|
||||
Directory documentsDir = await appFlowyDocumentDirectory();
|
||||
|
||||
return documentsDir;
|
||||
}
|
||||
|
||||
Future<String> get _localPath async {
|
||||
final dir = await _exportDir;
|
||||
return dir.path;
|
||||
}
|
||||
|
||||
Future<File> get _localFile async {
|
||||
final path = await _localPath;
|
||||
return File('$path/${view.name}.md');
|
||||
}
|
||||
|
||||
Future<File> writeFile(String md) async {
|
||||
final file = await _localFile;
|
||||
return file.writeAsString(md);
|
||||
String _convertDocumentToMarkdown(ExportDataPB value) {
|
||||
final json = jsonDecode(value.data);
|
||||
final document = Document.fromJson(json);
|
||||
return documentToMarkdown(document);
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class DocShareEvent with _$DocShareEvent {
|
||||
const factory DocShareEvent.shareMarkdown() = ShareMarkdown;
|
||||
const factory DocShareEvent.shareMarkdown(String path) = ShareMarkdown;
|
||||
const factory DocShareEvent.shareText() = ShareText;
|
||||
const factory DocShareEvent.shareLink() = ShareLink;
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
import 'dart:async';
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:flowy_sdk/dispatch/dispatch.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-document/protobuf.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart';
|
||||
|
||||
class ShareService {
|
||||
Future<Either<ExportDataPB, FlowyError>> export(
|
||||
ViewPB view, ExportType type) {
|
||||
var payload = ExportPayloadPB.create()
|
||||
..viewId = view.id
|
||||
..exportType = type
|
||||
..documentVersion = DocumentVersionPB.V1;
|
||||
|
||||
return DocumentEventExportDocument(payload).send();
|
||||
}
|
||||
|
||||
Future<Either<ExportDataPB, FlowyError>> exportText(ViewPB view) {
|
||||
return export(view, ExportType.Text);
|
||||
}
|
||||
|
||||
Future<Either<ExportDataPB, FlowyError>> exportMarkdown(ViewPB view) {
|
||||
return export(view, ExportType.Markdown);
|
||||
}
|
||||
|
||||
Future<Either<ExportDataPB, FlowyError>> exportURL(ViewPB view) {
|
||||
return export(view, ExportType.Link);
|
||||
}
|
||||
}
|
@ -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';
|
||||
@ -14,8 +13,8 @@ import 'package:app_flowy/workspace/presentation/widgets/pop_up_action.dart';
|
||||
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';
|
||||
@ -23,7 +22,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';
|
||||
|
||||
@ -40,11 +38,14 @@ class DocumentPluginBuilder extends PluginBuilder {
|
||||
@override
|
||||
String get menuName => LocaleKeys.document_menuName.tr();
|
||||
|
||||
@override
|
||||
String get menuIcon => "editor/documents";
|
||||
|
||||
@override
|
||||
PluginType get pluginType => PluginType.editor;
|
||||
|
||||
@override
|
||||
ViewDataTypePB get dataType => ViewDataTypePB.Text;
|
||||
ViewDataFormatPB get dataFormatType => ViewDataFormatPB.TreeFormat;
|
||||
}
|
||||
|
||||
class DocumentPlugin extends Plugin<int> {
|
||||
@ -128,21 +129,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: const ShareActionList(),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
builder: (context, state) => ConstrainedBox(
|
||||
constraints: const BoxConstraints.expand(
|
||||
height: 30,
|
||||
width: 100,
|
||||
),
|
||||
child: ShareActionList(view: view),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
@ -165,11 +158,16 @@ class DocumentShareButton extends StatelessWidget {
|
||||
}
|
||||
|
||||
class ShareActionList extends StatelessWidget {
|
||||
const ShareActionList({Key? key}) : super(key: key);
|
||||
const ShareActionList({
|
||||
Key? key,
|
||||
required this.view,
|
||||
}) : super(key: key);
|
||||
|
||||
final ViewPB view;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = context.watch<AppTheme>();
|
||||
final docShareBloc = context.read<DocShareBloc>();
|
||||
return PopoverActionList<ShareActionWrapper>(
|
||||
direction: PopoverDirection.bottomWithCenterAligned,
|
||||
actions: ShareAction.values
|
||||
@ -178,20 +176,23 @@ class ShareActionList extends StatelessWidget {
|
||||
buildChild: (controller) {
|
||||
return RoundedTextButton(
|
||||
title: LocaleKeys.shareAction_buttonText.tr(),
|
||||
fontSize: 12,
|
||||
fontSize: FontSizes.s12,
|
||||
borderRadius: Corners.s6Border,
|
||||
color: theme.main1,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
onPressed: () => controller.show(),
|
||||
);
|
||||
},
|
||||
onSelected: (action, controller) {
|
||||
onSelected: (action, controller) async {
|
||||
switch (action.inner) {
|
||||
case ShareAction.markdown:
|
||||
context
|
||||
.read<DocShareBloc>()
|
||||
.add(const DocShareEvent.shareMarkdown());
|
||||
showMessageToast(
|
||||
'Exported to: ${LocaleKeys.notifications_export_path.tr()}');
|
||||
final exportPath = await FilePicker.platform.saveFile(
|
||||
dialogTitle: '',
|
||||
fileName: '${view.name}.md',
|
||||
);
|
||||
if (exportPath != null) {
|
||||
docShareBloc.add(DocShareEvent.shareMarkdown(exportPath));
|
||||
showMessageToast('Exported to: $exportPath');
|
||||
}
|
||||
break;
|
||||
case ShareAction.copyLink:
|
||||
NavigatorAlertDialog(
|
@ -1,17 +1,14 @@
|
||||
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/workspace/application/appearance.dart';
|
||||
import 'package:app_flowy/plugins/doc/presentation/banner.dart';
|
||||
import 'package:app_flowy/plugins/doc/presentation/toolbar/tool_bar.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/scrolling/styled_scroll_bar.dart';
|
||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||
import 'package:flutter_quill/flutter_quill.dart' as quill;
|
||||
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';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'application/doc_bloc.dart';
|
||||
import 'styles.dart';
|
||||
|
||||
class DocumentPage extends StatefulWidget {
|
||||
final VoidCallback onDeleted;
|
||||
@ -29,11 +26,12 @@ class DocumentPage extends StatefulWidget {
|
||||
|
||||
class _DocumentPageState extends State<DocumentPage> {
|
||||
late DocumentBloc documentBloc;
|
||||
final scrollController = ScrollController();
|
||||
final FocusNode _focusNode = FocusNode();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
// 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());
|
||||
super.initState();
|
||||
@ -48,9 +46,9 @@ class _DocumentPageState extends State<DocumentPage> {
|
||||
child:
|
||||
BlocBuilder<DocumentBloc, DocumentState>(builder: (context, state) {
|
||||
return state.loadingState.map(
|
||||
// loading: (_) => const FlowyProgressIndicator(),
|
||||
loading: (_) =>
|
||||
SizedBox.expand(child: Container(color: Colors.transparent)),
|
||||
loading: (_) => SizedBox.expand(
|
||||
child: Container(color: Colors.transparent),
|
||||
),
|
||||
finish: (result) => result.successOrFail.fold(
|
||||
(_) {
|
||||
if (state.forceClose) {
|
||||
@ -75,23 +73,12 @@ class _DocumentPageState extends State<DocumentPage> {
|
||||
}
|
||||
|
||||
Widget _renderDocument(BuildContext context, DocumentState state) {
|
||||
quill.QuillController controller = quill.QuillController(
|
||||
document: context.read<DocumentBloc>().document,
|
||||
selection: const TextSelection.collapsed(offset: 0),
|
||||
);
|
||||
return Column(
|
||||
children: [
|
||||
if (state.isDeleted) _renderBanner(context),
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
_renderEditor(controller),
|
||||
const VSpace(10),
|
||||
_renderToolbar(controller),
|
||||
const VSpace(10),
|
||||
],
|
||||
),
|
||||
// AppFlowy Editor
|
||||
_renderAppFlowyEditor(
|
||||
context.read<DocumentBloc>().editorState,
|
||||
),
|
||||
],
|
||||
);
|
||||
@ -107,36 +94,26 @@ class _DocumentPageState extends State<DocumentPage> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _renderEditor(quill.QuillController controller) {
|
||||
final editor = quill.QuillEditor(
|
||||
controller: controller,
|
||||
focusNode: _focusNode,
|
||||
scrollable: true,
|
||||
paintCursorAboveText: true,
|
||||
autoFocus: controller.document.isEmpty(),
|
||||
expands: false,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
readOnly: false,
|
||||
scrollBottomInset: 0,
|
||||
scrollController: scrollController,
|
||||
customStyles: customStyles(context),
|
||||
Widget _renderAppFlowyEditor(EditorState editorState) {
|
||||
final theme = Theme.of(context);
|
||||
final editor = AppFlowyEditor(
|
||||
editorState: editorState,
|
||||
autoFocus: editorState.document.isEmpty,
|
||||
customBuilders: {
|
||||
'horizontal_rule': HorizontalRuleWidgetBuilder(),
|
||||
},
|
||||
shortcutEvents: [
|
||||
insertHorizontalRule,
|
||||
],
|
||||
themeData: theme.copyWith(extensions: [
|
||||
...theme.extensions.values,
|
||||
customEditorTheme(context),
|
||||
...customPluginTheme(context),
|
||||
]),
|
||||
);
|
||||
|
||||
return Expanded(
|
||||
child: ScrollbarListStack(
|
||||
axis: Axis.vertical,
|
||||
controller: scrollController,
|
||||
barSize: 6.0,
|
||||
child: SizedBox.expand(child: editor),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _renderToolbar(quill.QuillController controller) {
|
||||
return ChangeNotifierProvider.value(
|
||||
value: Provider.of<AppearanceSetting>(context, listen: true),
|
||||
child: EditorToolbar.basic(
|
||||
controller: controller,
|
||||
child: SizedBox.expand(
|
||||
child: editor,
|
||||
),
|
||||
);
|
||||
}
|
65
frontend/app_flowy/lib/plugins/document/editor_styles.dart
Normal file
65
frontend/app_flowy/lib/plugins/document/editor_styles.dart
Normal file
@ -0,0 +1,65 @@
|
||||
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(
|
||||
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);
|
||||
}
|
@ -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,
|
||||
)),
|
||||
],
|
||||
),
|
||||
),
|
@ -0,0 +1,168 @@
|
||||
import 'dart:collection';
|
||||
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
ShortcutEvent insertHorizontalRule = ShortcutEvent(
|
||||
key: 'Horizontal rule',
|
||||
command: 'Minus',
|
||||
handler: _insertHorzaontalRule,
|
||||
);
|
||||
|
||||
ShortcutEventHandler _insertHorzaontalRule = (editorState, event) {
|
||||
final selection = editorState.service.selectionService.currentSelection.value;
|
||||
final textNodes = editorState.service.selectionService.currentSelectedNodes
|
||||
.whereType<TextNode>();
|
||||
if (textNodes.length != 1 || selection == null) {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
final textNode = textNodes.first;
|
||||
if (textNode.toPlainText() == '--') {
|
||||
final transaction = editorState.transaction
|
||||
..deleteText(textNode, 0, 2)
|
||||
..insertNode(
|
||||
textNode.path,
|
||||
Node(
|
||||
type: 'horizontal_rule',
|
||||
children: LinkedList(),
|
||||
attributes: {},
|
||||
),
|
||||
)
|
||||
..afterSelection =
|
||||
Selection.single(path: textNode.path.next, startOffset: 0);
|
||||
editorState.apply(transaction);
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
return KeyEventResult.ignored;
|
||||
};
|
||||
|
||||
SelectionMenuItem horizontalRuleMenuItem = SelectionMenuItem(
|
||||
name: () => 'Horizontal rule',
|
||||
icon: (editorState, onSelected) => Icon(
|
||||
Icons.horizontal_rule,
|
||||
color: onSelected
|
||||
? editorState.editorStyle.selectionMenuItemSelectedIconColor
|
||||
: editorState.editorStyle.selectionMenuItemIconColor,
|
||||
size: 18.0,
|
||||
),
|
||||
keywords: ['horizontal rule'],
|
||||
handler: (editorState, _, __) {
|
||||
final selection =
|
||||
editorState.service.selectionService.currentSelection.value;
|
||||
final textNodes = editorState.service.selectionService.currentSelectedNodes
|
||||
.whereType<TextNode>();
|
||||
if (selection == null || textNodes.isEmpty) {
|
||||
return;
|
||||
}
|
||||
final textNode = textNodes.first;
|
||||
if (textNode.toPlainText().isEmpty) {
|
||||
final transaction = editorState.transaction
|
||||
..insertNode(
|
||||
textNode.path,
|
||||
Node(
|
||||
type: 'horizontal_rule',
|
||||
children: LinkedList(),
|
||||
attributes: {},
|
||||
),
|
||||
)
|
||||
..afterSelection =
|
||||
Selection.single(path: textNode.path.next, startOffset: 0);
|
||||
editorState.apply(transaction);
|
||||
} else {
|
||||
final transaction = editorState.transaction
|
||||
..insertNode(
|
||||
selection.end.path.next,
|
||||
TextNode(
|
||||
children: LinkedList(),
|
||||
attributes: {
|
||||
'subtype': 'horizontal_rule',
|
||||
},
|
||||
delta: Delta()..insert('---'),
|
||||
),
|
||||
)
|
||||
..afterSelection = selection;
|
||||
editorState.apply(transaction);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
class HorizontalRuleWidgetBuilder extends NodeWidgetBuilder<Node> {
|
||||
@override
|
||||
Widget build(NodeWidgetContext<Node> context) {
|
||||
return _HorizontalRuleWidget(
|
||||
key: context.node.key,
|
||||
node: context.node,
|
||||
editorState: context.editorState,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
NodeValidator<Node> get nodeValidator => (node) {
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
class _HorizontalRuleWidget extends StatefulWidget {
|
||||
const _HorizontalRuleWidget({
|
||||
Key? key,
|
||||
required this.node,
|
||||
required this.editorState,
|
||||
}) : super(key: key);
|
||||
|
||||
final Node node;
|
||||
final EditorState editorState;
|
||||
|
||||
@override
|
||||
State<_HorizontalRuleWidget> createState() => __HorizontalRuleWidgetState();
|
||||
}
|
||||
|
||||
class __HorizontalRuleWidgetState extends State<_HorizontalRuleWidget>
|
||||
with SelectableMixin {
|
||||
RenderBox get _renderBox => context.findRenderObject() as RenderBox;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 10),
|
||||
child: Container(
|
||||
height: 1,
|
||||
color: Colors.grey,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Position start() => Position(path: widget.node.path, offset: 0);
|
||||
|
||||
@override
|
||||
Position end() => Position(path: widget.node.path, offset: 1);
|
||||
|
||||
@override
|
||||
Position getPositionInOffset(Offset start) => end();
|
||||
|
||||
@override
|
||||
bool get shouldCursorBlink => false;
|
||||
|
||||
@override
|
||||
CursorStyle get cursorStyle => CursorStyle.borderLine;
|
||||
|
||||
@override
|
||||
Rect? getCursorRectInPosition(Position position) {
|
||||
final size = _renderBox.size;
|
||||
return Rect.fromLTWH(-size.width / 2.0, 0, size.width, size.height);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Rect> getRectsInSelection(Selection selection) =>
|
||||
[Offset.zero & _renderBox.size];
|
||||
|
||||
@override
|
||||
Selection getSelectionInRange(Offset start, Offset end) => Selection.single(
|
||||
path: widget.node.path,
|
||||
startOffset: 0,
|
||||
endOffset: 1,
|
||||
);
|
||||
|
||||
@override
|
||||
Offset localToGlobal(Offset offset) => _renderBox.localToGlobal(offset);
|
||||
}
|
@ -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),
|
||||
|
@ -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,20 +290,20 @@ 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;
|
||||
}
|
||||
}
|
||||
@ -329,7 +329,7 @@ class GridCellFieldNotifierImpl extends IGridCellFieldNotifier {
|
||||
|
||||
@override
|
||||
void onCellFieldChanged(void Function(FieldPB p1) callback) {
|
||||
_onChangesetFn = (FieldChangesetPB changeset) {
|
||||
_onChangesetFn = (GridFieldChangesetPB changeset) {
|
||||
for (final updatedField in changeset.updatedFields) {
|
||||
callback(updatedField);
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -31,7 +31,7 @@ class DateCellBloc extends Bloc<DateCellEvent, DateCellState> {
|
||||
cellController.removeListener(_onCellChangedFn!);
|
||||
_onCellChangedFn = null;
|
||||
}
|
||||
cellController.dispose();
|
||||
await cellController.dispose();
|
||||
return super.close();
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,6 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:flowy_sdk/log.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/select_type_option.pb.dart';
|
||||
@ -46,19 +45,29 @@ class SelectOptionCellEditorBloc
|
||||
));
|
||||
},
|
||||
deleteOption: (_DeleteOption value) {
|
||||
_deleteOption(value.option);
|
||||
_deleteOption([value.option]);
|
||||
},
|
||||
deleteAllOptions: (_DeleteAllOptions value) {
|
||||
if (state.allOptions.isNotEmpty) {
|
||||
_deleteOption(state.allOptions);
|
||||
}
|
||||
},
|
||||
updateOption: (_UpdateOption value) {
|
||||
_updateOption(value.option);
|
||||
},
|
||||
selectOption: (_SelectOption value) {
|
||||
_onSelectOption(value.optionId);
|
||||
_selectOptionService.select(optionIds: [value.optionId]);
|
||||
},
|
||||
unSelectOption: (_UnSelectOption value) {
|
||||
_selectOptionService.unSelect(optionIds: [value.optionId]);
|
||||
},
|
||||
trySelectOption: (_TrySelectOption value) {
|
||||
_trySelectOption(value.optionName, emit);
|
||||
},
|
||||
selectMultipleOptions: (_SelectMultipleOptions value) {
|
||||
_selectMultipleOptions(value.optionNames);
|
||||
if (value.optionNames.isNotEmpty) {
|
||||
_selectMultipleOptions(value.optionNames);
|
||||
}
|
||||
_filterOption(value.remainder, emit);
|
||||
},
|
||||
filterOption: (_SelectOptionFilter value) {
|
||||
@ -72,7 +81,7 @@ class SelectOptionCellEditorBloc
|
||||
@override
|
||||
Future<void> close() async {
|
||||
_delayOperation?.cancel();
|
||||
cellController.dispose();
|
||||
await cellController.dispose();
|
||||
return super.close();
|
||||
}
|
||||
|
||||
@ -81,11 +90,8 @@ class SelectOptionCellEditorBloc
|
||||
result.fold((l) => {}, (err) => Log.error(err));
|
||||
}
|
||||
|
||||
void _deleteOption(SelectOptionPB option) async {
|
||||
final result = await _selectOptionService.delete(
|
||||
option: option,
|
||||
);
|
||||
|
||||
void _deleteOption(List<SelectOptionPB> options) async {
|
||||
final result = await _selectOptionService.delete(options: options);
|
||||
result.fold((l) => null, (err) => Log.error(err));
|
||||
}
|
||||
|
||||
@ -97,16 +103,6 @@ class SelectOptionCellEditorBloc
|
||||
result.fold((l) => null, (err) => Log.error(err));
|
||||
}
|
||||
|
||||
void _onSelectOption(String optionId) {
|
||||
final hasSelected = state.selectedOptions
|
||||
.firstWhereOrNull((option) => option.id == optionId);
|
||||
if (hasSelected != null) {
|
||||
_selectOptionService.unSelect(optionIds: [optionId]);
|
||||
} else {
|
||||
_selectOptionService.select(optionIds: [optionId]);
|
||||
}
|
||||
}
|
||||
|
||||
void _trySelectOption(
|
||||
String optionName, Emitter<SelectOptionEditorState> emit) {
|
||||
SelectOptionPB? matchingOption;
|
||||
@ -138,9 +134,19 @@ class SelectOptionCellEditorBloc
|
||||
}
|
||||
|
||||
void _selectMultipleOptions(List<String> optionNames) {
|
||||
final optionIds = state.options
|
||||
.where((e) => optionNames.contains(e.name))
|
||||
.map((e) => e.id);
|
||||
// The options are unordered. So in order to keep the inserted [optionNames]
|
||||
// order, it needs to get the option id in the [optionNames] order.
|
||||
final lowerCaseNames = optionNames.map((e) => e.toLowerCase());
|
||||
final Map<String, String> optionIdsMap = {};
|
||||
for (final option in state.options) {
|
||||
optionIdsMap[option.name.toLowerCase()] = option.id;
|
||||
}
|
||||
|
||||
final optionIds = lowerCaseNames
|
||||
.where((name) => optionIdsMap[name] != null)
|
||||
.map((name) => optionIdsMap[name]!)
|
||||
.toList();
|
||||
|
||||
_selectOptionService.select(optionIds: optionIds);
|
||||
}
|
||||
|
||||
@ -162,8 +168,10 @@ class SelectOptionCellEditorBloc
|
||||
return;
|
||||
}
|
||||
return result.fold(
|
||||
(data) => add(SelectOptionEditorEvent.didReceiveOptions(
|
||||
data.options, data.selectOptions)),
|
||||
(data) => add(
|
||||
SelectOptionEditorEvent.didReceiveOptions(
|
||||
data.options, data.selectOptions),
|
||||
),
|
||||
(err) {
|
||||
Log.error(err);
|
||||
return null;
|
||||
@ -225,10 +233,13 @@ class SelectOptionEditorEvent with _$SelectOptionEditorEvent {
|
||||
_NewOption;
|
||||
const factory SelectOptionEditorEvent.selectOption(String optionId) =
|
||||
_SelectOption;
|
||||
const factory SelectOptionEditorEvent.unSelectOption(String optionId) =
|
||||
_UnSelectOption;
|
||||
const factory SelectOptionEditorEvent.updateOption(SelectOptionPB option) =
|
||||
_UpdateOption;
|
||||
const factory SelectOptionEditorEvent.deleteOption(SelectOptionPB option) =
|
||||
_DeleteOption;
|
||||
const factory SelectOptionEditorEvent.deleteAllOptions() = _DeleteAllOptions;
|
||||
const factory SelectOptionEditorEvent.filterOption(String optionName) =
|
||||
_SelectOptionFilter;
|
||||
const factory SelectOptionEditorEvent.trySelectOption(String optionName) =
|
||||
|
@ -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,24 +39,23 @@ 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();
|
||||
}
|
||||
|
||||
Future<Either<Unit, FlowyError>> delete({
|
||||
required SelectOptionPB option,
|
||||
}) {
|
||||
final payload = SelectOptionChangesetPayloadPB.create()
|
||||
..deleteOptions.add(option)
|
||||
Future<Either<Unit, FlowyError>> delete(
|
||||
{required Iterable<SelectOptionPB> options}) {
|
||||
final payload = SelectOptionChangesetPB.create()
|
||||
..deleteOptions.addAll(options)
|
||||
..cellIdentifier = _cellIdentifier();
|
||||
|
||||
return GridEventUpdateSelectOption(payload).send();
|
||||
}
|
||||
|
||||
Future<Either<SelectOptionCellDataPB, FlowyError>> getOptionContext() {
|
||||
final payload = GridCellIdPB.create()
|
||||
final payload = CellPathPB.create()
|
||||
..gridId = gridId
|
||||
..fieldId = fieldId
|
||||
..rowId = rowId;
|
||||
@ -66,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();
|
||||
@ -74,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();
|
||||
}
|
||||
|
||||
|
@ -11,9 +11,16 @@ class FieldActionSheetBloc
|
||||
extends Bloc<FieldActionSheetEvent, FieldActionSheetState> {
|
||||
final FieldService fieldService;
|
||||
|
||||
FieldActionSheetBloc({required FieldPB field, required this.fieldService})
|
||||
: super(FieldActionSheetState.initial(
|
||||
FieldTypeOptionDataPB.create()..field_2 = field)) {
|
||||
FieldActionSheetBloc({required GridFieldCellContext fieldCellContext})
|
||||
: fieldService = FieldService(
|
||||
gridId: fieldCellContext.gridId,
|
||||
fieldId: fieldCellContext.field.id,
|
||||
),
|
||||
super(
|
||||
FieldActionSheetState.initial(
|
||||
TypeOptionPB.create()..field_2 = fieldCellContext.field,
|
||||
),
|
||||
) {
|
||||
on<FieldActionSheetEvent>(
|
||||
(event, emit) async {
|
||||
await event.map(
|
||||
@ -31,6 +38,13 @@ class FieldActionSheetBloc
|
||||
(err) => Log.error(err),
|
||||
);
|
||||
},
|
||||
showField: (_ShowField value) async {
|
||||
final result = await fieldService.updateField(visibility: true);
|
||||
result.fold(
|
||||
(l) => null,
|
||||
(err) => Log.error(err),
|
||||
);
|
||||
},
|
||||
deleteField: (_DeleteField value) async {
|
||||
final result = await fieldService.deleteField();
|
||||
result.fold(
|
||||
@ -62,6 +76,7 @@ class FieldActionSheetEvent with _$FieldActionSheetEvent {
|
||||
const factory FieldActionSheetEvent.updateFieldName(String name) =
|
||||
_UpdateFieldName;
|
||||
const factory FieldActionSheetEvent.hideField() = _HideField;
|
||||
const factory FieldActionSheetEvent.showField() = _ShowField;
|
||||
const factory FieldActionSheetEvent.duplicateField() = _DuplicateField;
|
||||
const factory FieldActionSheetEvent.deleteField() = _DeleteField;
|
||||
const factory FieldActionSheetEvent.saveField() = _SaveField;
|
||||
@ -70,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: '',
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user