Merge branch 'main' into #692

This commit is contained in:
Naughtz 2022-08-22 11:06:44 +08:00 committed by GitHub
commit 6dca0fddf9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
608 changed files with 18894 additions and 8884 deletions

View File

@ -3,11 +3,11 @@ name: CI
on:
push:
branches:
- 'main'
- "main"
pull_request:
branches:
- 'main'
- "main"
jobs:
build:
@ -23,36 +23,37 @@ jobs:
steps:
- uses: actions/checkout@v2
- id: rust_toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: 'stable-2022-01-20'
toolchain: "stable-2022-01-20"
- id: flutter
uses: subosito/flutter-action@v2
with:
channel: 'stable'
channel: "stable"
cache: true
flutter-version: '3.0.5'
flutter-version: "3.0.5"
- name: Cache Cargo
id: cache-cargo
uses: actions/cache@v2
with:
with:
path: |
~/.cargo
key: ${{ runner.os }}-cargo-${{ steps.rust_toolchain.outputs.rustc_hash }}-${{ hashFiles('./frontend/rust-lib/Cargo.toml') }}
- name: Cache Rust
uses: actions/cache@v2
with:
with:
path: |
frontend/rust-lib/target
shared-lib/target
key: ${{ runner.os }}-rust-rust-lib-share-lib-${{ steps.rust_toolchain.outputs.rustc_hash }}-${{ hashFiles('./frontend/rust-lib/Cargo.toml') }}
key: ${{ runner.os }}-rust-rust-lib-share-lib-${{ steps.rust_toolchain.outputs.rustc_hash }}-${{ hashFiles('./frontend/rust-lib/Cargo.toml') }}
- name: Setup Environment
run: |
run: |
if [ "$RUNNER_OS" == "Linux" ]; then
sudo wget -qO /etc/apt/trusted.gpg.d/dart_linux_signing_key.asc https://dl-ssl.google.com/linux/linux_signing_key.pub
sudo wget -qO /etc/apt/sources.list.d/dart_stable.list https://storage.googleapis.com/download.dartlang.org/linux/debian/dart_stable.list
@ -64,11 +65,16 @@ jobs:
fi
shell: bash
- name: Deps
- if: steps.cache-cargo.outputs.cache-hit != 'true'
name: Deps
working-directory: frontend
run: |
cargo install cargo-make
cargo install duckscript_cli
- name: Cargo make flowy_dev
working-directory: frontend
run: |
cargo make flowy_dev
- name: Config Flutter

View File

@ -7,14 +7,14 @@ name: Flutter lint
on:
push:
branches: [ main ]
branches: [main]
pull_request:
branches: [ main ]
branches: [main]
env:
CARGO_TERM_COLOR: always
jobs:
jobs:
flutter-analyze:
name: flutter analyze
runs-on: ubuntu-latest
@ -23,16 +23,38 @@ jobs:
uses: actions/checkout@v2
- uses: subosito/flutter-action@v1
with:
flutter-version: '3.0.5'
flutter-version: "3.0.5"
channel: "stable"
- uses: actions-rs/toolchain@v1
with:
toolchain: 'stable-2022-01-20'
toolchain: "stable-2022-01-20"
- name: Rust Deps
- name: Cache Cargo
id: cache-cargo
uses: actions/cache@v2
with:
path: |
~/.cargo
key: ${{ runner.os }}-cargo-${{ steps.rust_toolchain.outputs.rustc_hash }}-${{ hashFiles('./frontend/rust-lib/Cargo.toml') }}
- name: Cache Rust
id: cache-rust-target
uses: actions/cache@v2
with:
path: |
frontend/rust-lib/target
shared-lib/target
key: ${{ runner.os }}-rust-rust-lib-share-lib-${{ steps.rust_toolchain.outputs.rustc_hash }}-${{ hashFiles('./frontend/rust-lib/Cargo.toml') }}
- if: steps.cache-cargo.outputs.cache-hit != 'true'
name: Rust Deps
working-directory: frontend
run: |
cargo install cargo-make
- name: Cargo make flowy dev
working-directory: frontend
run: |
cargo make flowy_dev
- name: Flutter Deps
@ -53,4 +75,3 @@ jobs:
- name: Run Flutter Analyzer
working-directory: frontend/app_flowy
run: flutter analyze

View File

@ -3,12 +3,12 @@ name: Unit test(Flutter)
on:
push:
branches:
- 'main'
- "main"
pull_request:
branches:
- 'main'
- 'feat/flowy_editor'
- "main"
- "feat/flowy_editor"
env:
CARGO_TERM_COLOR: always
@ -18,42 +18,49 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
toolchain: 'stable-2022-01-20'
toolchain: "stable-2022-01-20"
- uses: subosito/flutter-action@v2
with:
channel: 'stable'
flutter-version: '3.0.5'
channel: "stable"
flutter-version: "3.0.5"
cache: true
- name: Cache Cargo
uses: actions/cache@v2
with:
with:
path: |
~/.cargo
key: ${{ runner.os }}-cargo-${{ steps.rust_toolchain.outputs.rustc_hash }}-${{ hashFiles('./frontend/rust-lib/Cargo.toml') }}
- name: Cache Rust
id: cache-rust-target
uses: actions/cache@v2
with:
with:
path: |
frontend/rust-lib/target
shared-lib/target
key: ${{ runner.os }}-rust-rust-lib-share-lib-${{ steps.rust_toolchain.outputs.rustc_hash }}-${{ hashFiles('./frontend/rust-lib/Cargo.toml') }}
key: ${{ runner.os }}-rust-rust-lib-share-lib-${{ steps.rust_toolchain.outputs.rustc_hash }}-${{ hashFiles('./frontend/rust-lib/Cargo.toml') }}
- if: steps.cache-cargo.outputs.cache-hit != 'true'
name: Rust Deps
working-directory: frontend
run: |
cargo install cargo-make
- name: Cargo make flowy dev
working-directory: frontend
run: |
cargo make flowy_dev
- name: Flutter Deps
working-directory: frontend/app_flowy
run: |
flutter config --enable-linux-desktop
- name: Rust Deps
working-directory: frontend
run: |
cargo install cargo-make
cargo make flowy_dev
- name: Build FlowySDK
working-directory: frontend
run: |
@ -65,15 +72,9 @@ jobs:
flutter packages pub get
flutter packages pub run easy_localization:generate -f keys -o locale_keys.g.dart -S assets/translations -s en.json
flutter packages pub run build_runner build --delete-conflicting-outputs
- name: Run bloc tests
working-directory: frontend/app_flowy
run: |
flutter pub get
flutter test
- name: Run FlowyEditor tests
working-directory: frontend/app_flowy/packages/flowy_editor
run: |
flutter pub get
flutter test

36
.github/workflows/flowy_editor_test.yml vendored Normal file
View File

@ -0,0 +1,36 @@
name: FlowyEditor test
on:
push:
branches:
- "main"
pull_request:
branches:
- "main"
env:
CARGO_TERM_COLOR: always
jobs:
tests:
strategy:
matrix:
os: [macos-latest, ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v2
- uses: subosito/flutter-action@v2
with:
channel: "stable"
flutter-version: "3.0.5"
cache: true
- name: Run FlowyEditor tests
working-directory: frontend/app_flowy/packages/appflowy_editor
run: |
flutter pub get
flutter test

View File

@ -29,7 +29,7 @@
"program": "./lib/main.dart",
"type": "dart",
"env": {
"RUST_LOG": "debug"
"RUST_LOG": "trace"
},
"cwd": "${workspaceRoot}/app_flowy"
},
@ -44,7 +44,7 @@
"type": "dart",
"preLaunchTask": "AF: Clean + Rebuild All",
"env": {
"RUST_LOG": "info"
"RUST_LOG": "trace"
},
"cwd": "${workspaceRoot}/app_flowy"
},

View File

@ -14,7 +14,7 @@ analyzer:
exclude:
- "**/*.g.dart"
- "**/*.freezed.dart"
- "packages/flowy_editor/**"
- "packages/appflowy_editor/**"
- "packages/editor/**"
# - "packages/flowy_infra_ui/**"
linter:

View File

@ -0,0 +1,32 @@
<svg id="emoji" viewBox="0 0 72 72" xmlns="http://www.w3.org/2000/svg">
<g id="color">
<path fill="#E27022" stroke="none" d="M19.3685,9.2168c0,0-8.31-0.7683-4.7954,9.1741c0,0,2.4911,6.1326,4.7954,7.3748 c0,0-9.3677,6.4041-3.7409,18.5692l0.6476,3.8526c0,0,3.75,5.5625,8.5,5.6875l1.75,1.625c0,0,1.1875,3.5625,2.125,4 c0.9375,0.4375,5.4375,3.0289,5.4375,3.0289l4.3582-0.2763l5.4543-2.5651c0,0,2.4375-2.0625,2.4375-4.6875l1.0021-1.375 c0,0,6.1854-1.2168,8.6229-5.8834l1.9107-7.1166v-4.5268l-0.3757-2.8898l-2.3475-5.8333l-1.5625-1.75c0,0,1.3125-2.2252,2-3.3001 c0.6875-1.0749,2.2857-4.745,2.2857-4.745l1.4018-3.3299l-1.4018-3.5l-3.9951-1.547l-2.3531,0.8595l-5.4035,5.7083 c0,0-5.5911-3.0058-9.1563-2.7633c-3.5652,0.2425-11.0354,2.43-11.0354,2.43L19.3685,9.2168z"/>
<path fill="#FFFFFF" stroke="none" d="M38.3377,29.5055c0,0,4.1875,3.7873,4.5,8.1312c0,0,4,3.7631,5.9375-1.9828l2.8125-6.4229 c0,0-1.9316-3.5892-8.125-3.7797L38.3377,29.5055z"/>
<path fill="#FFFFFF" stroke="none" d="M33.8377,29.5695c0,0-3.0625,3.7797-3.375,8.1236c0,0-4,3.7631-5.9375-1.9828l-4.125-6.4793 c0,0,2.3691-4.0405,8.5625-4.231L33.8377,29.5695z"/>
<path fill="#FFFFFF" stroke="none" d="M52.2127,33.5c0,0.3125,0.125,12-11.0625,13.375c0,0-6.625-2.125-10.625,0.375 c0,0-11.4375-4.3125-10.9375-13.875l-0.1875,5.8125c-1.4367,1.1319-3.6761-4.0312-0.6458-13.0208 c-5.2301,3.4846-5.9625,16.141-2.4792,22.0208c0,0,3.5,5.75,9.3125,5.9375c0,0,3.5,4.4375,10.3125,1.1875 c0,0,5.625,3.625,10.5-1.375c0,0,7.1875-0.7669,9.5625-6.1959c0,0,5.2083-15.2416-2.2083-21.6582 c0,0,3.3333,13.5417-1.2292,13.1667L52.2127,33.5z"/>
<path fill="#D0CFCE" stroke="none" d="M26.5252,55.5c0,0,0.8125,4.25,3.5625,4.9167l5.6667,2.25l4.75-0.9167l3.3958-2.0625 c0,0,2.6875-1.5208,2.4375-4.6875c0,0-6.75,2.5-10.1667,0.1667C36.171,55.1667,30.3794,57.3333,26.5252,55.5z"/>
<path fill="#E27022" stroke="none" d="M31.5044,47.0833c0,0,3.5,2.75,9.0833,0.0833l-0.25,2.6667l-4.4167,1.8333l-3.9167-1L31.5044,47.0833z"/>
</g>
<g id="hair"/>
<g id="skin"/>
<g id="skin-shadow"/>
<g id="line">
<path fill="#000000" stroke="none" d="M47.3393,34.6182c0,0,0.0607,2.3422-1.2037,3.1596c-1.2644,0.8174-3.3752-0.1996-3.3752-0.1996 s-0.0607-2.3422,1.2037-3.1596S47.3393,34.6182,47.3393,34.6182z"/>
<path fill="#000000" stroke="none" d="M25.9305,34.6182c0,0-0.0607,2.3422,1.2037,3.1596c1.2644,0.8174,3.3752-0.1996,3.3752-0.1996s0.0607-2.3422-1.2037-3.1596 S25.9305,34.6182,25.9305,34.6182z"/>
<path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M24.1981,47.3666c0,0-1.6667,12.7083,11.9583,7.5833v-3.3438"/>
<path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M21.2398,28.4916c-0.1667,0.3333-4.6667,9.1667,0.9167,16.25"/>
<path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M22.1549,23.2401c-4.6652,2.7745-12.117,9.7821-5.2485,24.1265"/>
<path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M27.3231,48.9916c0,0-11.1667,0.75-12.75,6.8333"/>
<path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M28.9064,52.1582c-0.25,0.1667-10.3333,5.75-8.5833,10.6667"/>
<path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M29.5766,59.1521c1.9799,2.0495,7.4123,6.3778,13.517,0.1296"/>
<path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M17.4898,22.3249c-7.9284-9.061-3.0759-11.6581-0.2234-12.8765c0.9417-0.4022,2.0253-0.2705,2.8642,0.3168l5.7994,6.0597 c0,0,9.3237-6.3941,20.14,0l5.7994-6.0597c0.8389-0.5872,1.9225-0.719,2.8642-0.3168c2.8525,1.2184,7.705,3.8155-0.2234,12.8765"/>
<path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M40.7186,47.7416c0,0,1,2.875-3.4167,3.625h-2.6038c-4.4167-0.75-3.4167-3.625-3.4167-3.625"/>
<path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M47.8019,47.3666c0,0,1.6667,12.7083-11.9583,7.5833v-3.5833"/>
<path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M50.7602,28.4916c0.1667,0.3333,4.6667,9.1667-0.9167,16.25"/>
<path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M49.8832,23.2628c4.6675,2.7899,12.0602,9.7983,5.2104,24.1037"/>
<path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M44.6769,48.9916c0,0,11.1667,0.75,12.75,6.8333"/>
<path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M43.0936,52.1582c0.25,0.1667,10.3333,5.75,8.5833,10.6667"/>
<path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M29.5766,24.1069c0,0,7.0776-3.3333,13.1833,0.4167"/>
<path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M31.5208,29.2864c0,0,4.9901-3.3333,9.2949,0.4167"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.4 KiB

View File

@ -0,0 +1,24 @@
<svg id="emoji" viewBox="0 0 72 72" xmlns="http://www.w3.org/2000/svg">
<g id="color">
<path fill="#F4AA41" stroke="none" d="M58.4953,11.21c0,0-10.4076,2.3754-15.5743,6.7088c0,0-9-2.5-13.8333,0.1667 c0,0-9.6549-6.7318-15.6549-6.7318c0,0-5.0326,3.75,0.3216,21.0651c0,0-2.6667,10.6667,1.6667,16.3333s9.8333,9.5,9.8333,9.5 l8.7917,4.0417l3.9583,0.1667l9.9167-4.375c0,0,6.6667-4.8333,8.8333-9.5c2.1667-4.6667,2.1667-13.8333,2.1667-13.8333l-1-3.3333 l2.8333-11.0208L58.4953,11.21z"/>
<path fill="#FFFFFF" stroke="none" d="M30.8377,47.3355c0,0-7.625,2.75-1.375,9.25c0,0-0.9739,4.625,3.8034,5.4849h5.1966 c2.0529,0.0031,4.5833-1.0683,3.7404-5.3628c0,0,7.5513-6.3722-1.3654-9.3722l-4.875,2L30.8377,47.3355z"/>
</g>
<g id="hair"/>
<g id="skin"/>
<g id="skin-shadow"/>
<g id="line">
<ellipse cx="45.0854" cy="38.1033" rx="1.6461" ry="2.8119" fill="#000000" stroke="none"/>
<ellipse cx="26.8427" cy="38.1033" rx="1.6461" ry="2.8119" fill="#000000" stroke="none"/>
<polyline fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" points="31.9328,47.2287 36.037,50.0204 39.8495,47.2287"/>
<path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M36.037,50.0204v4.2708c0,0-1.1042,3.6875-5.5417,2.875"/>
<path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M15.8717,48.4759c-4.8928-7.2535-2.0014-15.8722-2.0014-15.8722s-5.25-14.875-0.4375-21.25c0,0,9.1875,1.5,15.6875,7.375 c4.5946-1.9379,9.1575-2.0128,13.6875-0.1437c6.5-5.875,15.6875-7.375,15.6875-7.375c4.8125,6.375-0.4375,21.25-0.4375,21.25 s2.8914,8.6187-2.0014,15.8722"/>
<path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M14.7453,15.1037c0,0,12.8125,6.1875,10.0625,11.8125"/>
<path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M24.8491,50.8753c0,0-9.3615-0.458-13.6525,7.5243"/>
<path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M35.8911,49.8767v4.2708c0,0,1.1042,3.6875,5.5417,2.875"/>
<path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M57.1828,14.96c0,0-12.8125,6.1875-10.0625,11.8125"/>
<path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M47.2048,54.6836c0,0,8.2116,2.2454,8.6795,11.2958"/>
<path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M48.079,50.7316c0,0,9.3615-0.458,13.6525,7.5243"/>
<path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M31.3859,60.7598c3.88,1.6845,5.6481,1.8093,9.3021,0"/>
<path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M25.4446,54.6836c0,0-8.2116,2.2454-8.6795,11.2958"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@ -0,0 +1,28 @@
<svg id="emoji" viewBox="0 0 72 72" xmlns="http://www.w3.org/2000/svg">
<g id="color">
<path fill="#3F3F3F" stroke="none" d="M35.8377,9.4188c-0.8333,0.1667-9,0.1667-9,0.1667l-8.75,7.25l-4.9167,10.25l-3,12.4167l1.3333,8.75 l5.75,7.1667c0,0,4.75,4,11.1667,3.75c0,0,20,2.9167,26.5-3.5833l3.3333-3.5833l3.1667-6.3333l0.0833-7.6667l-1.5-7l-2.1667-6.9167 l-4.0833-7.5l-4.9167-4.1667L44.921,9.8355l-4.1667-0.8333L35.8377,9.4188z"/>
<path fill="#F4AA41" stroke="none" d="M55.5877,19.5021l2.5-1.8333l3.0833-0.5l1.9167,1.25l0.75,1c0,0,0.9167,3.6667,1,3.9167 c0.0833,0.25,0,6,0,6l-1.25,3.1667l-2.1667,1.9167l-2.3333-5.5l-3.1667-7.25L55.5877,19.5021z"/>
<polygon fill="#F4AA41" stroke="none" points="16.671,19.0021 14.0044,17.1688 11.2544,16.5855 8.3377,18.4188 6.671,23.3355 7.171,29.1688 8.3377,31.8355 10.8377,34.6688 12.2544,30.1688 14.421,24.2521"/>
<path fill="#F4AA41" stroke="none" d="M46.3377,44.5855l4.0833-2.4167l1.75-4.6667l1.25-7.25l-1.0833-4.75l-2.4167-2.8333l-4.3333-1.8333 l-5.4167,0.1667l-3.5833-0.0833l-7-0.3333l-4.6667,0.3333l-4.25,2.8333l-2,4.5833l0.0833,3.9167c0,0-0.0833,3.5,0.1667,3.9167 c0.25,0.4167,1.5833,4.4167,1.5833,4.4167l1.1667,1.6667l4,1.8333l-1,4c0,0-0.1667,6.5,1.25,8.4167 c1.4167,1.9167,5.75,5.4167,5.75,5.4167l6.4167,0.6667l4.5833-1.6667l3.8334-4.8333c0.8412-2.6884,1.1213-5.4142,1-8.1667 L46.3377,44.5855z"/>
</g>
<g id="hair"/>
<g id="skin"/>
<g id="skin-shadow"/>
<g id="line">
<circle cx="27.6338" cy="29.6771" r="2" fill="#000000" stroke="none"/>
<path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M23.0393,27.9538c0,0,4.726-6.5434,9.4521,0.1023"/>
<path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M30.8968,40.1701c0,0,0.6484,2.299,3.537,2.7117"/>
<path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M27.989,51.7674l1.2915,0.8045c1.2002,0.7477,2.5573,1.1408,3.9382,1.1408h5.6222c1.3232,0,2.6255-0.361,3.7893-1.0503 l1.5111-0.895"/>
<path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M31.0557,35.5878c-2.6158,1.5082-5.9686,4.8289-5.5798,11.8762"/>
<path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M26.8178,56.0264l0.2809,0.6695c1.0574,2.5206,3.1675,4.4503,5.7723,5.279l0.7864,0.2502 c2.2541,0.7171,4.6965,0.5509,6.8326-0.4651l0,0c2.0135-0.9577,3.6173-2.6051,4.5206-4.6436l0.4855-1.0957"/>
<path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M16.0795,18.8447l-1.7688-1.228c-1.6197-1.1245-3.7931-1.0191-5.2965,0.2567l0,0c-0.7659,0.65-1.2866,1.5421-1.4642,2.5308 c-0.6192,3.4457-1.5742,11.5715,2.6707,13.7114"/>
<circle cx="43.3662" cy="29.9126" r="2" fill="#000000" stroke="none"/>
<path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M48.0175,28.1759c0,0-4.726-6.5434-9.4521,0.1023"/>
<path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M49.9607,41.4195c2.5152-1.7505,2.6995-5.9685,3.3012-10.1803c0.4127-2.8885-0.4716-5.895-3.5959-8.4888 c-3.1321-2.6002-7.3146-2.1486-9.39-1.6993c-1.0224,0.2213-2.0608,0.3145-3.1062,0.2758l-2.7267-0.1011 c-0.842-0.0312-1.6765-0.1564-2.4973-0.3471c-1.9859-0.4613-6.3637-1.0469-9.6117,1.6496 c-3.1243,2.5938-4.0086,5.6002-3.5959,8.4888c0.6017,4.2118,0.786,8.4298,3.3012,10.1803"/>
<path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M41.1032,40.1701c0,0-0.6484,2.299-3.537,2.7117"/>
<path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M41.2534,35.9947c2.5625,1.588,5.6444,4.9189,5.2707,11.6914"/>
<path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M41.2534,35.9947c-0.5708-0.3538-1.1928-0.6531-1.7718-0.8958c-1.2375-0.5188-2.5724-0.7524-3.9139-0.7201l-0.0521,0.0013 c-1.5439,0.0372-3.0699,0.4152-4.4116,1.18c-0.0161,0.0092-0.0322,0.0184-0.0483,0.0277"/>
<path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M54.4627,54.7601c0,0,12.6763-6.0522,3.9518-28.4623S36.0651,9.7105,36.0651,9.7105S22.3101,3.6656,13.5856,26.0757 s3.9518,28.4623,3.9518,28.4623"/>
<path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M55.9205,19.0668l1.7688-1.228c1.6197-1.1245,3.7931-1.0191,5.2965,0.2567l0,0c0.7659,0.65,1.2866,1.5421,1.4642,2.5308 c0.6192,3.4457,1.5742,11.5715-2.6707,13.7114"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

@ -0,0 +1,28 @@
<svg id="emoji" viewBox="0 0 72 72" xmlns="http://www.w3.org/2000/svg">
<g id="color">
<path fill="#9B9B9A" stroke="none" d="M53.2127,6.2105l-6.25,6c0,0-5.875-3.125-12-3.125s-10.75,3-10.75,3l-6.75-6l-3.25-0.375l-3,3.75l-1.5,7.5 c0,0-0.125,9.75,2.5,11.625s-2,13.25-2,13.25l3.375,6.75l8,5.625l2.75,1.875c0,0,3.3331,7.5833,10.9944,5.5959 c0,0,5.7556,3.4875,10.3806-5.4709l2.5-1.5l6.375-3.625l6-8.125c0,0-3.25-10.75-2.5-14.25s2.875-9.5,2.625-11.25s-2-8.625-2-8.625 S56.8377,4.0855,53.2127,6.2105z"/>
<path fill="#3F3F3F" stroke="none" d="M12.421,45.5021c0,0,4.0833-10.25,10.1667-10l7.5833-0.4167l2.25,3.1667c0,0,0.9857,5.682-0.6702,9.5016 c-0.3188,0.7354,0.2502,1.5299,1.0442,1.4201c1.1109-0.1535,2.5407-0.2071,4.1332,0.0797c0.7536,0.1357,1.3819-0.5736,1.13-1.2966 c-0.6854-1.9673-1.4463-5.4314-0.5539-9.7882l2.75-2.9167l3.75,0.1667l3.5,0.5833c0,0,9.5-2,9.9167,10.25l2.6667-3.9167l-2.5-12.25 l1-2.5833l2-7.5l-1.0833-8.9167l-2.75-5.1667L52.671,6.0021l-4.4167,5.1667l-1.6667,0.6667l-5.25-1.8333l-9.25-0.6667 l-8.4167,2.8333l-6-6.8333l-3.5833-0.1667l-3.5,5.6667l-0.8333,6.9167l1.25,6.9167l1.5833,3.6667l-0.75,6.8333l-2,6.5 L12.421,45.5021z"/>
<path fill="#3F3F3F" stroke="none" d="M29.489,53.0348c0,0,4.4375-2.5625,11.1875,0.125l0.0625,0.8125l-2.25,1.5l-0.9375,1.625 c0,0-1.3125,1.25-2,1c-0.6875-0.25-3-1.125-3-1.125l-0.75-1.5625l-2.625-1.4375l0.0625-0.875"/>
<path fill="#9B9B9A" stroke="none" d="M55.2752,8.7105c0,0,5.25,8.5625,0.75,17.5c0,0-4.4375-6.5-7.9375-6.375 C48.0877,19.8355,55.1502,15.5855,55.2752,8.7105z"/>
<path fill="#9B9B9A" stroke="none" d="M14.7227,8.7105c0,0-5.25,8.5625-0.75,17.5c0,0,4.4375-6.5,7.9375-6.375 C21.9102,19.8355,14.8477,15.5855,14.7227,8.7105z"/>
<path fill="#9B9B9A" stroke="none" d="M29.1765,62.0759c0.1166,2.4426,0.8751,4.2037,2.6761,4.858h6.5847 c2.1107-0.7139,2.5376-2.4764,2.3018-4.6519L29.1765,62.0759"/>
</g>
<g id="hair"/>
<g id="skin"/>
<g id="skin-shadow"/>
<g id="line">
<circle cx="26.2495" cy="40.2751" r="2" fill="#000000" stroke="none"/>
<circle cx="44.2944" cy="40.5106" r="2" fill="#000000" stroke="none"/>
<path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M29.3116,53.9749l2.375,1.3959c0,0,0.4899,2.9667,3.2712,2.748v3.5625c0,0-3.4688,1.7604-6.3438-0.5312 c-2.2371-1.7832-3.5938-4.6875-3.5938-4.6875"/>
<path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M21.739,53.3376c-3.8308-1.0774-6.3025-3.0804-7.8464-5.5039L9.871,42.3259c2.7739-7.0508,2.5763-14.1342,2.5763-14.1342 c-4.7083-8.6667-1.625-17.4583-1.625-17.4583c2.5417-8.2917,6.7917-5.0417,6.7917-5.0417c3.0417,5.1667,9.875,8.9167,9.875,8.9167"/>
<path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M15.614,9.2126c0,0,0.375,7.5,7.3125,10.375c0,0-6.6983,1.679-7.7816,6.3457"/>
<path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M54.8556,9.2126c0,0-0.375,7.5-7.3125,10.375c0,0,6.6983,1.679,7.7816,6.3457"/>
<path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M30.9474,40.5106c0,0,2.711-4.3812-1.25-5.6689c-2.7917-0.9075-6.2083,1.0584-6.2083,1.0584"/>
<path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M35.3321,61.6813c0,0,3.4688,1.7604,6.3438-0.5312c2.2371-1.7832,3.5938-4.6875,3.5938-4.6875"/>
<path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M40.6873,53.9749l-2.7917,1.3959c0,0-0.1566,2.9667-2.9378,2.748"/>
<path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M42.8008,14.9502c0,0,6.8333-3.75,9.875-8.9167c0,0,4.25-3.25,6.7917,5.0417c0,0,3.0833,8.7917-1.625,17.4583 c0,0-0.0675,7.3188,2.6299,13.7929l-4.6974,6.7357c-1.5598,2.0223-3.8715,3.6742-7.2242,4.6172"/>
<path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M39.3425,40.8524c0,0-2.7109-4.3812,1.25-5.6689c2.7917-0.9075,6.2083,1.0584,6.2083,1.0584"/>
<path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M24.6882,11.4515c0,0,7.9524-5.5773,21.3522,0"/>
<path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M31.3533,65.8686c2.7191,1.4858,5.2903,1.3535,7.75,0"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.6 KiB

View File

@ -0,0 +1,17 @@
<svg id="emoji" viewBox="0 0 72 72" xmlns="http://www.w3.org/2000/svg">
<g id="color">
<circle cx="36" cy="36" r="23" fill="#FCEA2B"/>
<path fill="#FFFFFF" d="M50.595,41.64c0.012,1.5397-0.2838,3.0662-0.87,4.49c-12.49,3.03-25.43,0.34-27.49-0.13 c-0.5588-1.3852-0.8407-2.8664-0.83-4.36h0.11c0,0,14.8,3.59,28.89,0.07L50.595,41.64z"/>
<path fill="#EA5A47" d="M49.7251,46.13c-1.79,4.27-6.35,7.23-13.69,7.23c-7.41,0-12.03-3.03-13.8-7.36 C24.2951,46.47,37.235,49.16,49.7251,46.13z"/>
</g>
<g id="hair"/>
<g id="skin"/>
<g id="skin-shadow"/>
<g id="line">
<circle cx="36" cy="36" r="23" fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M50.595,41.64 c0.012,1.5397-0.2838,3.0662-0.87,4.49c-12.49,3.03-25.43,0.34-27.49-0.13c-0.5588-1.3852-0.8407-2.8664-0.83-4.36h0.11 c0,0,14.8,3.59,28.89,0.07L50.595,41.64z"/>
<path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M49.7251,46.13 c-1.79,4.27-6.35,7.23-13.69,7.23c-7.41,0-12.03-3.03-13.8-7.36C24.2951,46.47,37.235,49.16,49.7251,46.13z"/>
<path d="M30,31c0,1.6568-1.3448,3-3,3c-1.6553,0-3-1.3433-3-3c0-1.6552,1.3447-3,3-3C28.6552,28,30,29.3448,30,31"/>
<path d="M48,31c0,1.6568-1.3447,3-3,3s-3-1.3433-3-3c0-1.6552,1.3447-3,3-3S48,29.3448,48,31"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,21 @@
<svg id="emoji" viewBox="0 0 72 72" xmlns="http://www.w3.org/2000/svg">
<g id="color">
<path fill="#FFFFFF" stroke="none" d="M23.7544,12.3618l1.6667,7.1667l-5.3333,5.3333l-8.3333,14.3333l1,4.6667l2.1667,1.3333l4-0.1667 l3.5-3.3333l6.8333-1.8333c0,0,1.3333,1.5,2.1667,3s3.6667,4.1667,3.6667,4.1667l0.5,6l-1.8333,6.1667l-2,2.8333 c0,0,22,9.5,33.1667-7l-0.5-6l-1.8333-5l-3.3333-5.1667l-1-1.5l-0.1667-5.1667l-2.8333-5.3333l-5-3l-2.6667-4.5l-5.1667-4.1667 l-6.5-1.5l-5.6667,1l-4.1667-2.1667L23.7544,12.3618z"/>
<path fill="#EA5A47" stroke="none" d="M50.671,23.155l5.2083,4.095c0,0,5.5638,8.2181-0.3258,17.8201c-7.0492,11.4924,0,0,0,0 c-1.6183,3.4754-2.3141,6.7423-1.738,9.7216l-5.3111-4.4167V34.2917L50.671,23.155z"/>
<polyline fill="#EA5A47" stroke="none" points="25.8985,19.2712 10.7847,12.0212 15.951,18.1399 21.1747,23.995 25.8985,19.2712"/>
<path fill="#92D3F5" stroke="none" d="M29.7367,13.6311l10.7677,0.1362c0,0,9.2377,4.0661,10.5355,11.8161l0.6874,8.9567 c-2.6337,6.5386-3.0562,14.1267,2.0883,20.8336l0,0c0,0-7.1444,1.3215-9.8944-7.1094L42.3377,43.5l0.3258-6.0341l1.4169-5.6426 l-0.2833-4.8929l-2.2761-4.3124l-3.5322-2.8413l-5.792-2.0801L29.7367,13.6311"/>
<path fill="#61B2E4" stroke="none" d="M58.4549,36.75c0,0,5.5192,6.4066,6.9982,15.1193c0.1838,1.0826,0.1251,2.193-0.1377,3.2591 c-0.4317,1.7512-0.8179,4.9979,0.1452,7.3825c0.4689,1.1611-0.5621,2.3655-1.7883,2.1115 c-3.7094-0.7686-9.2437-3.6474-10.2567-8.0876c-0.0239-0.1047-0.0368-0.2138-0.0417-0.321l-0.266-5.7459 c-0.0132-0.2857,0.0516-0.5695,0.1875-0.8211l3.692-6.8359c0.0666-0.1233,0.1164-0.2549,0.1482-0.3914L58.4549,36.75"/>
</g>
<g id="hair"/>
<g id="skin"/>
<g id="skin-shadow"/>
<g id="line">
<path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M58.4549,37.7826C60.2229,40.1443,65,44.4647,64.5,54.0208"/>
<path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M32.5,41.8854c0,0,8.4783,6.7823,0,18.7647"/>
<polyline fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" points="24.809,19.1338 10.25,11.75 21.1747,23.995"/>
<path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M35.1962,30.8696c0.5489,8.3555-9.3225,9.703-11.954,10.3347c-0.3325,0.0798-0.6318,0.25-0.8736,0.4919l-2.2227,2.2227 c-0.3494,0.3494-0.8234,0.5458-1.3176,0.5458h-3.5121c-1.203,0-2.2711-0.7698-2.6515-1.9111l-0.531-1.5931 c-0.258-0.774-0.1649-1.6222,0.2549-2.3218l8.7862-14.6436l4.7238-4.7238l-2.1151-6.9054c0,0,7.8026-0.6987,8.4135,5.3308 c0,0,16.9281,2.4418,10.5531,19.383c0,0-1.625,5.9489,2.375,11.1846"/>
<path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M30.9167,14.0208c0,0,22.2444-4.0208,19.9583,19.9583"/>
<path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M49.9187,23.155c0,0,14.7665,6.5865,5.4563,22.22c0,0-5.375,6.5625,0.625,13.6042"/>
<circle cx="24.4167" cy="28.9304" r="2" fill="#000000" stroke="none"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@ -0,0 +1,33 @@
<svg id="emoji" viewBox="0 0 72 72" xmlns="http://www.w3.org/2000/svg">
<g id="color">
<path fill="#e27022" d="M50.63,15.6a1.2,1.2,0,0,1-1.73,1.18c-1.08-.56-2.98-1.24-3.92-.35a.3039.3039,0,0,1-.07.05c-1.52-2.09-4.21-3.79-8.91-3.79a3.7944,3.7944,0,0,1,3.56-4.24,8.4848,8.4848,0,0,0,4.73-2.06,5.0935,5.0935,0,0,1,.65,3.96l.49.01C49.15,10.36,50.4,13.41,50.63,15.6Z"/>
<path fill="#b1cc33" d="M36.0022,42.0333c-15.2776,0-10.5548-6.0794-10.5548-12.7628,0-3.5808-.3769-3.4016-.3769-5.9038s.6328-10.681,10.9317-10.681,10.9317,8.1789,10.9317,10.681-.3769,2.323-.3769,5.9038C46.557,35.9539,51.28,42.0333,36.0022,42.0333Z"/>
<path fill="#b1cc33" d="M46.9339,23.3667c2.137-2.8369,5.3093-2.0623,8.8-4.36-.548,3.5228-1.6172,12.3056-8.9959,12.787Z"/>
<path fill="#b1cc33" d="M25.2622,31.7941c-7.3787-.4814-8.4479-9.2642-8.9959-12.787,3.49,2.2973,6.6628,1.5227,8.8,4.36Z"/>
<path fill="#b1cc33" d="M17.0625,57.8177s-2-13,10-13c3.1918,2.1279,5.9264,3.5984,9,3.5922h-.125c3.0736.0062,5.8081-1.4643,9-3.5922,12,0,10,13,10,13l.0076,1H17.0361Z"/>
<path fill="#5c9e31" d="M36,58.8164c8.1262-10.7061,8.3828-14.831,8.3828-14.831a1.0008,1.0008,0,0,1,.5547-.168c3.6563,0,6.4844,1.1357,8.4072,3.3769C56.8984,51.3369,55.9668,57.7,55.9258,57.97a.9991.9991,0,0,1-.9863.8467Z"/>
<path fill="#a57939" d="M44.9635,57.8182A47.8735,47.8735,0,0,1,51.43,50.9394c4.4058-3.4682,6.7662-3.56,10.948-8.9061,2.0629-2.6374,2.6374-7.2817,0-9.3447S56.024,31.4452,53.5,34.5164c-4.681,5.6962-3.8249,8.713-6.1991,13.1933A102.5608,102.5608,0,0,1,40.4972,57.816v1h4.4654Z"/>
<path fill="#fff" d="M31.0551,31.4981h0a2.6068,2.6068,0,0,1,2.6068,2.6068v.651a0,0,0,0,1,0,0H31.0551a0,0,0,0,1,0,0V31.4981A0,0,0,0,1,31.0551,31.4981Z"/>
<path transform="translate(79.3987 66.254) rotate(180)" fill="#fff" d="M38.3959,31.4981h2.6068a0,0,0,0,1,0,0v.651a2.6068,2.6068,0,0,1-2.6068,2.6068h0a0,0,0,0,1,0,0V31.4981A0,0,0,0,1,38.3959,31.4981Z"/>
</g>
<g id="line">
<path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M54.7288,52.4471a16.7057,16.7057,0,0,1,.2087,5.3706"/>
<path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.0625,57.8177s-2-13,10-13c3.1918,2.1279,5.9264,3.5984,9,3.5922h-.125c3.0736.0062,5.8081-1.4643,9-3.5922"/>
<path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M44.287,12.09a5.0267,5.0267,0,0,0,0-5.7006,8.4359,8.4359,0,0,1-4.7251,2.0561,3.7937,3.7937,0,0,0-3.56,4.24"/>
<path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M46.9339,23.3667c2.137-2.8369,5.3093-2.0623,8.8-4.36-.548,3.5228-1.6172,12.3056-8.9959,12.787"/>
<path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M25.2622,31.7941c-7.3787-.4814-8.4479-9.2642-8.9959-12.787,3.49,2.2973,6.6628,1.5227,8.8,4.36"/>
<path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M36.0022,42.0333c-15.2776,0-10.5548-6.0794-10.5548-12.7628,0-3.5808-.3769-3.4016-.3769-5.9038s.6328-10.681,10.9317-10.681,10.9317,8.1789,10.9317,10.681-.3769,2.323-.3769,5.9038C46.557,35.9539,51.28,42.0333,36.0022,42.0333Z"/>
<path d="M42.0023,26.4993a2,2,0,1,1-2-2,2,2,0,0,1,2,2"/>
<path d="M34.0023,26.4993a2,2,0,1,1-2-2,2,2,0,0,1,2,2"/>
<path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M38.039,23.0265a5.4257,5.4257,0,0,1,5.0488-1.2713"/>
<path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M28.97,21.7552a5.4256,5.4256,0,0,1,5.0488,1.2713"/>
<path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M44.9635,57.8182A47.8735,47.8735,0,0,1,51.43,50.9394c4.4058-3.4682,6.7662-3.56,10.948-8.9061,2.0629-2.6374,2.6374-7.2817,0-9.3447S56.024,31.4452,53.5,34.5164c-4.681,5.6962-3.8249,8.713-6.1991,13.1933A102.5608,102.5608,0,0,1,40.4972,57.816"/>
<line x1="50.3728" x2="53.5159" y1="36.0726" y2="37.974" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<line x1="56.4724" x2="58.3992" y1="45.2117" y2="48.3395" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<line x1="59.3606" x2="59.0523" y1="29.4325" y2="33.0931" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<line x1="31.0551" x2="31.0551" y1="32.4956" y2="35.2715" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<line x1="42.2494" x2="29.8085" y1="35.2715" y2="35.2715" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<line x1="41.0027" x2="41.0027" y1="32.4956" y2="35.2715" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M47.2467,15.1081a9.3183,9.3183,0,0,1,2.754,1.1523s0-5.21-4.4936-5.21"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

@ -0,0 +1,26 @@
<svg id="emoji" viewBox="0 0 72 72" version="1.1" xmlns="http://www.w3.org/2000/svg">
<g id="hair">
<path fill="#3F3F3F" d="M26.1,39.2c-4,0-4-6-4-13s4-14,14-14s14,7,14,14s0,13-4,13"/>
</g>
<g id="skin">
<path fill="#FCEA2B" d="M24.9494,31.1c0,9,4.9,14,11,14c5.9,0,11.1-5,11.1-14c0.0246-1.7187-0.3163-3.423-1-5c-3-3-7-8-7-8 c-4,3-7,6-13,7C26.0494,25.1,24.9494,26.1,24.9494,31.1z"/>
</g>
<g id="color">
<path fill="#D22F27" d="M35.65,45.1c-3.4-0.1-8.6-5.7-8.6-5.7h-13.6c1.5916,2.7161,3.0272,5.5207,4.3,8.4 c1.281,3.1916,2.2196,6.5102,2.8,9.9h30.9c0.5804-3.3898,1.519-6.7084,2.8-9.9c1.2728-2.8793,2.7084-5.6839,4.3-8.4h-13.9 C44.75,39.4,39.15,45.1,35.65,45.1z"/>
<path fill="#3F3F3F" d="M17,61v-3.8c0-5,5-9,10-9c6,5,12,5,18,0c5,0,10,4,10,9V61H17z"/>
<path d="M31.45,61h9c3.6-2.9,5.6-13.5,5.6-13.5c-5.7672,5-14.3328,5-20.1,0C25.95,47.4,27.95,58,31.45,61z"/>
</g>
<g id="line">
<path fill="none" stroke="#000000" stroke-linejoin="round" stroke-width="2" d="M26.5,39.4H13.4 c1.6694,2.6727,3.1079,5.4829,4.3,8.4c0.5072,1.2402,0.9413,2.509,1.3,3.8"/>
<path fill="none" stroke="#000000" stroke-linejoin="round" stroke-width="2" d="M53,51.4c0.4-1.2,0.8-2.4,1.3-3.6 c1.1713-2.9266,2.6108-5.7385,4.3-8.4H44.7"/>
<path d="M42,30.1c0,1.1046-0.8954,2-2,2s-2-0.8954-2-2s0.8954-2,2-2C41.1032,28.1032,41.9968,28.9968,42,30.1"/>
<path d="M34,30.1c0,1.1046-0.8954,2-2,2c-1.1046,0-2-0.8954-2-2s0.8954-2,2-2C33.1032,28.1032,33.9968,28.9968,34,30.1"/>
<path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M33,39 c1.875-1,4.125-1,6,0"/>
<path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" d="M38,38.5 c0.2408,0.0816,0.4748,0.1819,0.7,0.3"/>
<path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17,60v-3c0-5,5-9,10-9 c6,5,12,5,18,0c5,0,10,4,10,9v3"/>
<polygon stroke="#000000" stroke-linecap="round" stroke-linejoin="round" points="34,38.7 34.3,41.1 35.1,38.8 35,38.7"/>
<polygon stroke="#000000" stroke-linecap="round" stroke-linejoin="round" points="37.9,38.7 37.7,41.1 36.9,38.8 36.8,38.7"/>
<path fill="none" stroke="#000000" stroke-linejoin="round" stroke-width="2" d="M24.9494,31.1c0,9,4.9,14,11,14 c5.9,0,11.1-5,11.1-14c0.0246-1.7187-0.3163-3.423-1-5c-3-3-7-8-7-8c-4,3-7,6-13,7C26.0494,25.1,24.9494,26.1,24.9494,31.1z"/>
<path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M26,39.1c-4,0-4-6-4-13 s4-14,14-14s14,7,14,14s0,13-4,13"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@ -0,0 +1,39 @@
<svg id="emoji" viewBox="0 0 72 72" xmlns="http://www.w3.org/2000/svg">
<g id="color">
<path fill="#92d3f5" d="M54.9224,60.9315s2-14.0268-10-14.0268c-3.1919,2.1279-5.9264,3.5984-9,3.5921h.125c-3.0736.0063-5.8082-1.4642-9-3.5921-12,0-10,14.0268-10,14.0268Z"/>
<path fill="#61b2e4" d="M45.3084,46.9047a18.0129,18.0129,0,0,1-8.9375,3.5885c13.0625.4115,12.9775,6.395,13.74,10.395h5.1129S57.3084,46.9047,45.3084,46.9047Z"/>
<polyline fill="#6a462f" points="18.056 52.064 18.056 42.499 25.141 42.499 25.141 46.063"/>
<path fill="#a57939" d="M47.9673,60.9583c-18.4989-5.1358-20.92-14.0536-20.92-14.0536s-4.2.2046-5.6776,1.3537c3.1163,6.4287,5.9346,9.5505,14.1044,12.6807"/>
</g>
<g id="skin">
<path fill="#fcea2b" d="M38.1326,20.5319a9.3955,9.3955,0,0,1-2.18-5.054c-.028,0-.0549-.0052-.0829-.0052a8.2719,8.2719,0,0,1-1.98,5.0591,10.8766,10.8766,0,0,1-8.7937,4.7087,17.3993,17.3993,0,0,0-.5656,4.4052c0,7.8277,5.0765,14.1732,11.3386,14.1732S47.208,37.4734,47.208,29.6457a17.3453,17.3453,0,0,0-.6258-4.6109A9.7529,9.7529,0,0,1,38.1326,20.5319Z"/>
<path fill="#fcea2b" d="M46.5844,24.451l11.45-1.6359S51.7663,32.6639,47.54,31.614"/>
<path fill="#fcea2b" d="M25.4747,24.4322,14.1572,22.8151s6.1889,9.7351,10.3663,8.6973"/>
</g>
<g id="hair">
<path fill="#f4aa41" d="M35.7974,11.7565A15.35,15.35,0,0,0,20.661,24.6021c8.8261,3.0574,13.1564-3.9388,13.1564-3.9388a8.2716,8.2716,0,0,0,1.98-5.0592H35.88a9.3969,9.3969,0,0,0,2.18,5.0592s3.413,6.46,12.8727,3.93A15.3509,15.3509,0,0,0,35.7974,11.7565Z"/>
<path fill="#f4aa41" d="M37.48,44.3806l-5.1958-.6448-4.21-2.6168-3.4133-6.106-.3793-2.8445-2.3514-.4171L19.02,29.4878V42.0409l6.159.3305v4.5958l1.9062-.19c3.17,2.1133,5.8895,3.5747,8.9375,3.5885,3.048-.0138,5.7675-1.4752,8.9375-3.5885a9.6884,9.6884,0,0,1,6.5536,2.1348,12.86,12.86,0,0,0,.6612-4.1082c0-16.6561-.27-14.3257-.27-14.3257l-2.3265,1.4249-2.2.2654-1.29,4.8924-3.6029,4.9683Z"/>
</g>
<g id="line">
<path d="M42.0163,28.5884a2,2,0,1,1-2-2,2.0007,2.0007,0,0,1,2,2"/>
<path d="M34.0163,28.5884a2,2,0,1,1-2-2,2.0007,2.0007,0,0,1,2,2"/>
<path d="M36.0162,38.5908a7.6528,7.6528,0,0,1-3.4473-.8579,1,1,0,0,1,.8945-1.7891,5.3774,5.3774,0,0,0,5.1055,0,1,1,0,1,1,.8945,1.7891A7.6524,7.6524,0,0,1,36.0162,38.5908Z"/>
<path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M50.9636,24.15C46.49,10.5289,36.0162,11.9284,36.0162,11.9284S25.663,10.545,21.147,23.9148"/>
<circle cx="36.0162" cy="22.1497" r="1.5"/>
<path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M25.4613,24.7355,14.1438,23.1184s6.1889,9.7351,10.3663,8.6973"/>
<path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M36.0162,15.776a20.4581,20.4581,0,0,1-3.08,5.0592"/>
<path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M46.571,24.7543l11.45-1.6359s-6.2678,9.8488-10.494,8.7989"/>
<path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M36.0162,15.776a20.4558,20.4558,0,0,0,3.08,5.0592"/>
<line x1="51.2159" x2="51.2159" y1="31.033" y2="44.5357" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<polyline fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" points="18.088 52.108 18.088 42.543 25.173 42.543 25.173 46.108"/>
<line x1="20.0217" x2="20.0217" y1="42.5045" y2="37.7613" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<line x1="23.3932" x2="23.3932" y1="42.5433" y2="39.102" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M54.9537,59.9492s2-13-10-13c-3.1919,2.1279-5.9264,3.5984-9,3.5921h.125c-3.0736.0063-5.8082-1.4642-9-3.5921-12,0-10,13-10,13"/>
<path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M26.4391,24.5466l-.9825.2293a17.29,17.29,0,0,0-.779,5.1733c0,7.8276,5.0765,14.1732,11.3386,14.1732s11.3386-6.3456,11.3386-14.1732a17.29,17.29,0,0,0-.7791-5.1733l-.8123-.3239"/>
<path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M46.571,24.7543l11.45-1.6359s-6.2678,9.8488-10.494,8.7989"/>
<line x1="19.9468" x2="19.9468" y1="30.333" y2="33.5011" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M47.8955,60.0059C29.3966,54.87,27.0787,46.9492,27.0787,46.9492s-4.2.2046-5.6776,1.3537c3.1163,6.4287,5.69,8.6387,13.8595,11.7689"/>
<path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M46.7244,21.2945c-2.6425-.6931-3.2409,2.473-6.127,1.7854"/>
<path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M31.3867,23.08c-2.886.6876-2.8758-2.7625-5.5183-2.0694"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.1 KiB

View File

@ -0,0 +1,28 @@
<svg id="emoji" viewBox="0 0 72 72" xmlns="http://www.w3.org/2000/svg">
<g id="color">
<path fill="#92d3f5" d="M45.5317,50.6166l.797,2.48-1.1339,4.9948L40.0135,63.766A23.9672,23.9672,0,0,0,54.951,59.6536s2-13-10-13c-3.1918,2.1279-5.9264,3.5984-9,3.5921h.125c-3.0736.0063-5.8081-1.4642-9-3.5921-12,0-10,13-10,13,9.9776,6.0208,14.368,3.708,28.1188-1.5619l.6171-3.5208-24.0677,1.35,5.3094-.9076"/>
<path fill="#92d3f5" d="M54.9358,59.6536s2-13-10-13c-3.1919,2.1279-5.9264,3.5984-9,3.5921h.125c-3.0736.0063-5.8081-1.4642-9-3.5921-12,0-10,13-10,13"/>
<path fill="#61b2e4" d="M45.2181,46.6536a18.0129,18.0129,0,0,1-8.9375,3.5885c13.0625.4115,13.0625,5.4115,13.8247,9.4115h5.1128S57.2181,46.6536,45.2181,46.6536Z"/>
<path fill="#61b2e4" d="M55.2181,59.6536S43.4619,68.68,34.4813,62.6168l10.7135-4.5251,1.1339-4.9948S55.3057,50.6328,55.2181,59.6536Z"/>
<ellipse cx="36.0134" cy="30.1536" rx="11.3386" ry="14.1732" fill="#92d3f5"/>
<path fill="#fff" d="M49.1847,14.728c-13.1713,4.1165-24.51,13.5184-24.51,13.5184s-3.96-5.3394-2.2369-10.7265c0,0,2.2369-8.0773,13.036-8.0773,7.7035,0,11.5948,3.1694,13.7108,5.2854C55.331,23.4529,47.05,28.3322,47.05,28.3322a126.1662,126.1662,0,0,1-9.655-8.3413"/>
<path fill="#3f3f3f" d="M36.0422,46.9384c5.0174-.1253,9.5065-6.0464,10.3-10.8959-3.2477,5.68-8.6155,4.61-8.6155,4.61L36.02,38.0191l-1.7063,2.6338s-5.3678,1.0695-8.6155-4.61c.7933,4.85,5.2824,10.7706,10.3,10.8959"/>
<path fill="#d0cfce" d="M49.1847,14.728C55.331,23.4529,47.05,28.3322,47.05,28.3322a126.1662,126.1662,0,0,1-9.655-8.3413"/>
<path fill="#fcea2b" d="M36.0135,25.82h0a2,2,0,0,1-2-2v-4a2,2,0,0,1,2-2h0a2,2,0,0,1,2,2v4A2,2,0,0,1,36.0135,25.82Z"/>
<polygon fill="#fcea2b" points="45.195 58.092 40.783 59.923 40.783 54.678 45.812 54.678 45.195 58.092"/>
</g>
<g id="line">
<path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M40.0135,63.766A23.9672,23.9672,0,0,0,54.951,59.6536s2-13-10-13c-3.1918,2.1279-5.9264,3.5984-9,3.5921h.125c-3.0736.0063-5.8081-1.4642-9-3.5921-12,0-10,13-10,13,9.9776,6.0208,14.368,3.708,28.1188-1.5619l.6171-3.5208-24.0677,1.35"/>
<path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M45.5317,50.6166l.797,2.48-1.1339,4.9948"/>
<path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M25.7159,36.0725a17.002,17.002,0,0,1-1.04-5.92,18.7051,18.7051,0,0,1,.11-2"/>
<path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M47.2459,28.2025a17.9177,17.9177,0,0,1,.11,1.95,17.0185,17.0185,0,0,1-1.06,5.97"/>
<path d="M42.0135,28.7928a2,2,0,1,1-2-2,2.0007,2.0007,0,0,1,2,2"/>
<path d="M34.0135,28.7928a2,2,0,1,1-2-2,2.0007,2.0007,0,0,1,2,2"/>
<path d="M36.0134,38.7952a7.6528,7.6528,0,0,1-3.4473-.8579,1,1,0,0,1,.8946-1.7891,5.3772,5.3772,0,0,0,5.1054,0,1,1,0,0,1,.8946,1.7891A7.6528,7.6528,0,0,1,36.0134,38.7952Z"/>
<path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M36.0422,46.9384c5.0174-.1253,9.5065-6.0464,10.3-10.8959-3.2477,5.68-8.6155,4.61-8.6155,4.61L36.02,38.0191l-1.7063,2.6338s-5.3678,1.0695-8.6155-4.61c.7933,4.85,5.2824,10.7706,10.3,10.8959"/>
<path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M34.0159,21.7725c-1.28.77-2.47,1.53-3.53,2.24-3.13,2.09-5.21,3.75-5.7,4.14-.07.06-.11.09-.11.09s-3.96-5.34-2.24-10.72c0,0,2.24-8.08,13.04-8.08,7.7,0,11.59,3.17,13.71,5.29,5.58,7.91-.72,12.65-1.94,13.47-.13.09-.2.13-.2.13s-4.41-3.38-9.03-7.75"/>
<path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M37.9859,19.5125a66.4314,66.4314,0,0,1,11.2-4.78"/>
<path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M36.0135,25.82h0a2,2,0,0,1-2-2v-4a2,2,0,0,1,2-2h0a2,2,0,0,1,2,2v4A2,2,0,0,1,36.0135,25.82Z"/>
<line x1="40.7828" x2="40.7828" y1="54.9709" y2="59.7193" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@ -0,0 +1,28 @@
<svg id="emoji" viewBox="0 0 72 72" xmlns="http://www.w3.org/2000/svg">
<g id="color">
<circle cx="36.9435" cy="28.0392" r="10" fill="#e67a94"/>
<path fill="#b1cc33" d="M35.1773,17.1567c-5.8765.5287-10.5159,6.6407-10.5159,14.1212,0,7.8277,5.0764,14.1733,11.3386,14.1733s11.3387-6.3456,11.3387-14.1733a17.2594,17.2594,0,0,0-.7674-5.0941,9.9175,9.9175,0,0,1-11.394-9.0271Z"/>
<path fill="#b1cc33" d="M54.9375,60.9094s2-12.6032-10-12.6032c-3.1918,2.128-5.9264,3.5985-9,3.5922h.125c-3.0736.0063-5.8081-1.4642-9-3.5922-12,0-10,12.6032-10,12.6032"/>
<path fill="#d0cfce" d="M46.354,48.2762l-.9075.8851a15.6367,15.6367,0,0,1-5.3451,5.81l-2.0029,5.7447-2-4.5853c-3.337.02-6.674-2.2824-9.3481-6.9693L26.2,48.25C15.27,48.9272,17.1609,60.937,17.1609,60.937l3.1531-.007,2.0839-4.7825,1.5287,4.8108,31.18-.05"/>
<path fill="#fcea2b" d="M32,38.2987s8-3.11,8,0C40,44.5411,32,38.2987,32,38.2987Z"/>
<path fill="#9b9b9a" d="M42.5077,52.7252c6.4062,1.661,6.5337,5.0343,7.1339,8.1842h5.1128s1.8925-11.904-9.0495-12.5747q-.46-.0282-.9505-.0285"/>
<path fill="#5c9e31" d="M30,29.9325v2.041a2.1088,2.1088,0,0,0,1.7064,2.1339A2.0016,2.0016,0,0,0,34,32.1283V29.9325a.0571.0571,0,0,0-.0571-.0571H30.0573A.057.057,0,0,0,30,29.9325Z"/>
<path fill="#5c9e31" d="M38,29.8754v2.2529a2,2,0,0,0,4,0V29.8754Z"/>
<path fill="#fff" d="M42,29.9171a2,2,0,1,1-2-2,2.0007,2.0007,0,0,1,2,2"/>
<path fill="#fff" d="M34,29.9171a2,2,0,1,1-2-2,2.0007,2.0007,0,0,1,2,2"/>
</g>
<g id="line">
<path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M35.1773,17.1567c-5.8765.5287-10.5159,6.6407-10.5159,14.1212,0,7.8277,5.0764,14.1733,11.3386,14.1733s11.3387-6.3456,11.3387-14.1733a17.2594,17.2594,0,0,0-.7674-5.0941,9.9175,9.9175,0,0,1-11.394-9.0271Z"/>
<path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M45.3836,25.2642a2,2,0,0,0-1.3286-2.18,1.9753,1.9753,0,0,0,.3775-.8915,1.9931,1.9931,0,0,0-2.79-2.1141,1.9923,1.9923,0,0,0-3.3909-1.3058,1.9922,1.9922,0,0,0-2.7713-.4814"/>
<path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M35.1773,17.1567q.0408.48.1262.9464a9.9978,9.9978,0,0,0,9.4213,8.1874q.2037.0082.4093.0082a10.0162,10.0162,0,0,0,1.4372-.1149"/>
<path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M32,38.2987s8-3.11,8,0C40,44.5411,32,38.2987,32,38.2987Z"/>
<line x1="39.4332" x2="35.134" y1="40.0153" y2="40.0153" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<line x1="36.9435" x2="36.9435" y1="40.0153" y2="37.0896" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/>
<path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M26.6519,48.9249c2.6741,4.6869,6.0111,6.99,9.3481,6.9693l2,4.0183,2.0029-5.1777a15.6376,15.6376,0,0,0,5.3451-5.81"/>
<line x1="43.5496" x2="42.7939" y1="57.2401" y2="59.9029" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<polyline fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" points="20.216 59.871 22.3 56.308 23.828 59.871"/>
<path d="M55,60.9171a1,1,0,0,1-1-1v-3c0-4.4517-4.4961-7.81-8.6518-7.9922-6.2051,5.0117-12.4912,5.0117-18.6963,0C22.4961,49.1066,18,52.4654,18,56.9171v3a1,1,0,1,1-2,0v-3c0-5.3247,5.14-9.9976,11-10h0a.9994.9994,0,0,1,.64.2319c5.625,4.6875,11.0947,4.6875,16.72,0a.9994.9994,0,0,1,.64-.2319h.0005C50.86,46.92,56,51.5924,56,56.9171v3A1,1,0,0,1,55,60.9171Z"/>
<path fill="none" stroke="#000" stroke-miterlimit="10" stroke-width="1.5" d="M42,29.9171a2,2,0,1,1-2-2,2.0007,2.0007,0,0,1,2,2"/>
<path fill="none" stroke="#000" stroke-miterlimit="10" stroke-width="1.5" d="M34,29.9171a2,2,0,1,1-2-2,2.0007,2.0007,0,0,1,2,2"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

@ -16,7 +16,7 @@ class BlankPluginBuilder extends PluginBuilder {
String get menuName => "Blank";
@override
PluginType get pluginType => DefaultPlugin.blank.type();
PluginType get pluginType => PluginType.blank;
}
class BlankPluginConfig implements PluginConfig {

View File

@ -0,0 +1,290 @@
import 'dart:async';
import 'package:app_flowy/plugins/grid/application/block/block_cache.dart';
import 'package:app_flowy/plugins/grid/application/field/field_cache.dart';
import 'package:app_flowy/plugins/grid/application/row/row_cache.dart';
import 'package:app_flowy/plugins/grid/application/row/row_service.dart';
import 'package:appflowy_board/appflowy_board.dart';
import 'package:dartz/dartz.dart';
import 'package:equatable/equatable.dart';
import 'package:flowy_sdk/log.dart';
import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/protobuf.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'dart:collection';
import 'board_data_controller.dart';
import 'group_controller.dart';
part 'board_bloc.freezed.dart';
class BoardBloc extends Bloc<BoardEvent, BoardState> {
final BoardDataController _dataController;
late final AFBoardDataController afBoardDataController;
final MoveRowFFIService _rowService;
Map<String, GroupController> groupControllers = {};
GridFieldCache get fieldCache => _dataController.fieldCache;
String get gridId => _dataController.gridId;
BoardBloc({required ViewPB view})
: _rowService = MoveRowFFIService(gridId: view.id),
_dataController = BoardDataController(view: view),
super(BoardState.initial(view.id)) {
afBoardDataController = AFBoardDataController(
onMoveColumn: (
fromIndex,
toIndex,
) {},
onMoveColumnItem: (
columnId,
fromIndex,
toIndex,
) {
final fromRow = groupControllers[columnId]?.rowAtIndex(fromIndex);
final toRow = groupControllers[columnId]?.rowAtIndex(toIndex);
_moveRow(fromRow, toRow);
},
onMoveColumnItemToColumn: (
fromColumnId,
fromIndex,
toColumnId,
toIndex,
) {
final fromRow = groupControllers[fromColumnId]?.rowAtIndex(fromIndex);
final toRow = groupControllers[toColumnId]?.rowAtIndex(toIndex);
_moveRow(fromRow, toRow);
},
);
on<BoardEvent>(
(event, emit) async {
await event.when(
initial: () async {
_startListening();
await _loadGrid(emit);
},
createRow: (groupId) async {
final result = await _dataController.createBoardCard(groupId);
result.fold(
(rowPB) {
emit(state.copyWith(editingRow: some(rowPB)));
},
(err) => Log.error(err),
);
},
endEditRow: (rowId) {
assert(state.editingRow.isSome());
state.editingRow.fold(() => null, (row) {
assert(row.id == rowId);
emit(state.copyWith(editingRow: none()));
});
},
didReceiveGridUpdate: (GridPB grid) {
emit(state.copyWith(grid: Some(grid)));
},
didReceiveRows: (List<RowInfo> rowInfos) {
emit(state.copyWith(rowInfos: rowInfos));
},
didReceiveError: (FlowyError error) {
emit(state.copyWith(noneOrError: some(error)));
},
);
},
);
}
void _moveRow(RowPB? fromRow, RowPB? toRow) {
if (fromRow != null && toRow != null) {
_rowService
.moveRow(
fromRowId: fromRow.id,
toRowId: toRow.id,
)
.then((result) {
result.fold((l) => null, (r) => add(BoardEvent.didReceiveError(r)));
});
}
}
@override
Future<void> close() async {
await _dataController.dispose();
for (final controller in groupControllers.values) {
controller.dispose();
}
return super.close();
}
void initializeGroups(List<GroupPB> groups) {
for (final group in groups) {
final delegate = GroupControllerDelegateImpl(afBoardDataController);
final controller = GroupController(
gridId: state.gridId,
group: group,
delegate: delegate,
);
controller.startListening();
groupControllers[controller.group.groupId] = (controller);
}
}
GridRowCache? getRowCache(String blockId) {
final GridBlockCache? blockCache = _dataController.blocks[blockId];
return blockCache?.rowCache;
}
void _startListening() {
_dataController.addListener(
onGridChanged: (grid) {
if (!isClosed) {
add(BoardEvent.didReceiveGridUpdate(grid));
}
},
didLoadGroups: (groups) {
List<AFBoardColumnData> columns = groups.map((group) {
return AFBoardColumnData(
id: group.groupId,
desc: group.desc,
items: _buildRows(group.rows),
customData: group,
);
}).toList();
afBoardDataController.addColumns(columns);
initializeGroups(groups);
},
onRowsChanged: (List<RowInfo> rowInfos, RowsChangedReason reason) {
add(BoardEvent.didReceiveRows(rowInfos));
},
onError: (err) {
Log.error(err);
},
);
}
List<AFColumnItem> _buildRows(List<RowPB> rows) {
final items = rows.map((row) {
return BoardColumnItem(row: row);
}).toList();
return <AFColumnItem>[...items];
}
Future<void> _loadGrid(Emitter<BoardState> emit) async {
final result = await _dataController.loadData();
result.fold(
(grid) => emit(
state.copyWith(loadingState: GridLoadingState.finish(left(unit))),
),
(err) => emit(
state.copyWith(loadingState: GridLoadingState.finish(right(err))),
),
);
}
}
@freezed
class BoardEvent with _$BoardEvent {
const factory BoardEvent.initial() = InitialGrid;
const factory BoardEvent.createRow(String groupId) = _CreateRow;
const factory BoardEvent.endEditRow(String rowId) = _EndEditRow;
const factory BoardEvent.didReceiveError(FlowyError error) = _DidReceiveError;
const factory BoardEvent.didReceiveRows(List<RowInfo> rowInfos) =
_DidReceiveRows;
const factory BoardEvent.didReceiveGridUpdate(
GridPB grid,
) = _DidReceiveGridUpdate;
}
@freezed
class BoardState with _$BoardState {
const factory BoardState({
required String gridId,
required Option<GridPB> grid,
required Option<RowPB> editingRow,
required List<RowInfo> rowInfos,
required GridLoadingState loadingState,
required Option<FlowyError> noneOrError,
}) = _BoardState;
factory BoardState.initial(String gridId) => BoardState(
rowInfos: [],
grid: none(),
gridId: gridId,
editingRow: none(),
noneOrError: none(),
loadingState: const _Loading(),
);
}
@freezed
class GridLoadingState with _$GridLoadingState {
const factory GridLoadingState.loading() = _Loading;
const factory GridLoadingState.finish(
Either<Unit, FlowyError> successOrFail) = _Finish;
}
class GridFieldEquatable extends Equatable {
final UnmodifiableListView<FieldPB> _fields;
const GridFieldEquatable(
UnmodifiableListView<FieldPB> fields,
) : _fields = fields;
@override
List<Object?> get props {
if (_fields.isEmpty) {
return [];
}
return [
_fields.length,
_fields
.map((field) => field.width)
.reduce((value, element) => value + element),
];
}
UnmodifiableListView<FieldPB> get value => UnmodifiableListView(_fields);
}
class BoardColumnItem extends AFColumnItem {
final RowPB row;
BoardColumnItem({required this.row});
@override
String get id => row.id;
}
class CreateCardItem extends AFColumnItem {
@override
String get id => '$CreateCardItem';
}
class GroupControllerDelegateImpl extends GroupControllerDelegate {
final AFBoardDataController controller;
GroupControllerDelegateImpl(this.controller);
@override
void insertRow(String groupId, RowPB row, int? index) {
final item = BoardColumnItem(row: row);
if (index != null) {
controller.insertColumnItem(groupId, index, item);
} else {
controller.addColumnItem(groupId, item);
}
}
@override
void removeRow(String groupId, String rowId) {
controller.removeColumnItem(groupId, rowId);
}
@override
void updateRow(String groupId, RowPB row) {
//
}
}

View File

@ -0,0 +1,142 @@
import 'dart:collection';
import 'package:app_flowy/plugins/grid/application/block/block_cache.dart';
import 'package:app_flowy/plugins/grid/application/field/field_cache.dart';
import 'package:app_flowy/plugins/grid/application/grid_service.dart';
import 'package:app_flowy/plugins/grid/application/row/row_cache.dart';
import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart';
import 'dart:async';
import 'package:dartz/dartz.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/protobuf.dart';
typedef OnFieldsChanged = void Function(UnmodifiableListView<FieldPB>);
typedef OnGridChanged = void Function(GridPB);
typedef DidLoadGroups = void Function(List<GroupPB>);
typedef OnRowsChanged = void Function(
List<RowInfo>,
RowsChangedReason,
);
typedef OnError = void Function(FlowyError);
class BoardDataController {
final String gridId;
final GridFFIService _gridFFIService;
final GridFieldCache fieldCache;
// key: the block id
final LinkedHashMap<String, GridBlockCache> _blocks;
LinkedHashMap<String, GridBlockCache> get blocks => _blocks;
OnFieldsChanged? _onFieldsChanged;
OnGridChanged? _onGridChanged;
DidLoadGroups? _didLoadGroup;
OnRowsChanged? _onRowsChanged;
OnError? _onError;
List<RowInfo> get rowInfos {
final List<RowInfo> rows = [];
for (var block in _blocks.values) {
rows.addAll(block.rows);
}
return rows;
}
BoardDataController({required ViewPB view})
: gridId = view.id,
_blocks = LinkedHashMap.new(),
_gridFFIService = GridFFIService(gridId: view.id),
fieldCache = GridFieldCache(gridId: view.id);
void addListener({
OnGridChanged? onGridChanged,
OnFieldsChanged? onFieldsChanged,
DidLoadGroups? didLoadGroups,
OnRowsChanged? onRowsChanged,
OnError? onError,
}) {
_onGridChanged = onGridChanged;
_onFieldsChanged = onFieldsChanged;
_didLoadGroup = didLoadGroups;
_onRowsChanged = onRowsChanged;
_onError = onError;
fieldCache.addListener(onFields: (fields) {
_onFieldsChanged?.call(UnmodifiableListView(fields));
});
}
Future<Either<Unit, FlowyError>> loadData() async {
final result = await _gridFFIService.loadGrid();
return Future(
() => result.fold(
(grid) async {
_onGridChanged?.call(grid);
return await _loadFields(grid).then((result) {
return result.fold(
(l) {
_loadGroups(grid.blocks);
return left(l);
},
(err) => right(err),
);
});
},
(err) => right(err),
),
);
}
Future<Either<RowPB, FlowyError>> createBoardCard(String groupId) {
return _gridFFIService.createBoardCard(groupId);
}
Future<void> dispose() async {
await _gridFFIService.closeGrid();
await fieldCache.dispose();
for (final blockCache in _blocks.values) {
blockCache.dispose();
}
}
Future<Either<Unit, FlowyError>> _loadFields(GridPB grid) async {
final result = await _gridFFIService.getFields(fieldIds: grid.fields);
return Future(
() => result.fold(
(fields) {
fieldCache.fields = fields.items;
_onFieldsChanged?.call(UnmodifiableListView(fieldCache.fields));
return left(unit);
},
(err) => right(err),
),
);
}
Future<void> _loadGroups(List<BlockPB> blocks) async {
for (final block in blocks) {
final cache = GridBlockCache(
gridId: gridId,
block: block,
fieldCache: fieldCache,
);
cache.addListener(onRowsChanged: (reason) {
_onRowsChanged?.call(rowInfos, reason);
});
_blocks[block.id] = cache;
}
final result = await _gridFFIService.loadGroups();
return Future(
() => result.fold(
(groups) {
_didLoadGroup?.call(groups.items);
},
(err) => _onError?.call(err),
),
);
}
}

View File

@ -0,0 +1,71 @@
import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'dart:async';
part 'board_checkbox_cell_bloc.freezed.dart';
class BoardCheckboxCellBloc
extends Bloc<BoardCheckboxCellEvent, BoardCheckboxCellState> {
final GridCheckboxCellController cellController;
void Function()? _onCellChangedFn;
BoardCheckboxCellBloc({
required this.cellController,
}) : super(BoardCheckboxCellState.initial(cellController)) {
on<BoardCheckboxCellEvent>(
(event, emit) async {
await event.when(
initial: () async {
_startListening();
},
didReceiveCellUpdate: (cellData) {
emit(state.copyWith(isSelected: _isSelected(cellData)));
},
);
},
);
}
@override
Future<void> close() async {
if (_onCellChangedFn != null) {
cellController.removeListener(_onCellChangedFn!);
_onCellChangedFn = null;
}
cellController.dispose();
return super.close();
}
void _startListening() {
_onCellChangedFn = cellController.startListening(
onCellChanged: ((cellContent) {
if (!isClosed) {
add(BoardCheckboxCellEvent.didReceiveCellUpdate(cellContent ?? ""));
}
}),
);
}
}
@freezed
class BoardCheckboxCellEvent with _$BoardCheckboxCellEvent {
const factory BoardCheckboxCellEvent.initial() = _InitialCell;
const factory BoardCheckboxCellEvent.didReceiveCellUpdate(
String cellContent) = _DidReceiveCellUpdate;
}
@freezed
class BoardCheckboxCellState with _$BoardCheckboxCellState {
const factory BoardCheckboxCellState({
required bool isSelected,
}) = _CheckboxCellState;
factory BoardCheckboxCellState.initial(GridCellController context) {
return BoardCheckboxCellState(
isSelected: _isSelected(context.getCellData()));
}
}
bool _isSelected(String? cellData) {
return cellData == "Yes";
}

View File

@ -0,0 +1,85 @@
import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/date_type_option_entities.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'dart:async';
part 'board_date_cell_bloc.freezed.dart';
class BoardDateCellBloc extends Bloc<BoardDateCellEvent, BoardDateCellState> {
final GridDateCellController cellController;
void Function()? _onCellChangedFn;
BoardDateCellBloc({required this.cellController})
: super(BoardDateCellState.initial(cellController)) {
on<BoardDateCellEvent>(
(event, emit) async {
event.when(
initial: () => _startListening(),
didReceiveCellUpdate: (DateCellDataPB? cellData) {
emit(state.copyWith(
data: cellData, dateStr: _dateStrFromCellData(cellData)));
},
didReceiveFieldUpdate: (FieldPB value) =>
emit(state.copyWith(field: value)),
);
},
);
}
@override
Future<void> close() async {
if (_onCellChangedFn != null) {
cellController.removeListener(_onCellChangedFn!);
_onCellChangedFn = null;
}
cellController.dispose();
return super.close();
}
void _startListening() {
_onCellChangedFn = cellController.startListening(
onCellChanged: ((data) {
if (!isClosed) {
add(BoardDateCellEvent.didReceiveCellUpdate(data));
}
}),
);
}
}
@freezed
class BoardDateCellEvent with _$BoardDateCellEvent {
const factory BoardDateCellEvent.initial() = _InitialCell;
const factory BoardDateCellEvent.didReceiveCellUpdate(DateCellDataPB? data) =
_DidReceiveCellUpdate;
const factory BoardDateCellEvent.didReceiveFieldUpdate(FieldPB field) =
_DidReceiveFieldUpdate;
}
@freezed
class BoardDateCellState with _$BoardDateCellState {
const factory BoardDateCellState({
required DateCellDataPB? data,
required String dateStr,
required FieldPB field,
}) = _BoardDateCellState;
factory BoardDateCellState.initial(GridDateCellController context) {
final cellData = context.getCellData();
return BoardDateCellState(
field: context.field,
data: cellData,
dateStr: _dateStrFromCellData(cellData),
);
}
}
String _dateStrFromCellData(DateCellDataPB? cellData) {
String dateStr = "";
if (cellData != null) {
dateStr = cellData.date + " " + cellData.time;
}
return dateStr;
}

View File

@ -0,0 +1,67 @@
import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'dart:async';
part 'board_number_cell_bloc.freezed.dart';
class BoardNumberCellBloc
extends Bloc<BoardNumberCellEvent, BoardNumberCellState> {
final GridNumberCellController cellController;
void Function()? _onCellChangedFn;
BoardNumberCellBloc({
required this.cellController,
}) : super(BoardNumberCellState.initial(cellController)) {
on<BoardNumberCellEvent>(
(event, emit) async {
await event.when(
initial: () async {
_startListening();
},
didReceiveCellUpdate: (content) {
emit(state.copyWith(content: content));
},
);
},
);
}
@override
Future<void> close() async {
if (_onCellChangedFn != null) {
cellController.removeListener(_onCellChangedFn!);
_onCellChangedFn = null;
}
cellController.dispose();
return super.close();
}
void _startListening() {
_onCellChangedFn = cellController.startListening(
onCellChanged: ((cellContent) {
if (!isClosed) {
add(BoardNumberCellEvent.didReceiveCellUpdate(cellContent ?? ""));
}
}),
);
}
}
@freezed
class BoardNumberCellEvent with _$BoardNumberCellEvent {
const factory BoardNumberCellEvent.initial() = _InitialCell;
const factory BoardNumberCellEvent.didReceiveCellUpdate(String cellContent) =
_DidReceiveCellUpdate;
}
@freezed
class BoardNumberCellState with _$BoardNumberCellState {
const factory BoardNumberCellState({
required String content,
}) = _BoardNumberCellState;
factory BoardNumberCellState.initial(GridCellController context) =>
BoardNumberCellState(
content: context.getCellData() ?? "",
);
}

View File

@ -0,0 +1,76 @@
import 'dart:async';
import 'package:flowy_sdk/protobuf/flowy-grid/select_option.pb.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart';
part 'board_select_option_cell_bloc.freezed.dart';
class BoardSelectOptionCellBloc
extends Bloc<BoardSelectOptionCellEvent, BoardSelectOptionCellState> {
final GridSelectOptionCellController cellController;
void Function()? _onCellChangedFn;
BoardSelectOptionCellBloc({
required this.cellController,
}) : super(BoardSelectOptionCellState.initial(cellController)) {
on<BoardSelectOptionCellEvent>(
(event, emit) async {
await event.when(
initial: () async {
_startListening();
},
didReceiveOptions: (List<SelectOptionPB> selectedOptions) {
emit(state.copyWith(selectedOptions: selectedOptions));
},
);
},
);
}
@override
Future<void> close() async {
if (_onCellChangedFn != null) {
cellController.removeListener(_onCellChangedFn!);
_onCellChangedFn = null;
}
cellController.dispose();
return super.close();
}
void _startListening() {
_onCellChangedFn = cellController.startListening(
onCellChanged: ((selectOptionContext) {
if (!isClosed) {
add(BoardSelectOptionCellEvent.didReceiveOptions(
selectOptionContext?.selectOptions ?? [],
));
}
}),
);
}
}
@freezed
class BoardSelectOptionCellEvent with _$BoardSelectOptionCellEvent {
const factory BoardSelectOptionCellEvent.initial() = _InitialCell;
const factory BoardSelectOptionCellEvent.didReceiveOptions(
List<SelectOptionPB> selectedOptions,
) = _DidReceiveOptions;
}
@freezed
class BoardSelectOptionCellState with _$BoardSelectOptionCellState {
const factory BoardSelectOptionCellState({
required List<SelectOptionPB> selectedOptions,
}) = _BoardSelectOptionCellState;
factory BoardSelectOptionCellState.initial(
GridSelectOptionCellController context) {
final data = context.getCellData();
return BoardSelectOptionCellState(
selectedOptions: data?.selectOptions ?? [],
);
}
}

View File

@ -0,0 +1,66 @@
import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'dart:async';
part 'board_text_cell_bloc.freezed.dart';
class BoardTextCellBloc extends Bloc<BoardTextCellEvent, BoardTextCellState> {
final GridCellController cellController;
void Function()? _onCellChangedFn;
BoardTextCellBloc({
required this.cellController,
}) : super(BoardTextCellState.initial(cellController)) {
on<BoardTextCellEvent>(
(event, emit) async {
await event.when(
initial: () async {
_startListening();
},
didReceiveCellUpdate: (content) {
emit(state.copyWith(content: content));
},
);
},
);
}
@override
Future<void> close() async {
if (_onCellChangedFn != null) {
cellController.removeListener(_onCellChangedFn!);
_onCellChangedFn = null;
}
cellController.dispose();
return super.close();
}
void _startListening() {
_onCellChangedFn = cellController.startListening(
onCellChanged: ((cellContent) {
if (!isClosed) {
add(BoardTextCellEvent.didReceiveCellUpdate(cellContent ?? ""));
}
}),
);
}
}
@freezed
class BoardTextCellEvent with _$BoardTextCellEvent {
const factory BoardTextCellEvent.initial() = _InitialCell;
const factory BoardTextCellEvent.didReceiveCellUpdate(String cellContent) =
_DidReceiveCellUpdate;
}
@freezed
class BoardTextCellState with _$BoardTextCellState {
const factory BoardTextCellState({
required String content,
}) = _BoardTextCellState;
factory BoardTextCellState.initial(GridCellController context) =>
BoardTextCellState(
content: context.getCellData() ?? "",
);
}

View File

@ -0,0 +1,78 @@
import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/url_type_option_entities.pb.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'dart:async';
part 'board_url_cell_bloc.freezed.dart';
class BoardURLCellBloc extends Bloc<BoardURLCellEvent, BoardURLCellState> {
final GridURLCellController cellController;
void Function()? _onCellChangedFn;
BoardURLCellBloc({
required this.cellController,
}) : super(BoardURLCellState.initial(cellController)) {
on<BoardURLCellEvent>(
(event, emit) async {
event.when(
initial: () {
_startListening();
},
didReceiveCellUpdate: (cellData) {
emit(state.copyWith(
content: cellData?.content ?? "",
url: cellData?.url ?? "",
));
},
updateURL: (String url) {
cellController.saveCellData(url, deduplicate: true);
},
);
},
);
}
@override
Future<void> close() async {
if (_onCellChangedFn != null) {
cellController.removeListener(_onCellChangedFn!);
_onCellChangedFn = null;
}
cellController.dispose();
return super.close();
}
void _startListening() {
_onCellChangedFn = cellController.startListening(
onCellChanged: ((cellData) {
if (!isClosed) {
add(BoardURLCellEvent.didReceiveCellUpdate(cellData));
}
}),
);
}
}
@freezed
class BoardURLCellEvent with _$BoardURLCellEvent {
const factory BoardURLCellEvent.initial() = _InitialCell;
const factory BoardURLCellEvent.updateURL(String url) = _UpdateURL;
const factory BoardURLCellEvent.didReceiveCellUpdate(URLCellDataPB? cell) =
_DidReceiveCellUpdate;
}
@freezed
class BoardURLCellState with _$BoardURLCellState {
const factory BoardURLCellState({
required String content,
required String url,
}) = _BoardURLCellState;
factory BoardURLCellState.initial(GridURLCellController context) {
final cellData = context.getCellData();
return BoardURLCellState(
content: cellData?.content ?? "",
url: cellData?.url ?? "",
);
}
}

View File

@ -0,0 +1,116 @@
import 'dart:collection';
import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart';
import 'package:app_flowy/plugins/grid/application/row/row_cache.dart';
import 'package:app_flowy/plugins/grid/application/row/row_service.dart';
import 'package:equatable/equatable.dart';
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_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'dart:async';
import 'card_data_controller.dart';
part 'card_bloc.freezed.dart';
class BoardCardBloc extends Bloc<BoardCardEvent, BoardCardState> {
final RowFFIService _rowService;
final CardDataController _dataController;
BoardCardBloc({
required String gridId,
required CardDataController dataController,
}) : _rowService = RowFFIService(
gridId: gridId,
blockId: dataController.rowPB.blockId,
),
_dataController = dataController,
super(BoardCardState.initial(
dataController.rowPB, dataController.loadData())) {
on<BoardCardEvent>(
(event, emit) async {
await event.map(
initial: (_InitialRow value) async {
await _startListening();
},
didReceiveCells: (_DidReceiveCells value) async {
final cells = value.gridCellMap.values
.map((e) => GridCellEquatable(e.field))
.toList();
emit(state.copyWith(
gridCellMap: value.gridCellMap,
cells: UnmodifiableListView(cells),
changeReason: value.reason,
));
},
);
},
);
}
@override
Future<void> close() async {
_dataController.dispose();
return super.close();
}
RowInfo rowInfo() {
return RowInfo(
gridId: _rowService.gridId,
fields: UnmodifiableListView(
state.cells.map((cell) => cell._field).toList(),
),
rowPB: state.rowPB,
);
}
Future<void> _startListening() async {
_dataController.addListener(
onRowChanged: (cells, reason) {
if (!isClosed) {
add(BoardCardEvent.didReceiveCells(cells, reason));
}
},
);
}
}
@freezed
class BoardCardEvent with _$BoardCardEvent {
const factory BoardCardEvent.initial() = _InitialRow;
const factory BoardCardEvent.didReceiveCells(
GridCellMap gridCellMap, RowsChangedReason reason) = _DidReceiveCells;
}
@freezed
class BoardCardState with _$BoardCardState {
const factory BoardCardState({
required RowPB rowPB,
required GridCellMap gridCellMap,
required UnmodifiableListView<GridCellEquatable> cells,
RowsChangedReason? changeReason,
}) = _BoardCardState;
factory BoardCardState.initial(RowPB rowPB, GridCellMap cellDataMap) =>
BoardCardState(
rowPB: rowPB,
gridCellMap: cellDataMap,
cells: UnmodifiableListView(
cellDataMap.values.map((e) => GridCellEquatable(e.field)).toList(),
),
);
}
class GridCellEquatable extends Equatable {
final FieldPB _field;
const GridCellEquatable(FieldPB field) : _field = field;
@override
List<Object?> get props => [
_field.id,
_field.fieldType,
_field.visibility,
_field.width,
];
}

View File

@ -0,0 +1,49 @@
import 'package:app_flowy/plugins/board/presentation/card/card_cell_builder.dart';
import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart';
import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_field_notifier.dart';
import 'package:app_flowy/plugins/grid/application/field/field_cache.dart';
import 'package:app_flowy/plugins/grid/application/row/row_cache.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/block_entities.pb.dart';
import 'package:flutter/foundation.dart';
typedef OnCardChanged = void Function(GridCellMap, RowsChangedReason);
class CardDataController extends BoardCellBuilderDelegate {
final RowPB rowPB;
final GridFieldCache _fieldCache;
final GridRowCache _rowCache;
final List<VoidCallback> _onCardChangedListeners = [];
CardDataController({
required this.rowPB,
required GridFieldCache fieldCache,
required GridRowCache rowCache,
}) : _fieldCache = fieldCache,
_rowCache = rowCache;
GridCellMap loadData() {
return _rowCache.loadGridCells(rowPB.id);
}
void addListener({OnCardChanged? onRowChanged}) {
_onCardChangedListeners.add(_rowCache.addListener(
rowId: rowPB.id,
onCellUpdated: onRowChanged,
));
}
void dispose() {
for (final fn in _onCardChangedListeners) {
_rowCache.removeRowListener(fn);
}
}
@override
GridCellFieldNotifier buildFieldNotifier() {
return GridCellFieldNotifier(
notifier: GridCellFieldNotifierImpl(_fieldCache));
}
@override
GridCellCache get cellCache => _rowCache.cellCache;
}

View File

@ -0,0 +1,20 @@
import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
class BoardGroupService {
final String gridId;
FieldPB? groupField;
BoardGroupService(this.gridId);
void setGroupField(FieldPB field) {
groupField = field;
}
}
abstract class CanBeGroupField {
String get groupContent;
}
// class SingleSelectGroup extends CanBeGroupField {
// final SingleSelectTypeOptionContext typeOptionContext;
// }

View File

@ -0,0 +1,63 @@
import 'package:flowy_sdk/log.dart';
import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/protobuf.dart';
import 'group_listener.dart';
typedef OnGroupError = void Function(FlowyError);
abstract class GroupControllerDelegate {
void removeRow(String groupId, String rowId);
void insertRow(String groupId, RowPB row, int? index);
void updateRow(String groupId, RowPB row);
}
class GroupController {
final GroupPB group;
final GroupListener _listener;
final GroupControllerDelegate delegate;
GroupController({
required String gridId,
required this.group,
required this.delegate,
}) : _listener = GroupListener(group);
RowPB? rowAtIndex(int index) {
if (index < group.rows.length) {
return group.rows[index];
} else {
return null;
}
}
void startListening() {
_listener.start(onGroupChanged: (result) {
result.fold(
(GroupRowsChangesetPB changeset) {
for (final insertedRow in changeset.insertedRows) {
final index = insertedRow.hasIndex() ? insertedRow.index : null;
delegate.insertRow(
group.groupId,
insertedRow.row,
index,
);
}
for (final deletedRow in changeset.deletedRows) {
delegate.removeRow(group.groupId, deletedRow);
}
for (final updatedRow in changeset.updatedRows) {
delegate.updateRow(group.groupId, updatedRow);
}
},
(err) => Log.error(err),
);
});
}
Future<void> dispose() async {
_listener.stop();
}
}

View File

@ -0,0 +1,51 @@
import 'dart:typed_data';
import 'package:app_flowy/core/grid_notification.dart';
import 'package:flowy_infra/notifier.dart';
import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/dart_notification.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/group.pb.dart';
import 'package:dartz/dartz.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/group_changeset.pb.dart';
typedef UpdateGroupNotifiedValue = Either<GroupRowsChangesetPB, FlowyError>;
class GroupListener {
final GroupPB group;
PublishNotifier<UpdateGroupNotifiedValue>? _groupNotifier = PublishNotifier();
GridNotificationListener? _listener;
GroupListener(this.group);
void start({
required void Function(UpdateGroupNotifiedValue) onGroupChanged,
}) {
_groupNotifier?.addPublishListener(onGroupChanged);
_listener = GridNotificationListener(
objectId: group.groupId,
handler: _handler,
);
}
void _handler(
GridNotification ty,
Either<Uint8List, FlowyError> result,
) {
switch (ty) {
case GridNotification.DidUpdateGroup:
result.fold(
(payload) => _groupNotifier?.value =
left(GroupRowsChangesetPB.fromBuffer(payload)),
(error) => _groupNotifier?.value = right(error),
);
break;
default:
break;
}
}
Future<void> stop() async {
await _listener?.stop();
_groupNotifier?.dispose();
_groupNotifier = null;
}
}

View File

@ -20,15 +20,18 @@ class BoardPluginBuilder implements PluginBuilder {
String get menuName => "Board";
@override
PluginType get pluginType => DefaultPlugin.board.type();
PluginType get pluginType => PluginType.board;
@override
ViewDataType get dataType => ViewDataType.Grid;
ViewDataTypePB get dataType => ViewDataTypePB.Database;
@override
ViewLayoutTypePB? get subDataType => ViewLayoutTypePB.Board;
}
class BoardPluginConfig implements PluginConfig {
@override
bool get creatable => true;
bool get creatable => false;
}
class BoardPlugin extends Plugin {

View File

@ -1,17 +1,174 @@
// ignore_for_file: unused_field
import 'dart:collection';
import 'package:app_flowy/plugins/board/application/card/card_data_controller.dart';
import 'package:app_flowy/plugins/grid/application/row/row_cache.dart';
import 'package:app_flowy/plugins/grid/application/field/field_cache.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:appflowy_board/appflowy_board.dart';
import 'package:flowy_infra_ui/widget/error_page.dart';
import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/block_entities.pb.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../grid/application/row/row_cache.dart';
import '../application/board_bloc.dart';
import 'card/card.dart';
import 'card/card_cell_builder.dart';
class BoardPage extends StatelessWidget {
final ViewPB _view;
const BoardPage({required ViewPB view, Key? key})
: _view = view,
super(key: key);
final ViewPB view;
BoardPage({required this.view, Key? key}) : super(key: ValueKey(view.id));
@override
Widget build(BuildContext context) {
return Container();
return BlocProvider(
create: (context) =>
BoardBloc(view: view)..add(const BoardEvent.initial()),
child: BlocBuilder<BoardBloc, BoardState>(
builder: (context, state) {
return state.loadingState.map(
loading: (_) =>
const Center(child: CircularProgressIndicator.adaptive()),
finish: (result) {
return result.successOrFail.fold(
(_) => BoardContent(),
(err) => FlowyErrorPage(err.toString()),
);
},
);
},
),
);
}
}
class BoardContent extends StatelessWidget {
final config = AFBoardConfig(
columnBackgroundColor: HexColor.fromHex('#F7F8FC'),
);
BoardContent({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return BlocBuilder<BoardBloc, BoardState>(
builder: (context, state) {
return Container(
color: Colors.white,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 20),
child: AFBoard(
// key: UniqueKey(),
scrollController: ScrollController(),
dataController: context.read<BoardBloc>().afBoardDataController,
headerBuilder: _buildHeader,
footBuilder: _buildFooter,
cardBuilder: (_, data) => _buildCard(context, data),
columnConstraints: const BoxConstraints.tightFor(width: 240),
config: AFBoardConfig(
columnBackgroundColor: HexColor.fromHex('#F7F8FC'),
),
),
),
);
},
);
}
Widget _buildHeader(BuildContext context, AFBoardColumnData columnData) {
return AppFlowyColumnHeader(
icon: const Icon(Icons.lightbulb_circle),
title: Text(columnData.desc),
addIcon: const Icon(Icons.add, size: 20),
moreIcon: const Icon(Icons.more_horiz, size: 20),
height: 50,
margin: config.columnItemPadding,
);
}
Widget _buildFooter(BuildContext context, AFBoardColumnData columnData) {
return AppFlowyColumnFooter(
icon: const Icon(Icons.add, size: 20),
title: const Text('New'),
height: 50,
margin: config.columnItemPadding,
onAddButtonClick: () {
context.read<BoardBloc>().add(BoardEvent.createRow(columnData.id));
});
}
Widget _buildCard(BuildContext context, AFColumnItem item) {
final rowPB = (item as BoardColumnItem).row;
final rowCache = context.read<BoardBloc>().getRowCache(rowPB.blockId);
/// Return placeholder widget if the rowCache is null.
if (rowCache == null) return SizedBox(key: ObjectKey(item));
final fieldCache = context.read<BoardBloc>().fieldCache;
final gridId = context.read<BoardBloc>().gridId;
final cardController = CardDataController(
fieldCache: fieldCache,
rowCache: rowCache,
rowPB: rowPB,
);
final cellBuilder = BoardCellBuilder(cardController);
final isEditing = context.read<BoardBloc>().state.editingRow.fold(
() => false,
(editingRow) => editingRow.id == rowPB.id,
);
return AppFlowyColumnItemCard(
key: ObjectKey(item),
child: BoardCard(
gridId: gridId,
isEditing: isEditing,
cellBuilder: cellBuilder,
dataController: cardController,
onEditEditing: (rowId) {
context.read<BoardBloc>().add(BoardEvent.endEditRow(rowId));
},
openCard: (context) => _openCard(
gridId,
fieldCache,
rowPB,
rowCache,
context,
),
),
);
}
void _openCard(String gridId, GridFieldCache fieldCache, RowPB rowPB,
GridRowCache rowCache, BuildContext context) {
final rowInfo = RowInfo(
gridId: gridId,
fields: UnmodifiableListView(fieldCache.fields),
rowPB: rowPB,
);
final dataController = GridRowDataController(
rowInfo: rowInfo,
fieldCache: fieldCache,
rowCache: rowCache,
);
RowDetailPage(
cellBuilder: GridCellBuilder(delegate: dataController),
dataController: dataController,
).show(context);
}
}
extension HexColor on Color {
static Color fromHex(String hexString) {
final buffer = StringBuffer();
if (hexString.length == 6 || hexString.length == 7) buffer.write('ff');
buffer.write(hexString.replaceFirst('#', ''));
return Color(int.parse(buffer.toString(), radix: 16));
}
}

View File

@ -0,0 +1,59 @@
import 'package:app_flowy/plugins/board/application/card/board_checkbox_cell_bloc.dart';
import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart';
import 'package:flowy_infra/image.dart';
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class BoardCheckboxCell extends StatefulWidget {
final GridCellControllerBuilder cellControllerBuilder;
const BoardCheckboxCell({
required this.cellControllerBuilder,
Key? key,
}) : super(key: key);
@override
State<BoardCheckboxCell> createState() => _BoardCheckboxCellState();
}
class _BoardCheckboxCellState extends State<BoardCheckboxCell> {
late BoardCheckboxCellBloc _cellBloc;
@override
void initState() {
final cellController =
widget.cellControllerBuilder.build() as GridCheckboxCellController;
_cellBloc = BoardCheckboxCellBloc(cellController: cellController);
_cellBloc.add(const BoardCheckboxCellEvent.initial());
super.initState();
}
@override
Widget build(BuildContext context) {
return BlocProvider.value(
value: _cellBloc,
child: BlocBuilder<BoardCheckboxCellBloc, BoardCheckboxCellState>(
builder: (context, state) {
final icon = state.isSelected
? svgWidget('editor/editor_check')
: svgWidget('editor/editor_uncheck');
return Align(
alignment: Alignment.centerLeft,
child: FlowyIconButton(
iconPadding: EdgeInsets.zero,
icon: icon,
width: 20,
),
);
},
),
);
}
@override
Future<void> dispose() async {
_cellBloc.close();
super.dispose();
}
}

View File

@ -0,0 +1,59 @@
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_ui/style_widget/text.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class BoardDateCell extends StatefulWidget {
final GridCellControllerBuilder cellControllerBuilder;
const BoardDateCell({
required this.cellControllerBuilder,
Key? key,
}) : super(key: key);
@override
State<BoardDateCell> createState() => _BoardDateCellState();
}
class _BoardDateCellState extends State<BoardDateCell> {
late BoardDateCellBloc _cellBloc;
@override
void initState() {
final cellController =
widget.cellControllerBuilder.build() as GridDateCellController;
_cellBloc = BoardDateCellBloc(cellController: cellController)
..add(const BoardDateCellEvent.initial());
super.initState();
}
@override
Widget build(BuildContext context) {
return BlocProvider.value(
value: _cellBloc,
child: BlocBuilder<BoardDateCellBloc, BoardDateCellState>(
builder: (context, state) {
if (state.dateStr.isEmpty) {
return const SizedBox();
} else {
return Align(
alignment: Alignment.centerLeft,
child: FlowyText.regular(
state.dateStr,
fontSize: 14,
),
);
}
},
),
);
}
@override
Future<void> dispose() async {
_cellBloc.close();
super.dispose();
}
}

View File

@ -0,0 +1,59 @@
import 'package:app_flowy/plugins/board/application/card/board_number_cell_bloc.dart';
import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class BoardNumberCell extends StatefulWidget {
final GridCellControllerBuilder cellControllerBuilder;
const BoardNumberCell({
required this.cellControllerBuilder,
Key? key,
}) : super(key: key);
@override
State<BoardNumberCell> createState() => _BoardNumberCellState();
}
class _BoardNumberCellState extends State<BoardNumberCell> {
late BoardNumberCellBloc _cellBloc;
@override
void initState() {
final cellController =
widget.cellControllerBuilder.build() as GridNumberCellController;
_cellBloc = BoardNumberCellBloc(cellController: cellController)
..add(const BoardNumberCellEvent.initial());
super.initState();
}
@override
Widget build(BuildContext context) {
return BlocProvider.value(
value: _cellBloc,
child: BlocBuilder<BoardNumberCellBloc, BoardNumberCellState>(
builder: (context, state) {
if (state.content.isEmpty) {
return const SizedBox();
} else {
return Align(
alignment: Alignment.centerLeft,
child: FlowyText.regular(
state.content,
fontSize: 14,
),
);
}
},
),
);
}
@override
Future<void> dispose() async {
_cellBloc.close();
super.dispose();
}
}

View File

@ -0,0 +1,63 @@
import 'package:app_flowy/plugins/board/application/card/board_select_option_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/select_option_cell/extension.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class BoardSelectOptionCell extends StatefulWidget {
final GridCellControllerBuilder cellControllerBuilder;
const BoardSelectOptionCell({
required this.cellControllerBuilder,
Key? key,
}) : super(key: key);
@override
State<BoardSelectOptionCell> createState() => _BoardSelectOptionCellState();
}
class _BoardSelectOptionCellState extends State<BoardSelectOptionCell> {
late BoardSelectOptionCellBloc _cellBloc;
@override
void initState() {
final cellController =
widget.cellControllerBuilder.build() as GridSelectOptionCellController;
_cellBloc = BoardSelectOptionCellBloc(cellController: cellController)
..add(const BoardSelectOptionCellEvent.initial());
super.initState();
}
@override
Widget build(BuildContext context) {
return BlocProvider.value(
value: _cellBloc,
child: BlocBuilder<BoardSelectOptionCellBloc, BoardSelectOptionCellState>(
builder: (context, state) {
final children = state.selectedOptions
.map((option) => SelectOptionTag.fromOption(
context: context,
option: option,
))
.toList();
return Align(
alignment: Alignment.centerLeft,
child: AbsorbPointer(
child: Wrap(
children: children,
spacing: 4,
runSpacing: 2,
),
),
);
},
),
);
}
@override
Future<void> dispose() async {
_cellBloc.close();
super.dispose();
}
}

View File

@ -0,0 +1,61 @@
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:flowy_infra_ui/style_widget/text.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class BoardTextCell extends StatefulWidget {
final GridCellControllerBuilder cellControllerBuilder;
const BoardTextCell({required this.cellControllerBuilder, Key? key})
: super(key: key);
@override
State<BoardTextCell> createState() => _BoardTextCellState();
}
class _BoardTextCellState extends State<BoardTextCell> {
late BoardTextCellBloc _cellBloc;
@override
void initState() {
final cellController =
widget.cellControllerBuilder.build() as GridCellController;
_cellBloc = BoardTextCellBloc(cellController: cellController)
..add(const BoardTextCellEvent.initial());
super.initState();
}
@override
Widget build(BuildContext context) {
return BlocProvider.value(
value: _cellBloc,
child: BlocBuilder<BoardTextCellBloc, BoardTextCellState>(
builder: (context, state) {
if (state.content.isEmpty) {
return const SizedBox();
} else {
return Align(
alignment: Alignment.centerLeft,
child: ConstrainedBox(
constraints: BoxConstraints.loose(
const Size(double.infinity, 100),
),
child: FlowyText.regular(
state.content,
fontSize: 14,
),
),
);
}
},
),
);
}
@override
Future<void> dispose() async {
_cellBloc.close();
super.dispose();
}
}

View File

@ -0,0 +1,66 @@
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:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class BoardUrlCell extends StatefulWidget {
final GridCellControllerBuilder cellControllerBuilder;
const BoardUrlCell({
required this.cellControllerBuilder,
Key? key,
}) : super(key: key);
@override
State<BoardUrlCell> createState() => _BoardUrlCellState();
}
class _BoardUrlCellState extends State<BoardUrlCell> {
late BoardURLCellBloc _cellBloc;
@override
void initState() {
final cellController =
widget.cellControllerBuilder.build() as GridURLCellController;
_cellBloc = BoardURLCellBloc(cellController: cellController);
_cellBloc.add(const BoardURLCellEvent.initial());
super.initState();
}
@override
Widget build(BuildContext context) {
final theme = context.watch<AppTheme>();
return BlocProvider.value(
value: _cellBloc,
child: BlocBuilder<BoardURLCellBloc, BoardURLCellState>(
builder: (context, state) {
if (state.content.isEmpty) {
return const SizedBox();
} else {
return Align(
alignment: Alignment.centerLeft,
child: RichText(
textAlign: TextAlign.left,
text: TextSpan(
text: state.content,
style: TextStyle(
color: theme.main2,
fontSize: 14,
decoration: TextDecoration.underline,
),
),
),
);
}
},
),
);
}
@override
Future<void> dispose() async {
_cellBloc.close();
super.dispose();
}
}

View File

@ -0,0 +1,98 @@
import 'package:app_flowy/plugins/board/application/card/card_bloc.dart';
import 'package:app_flowy/plugins/board/application/card/card_data_controller.dart';
import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart';
import 'package:app_flowy/plugins/grid/presentation/widgets/row/row_action_sheet.dart';
import 'package:flowy_infra/image.dart';
import 'package:flowy_infra/theme.dart';
import 'package:flowy_infra_ui/flowy_infra_ui_web.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'card_cell_builder.dart';
import 'card_container.dart';
typedef OnEndEditing = void Function(String rowId);
class BoardCard extends StatefulWidget {
final String gridId;
final bool isEditing;
final CardDataController dataController;
final BoardCellBuilder cellBuilder;
final OnEndEditing onEditEditing;
final void Function(BuildContext) openCard;
const BoardCard({
required this.gridId,
required this.isEditing,
required this.dataController,
required this.cellBuilder,
required this.onEditEditing,
required this.openCard,
Key? key,
}) : super(key: key);
@override
State<BoardCard> createState() => _BoardCardState();
}
class _BoardCardState extends State<BoardCard> {
late BoardCardBloc _cardBloc;
@override
void initState() {
_cardBloc = BoardCardBloc(
gridId: widget.gridId,
dataController: widget.dataController,
);
super.initState();
}
@override
Widget build(BuildContext context) {
return BlocProvider.value(
value: _cardBloc,
child: BlocBuilder<BoardCardBloc, BoardCardState>(
builder: (context, state) {
return BoardCardContainer(
accessoryBuilder: (context) {
return [const _CardMoreOption()];
},
onTap: (context) {
widget.openCard(context);
},
child: Column(
children: _makeCells(context, state.gridCellMap),
),
);
},
),
);
}
List<Widget> _makeCells(BuildContext context, GridCellMap cellMap) {
return cellMap.values.map(
(cellId) {
final child = widget.cellBuilder.buildCell(cellId);
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 5),
child: child,
);
},
).toList();
}
}
class _CardMoreOption extends StatelessWidget with CardAccessory {
const _CardMoreOption({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return svgWidget('home/details', color: context.read<AppTheme>().iconColor);
}
@override
void onTap(BuildContext context) {
GridRowActionSheet(
rowData: context.read<BoardCardBloc>().rowInfo(),
).show(context, direction: AnchorDirection.bottomWithCenterAligned);
}
}

View File

@ -0,0 +1,69 @@
import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
import 'package:flutter/material.dart';
import 'board_checkbox_cell.dart';
import 'board_date_cell.dart';
import 'board_number_cell.dart';
import 'board_select_option_cell.dart';
import 'board_text_cell.dart';
import 'board_url_cell.dart';
abstract class BoardCellBuilderDelegate
extends GridCellControllerBuilderDelegate {
GridCellCache get cellCache;
}
class BoardCellBuilder {
final BoardCellBuilderDelegate delegate;
BoardCellBuilder(this.delegate);
Widget buildCell(GridCellIdentifier cellId) {
final cellControllerBuilder = GridCellControllerBuilder(
delegate: delegate,
cellId: cellId,
cellCache: delegate.cellCache,
);
final key = cellId.key();
switch (cellId.fieldType) {
case FieldType.Checkbox:
return BoardCheckboxCell(
cellControllerBuilder: cellControllerBuilder,
key: key,
);
case FieldType.DateTime:
return BoardDateCell(
cellControllerBuilder: cellControllerBuilder,
key: key,
);
case FieldType.SingleSelect:
return BoardSelectOptionCell(
cellControllerBuilder: cellControllerBuilder,
key: key,
);
case FieldType.MultiSelect:
return BoardSelectOptionCell(
cellControllerBuilder: cellControllerBuilder,
key: key,
);
case FieldType.Number:
return BoardNumberCell(
cellControllerBuilder: cellControllerBuilder,
key: key,
);
case FieldType.RichText:
return BoardTextCell(
cellControllerBuilder: cellControllerBuilder,
key: key,
);
case FieldType.URL:
return BoardUrlCell(
cellControllerBuilder: cellControllerBuilder,
key: key,
);
}
throw UnimplementedError;
}
}

View File

@ -0,0 +1,142 @@
import 'package:flowy_infra/theme.dart';
import 'package:flowy_infra_ui/style_widget/hover.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:styled_widget/styled_widget.dart';
class BoardCardContainer extends StatelessWidget {
final Widget child;
final CardAccessoryBuilder? accessoryBuilder;
final void Function(BuildContext) onTap;
const BoardCardContainer({
required this.child,
required this.onTap,
this.accessoryBuilder,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => _CardContainerNotifier(),
child: Consumer<_CardContainerNotifier>(
builder: (context, notifier, _) {
Widget container = Center(child: child);
if (accessoryBuilder != null) {
final accessories = accessoryBuilder!(context);
if (accessories.isNotEmpty) {
container = _CardEnterRegion(
child: container,
accessories: accessories,
);
}
}
return GestureDetector(
onTap: () => onTap(context),
child: Padding(
padding: const EdgeInsets.all(8),
child: ConstrainedBox(
constraints: const BoxConstraints(minHeight: 30),
child: container,
),
),
);
},
),
);
}
}
abstract class CardAccessory implements Widget {
void onTap(BuildContext context);
}
typedef CardAccessoryBuilder = List<CardAccessory> Function(
BuildContext buildContext,
);
class CardAccessoryContainer extends StatelessWidget {
final List<CardAccessory> accessories;
const CardAccessoryContainer({required this.accessories, Key? key})
: super(key: key);
@override
Widget build(BuildContext context) {
final theme = context.read<AppTheme>();
final children = accessories.map((accessory) {
final hover = FlowyHover(
style: HoverStyle(
hoverColor: theme.hover,
backgroundColor: theme.surface,
),
builder: (_, onHover) => Container(
width: 26,
height: 26,
padding: const EdgeInsets.all(3),
child: accessory,
),
);
return GestureDetector(
child: hover,
behavior: HitTestBehavior.opaque,
onTap: () => accessory.onTap(context),
);
}).toList();
return Wrap(children: children, spacing: 6);
}
}
class _CardEnterRegion extends StatelessWidget {
final Widget child;
final List<CardAccessory> accessories;
const _CardEnterRegion(
{required this.child, required this.accessories, Key? key})
: super(key: key);
@override
Widget build(BuildContext context) {
return Selector<_CardContainerNotifier, bool>(
selector: (context, notifier) => notifier.onEnter,
builder: (context, onEnter, _) {
List<Widget> children = [child];
if (onEnter) {
children.add(CardAccessoryContainer(accessories: accessories)
.positioned(right: 0));
}
return MouseRegion(
cursor: SystemMouseCursors.click,
onEnter: (p) =>
Provider.of<_CardContainerNotifier>(context, listen: false)
.onEnter = true,
onExit: (p) =>
Provider.of<_CardContainerNotifier>(context, listen: false)
.onEnter = false,
child: IntrinsicHeight(
child: Stack(
alignment: AlignmentDirectional.center,
fit: StackFit.expand,
children: children,
)),
);
},
);
}
}
class _CardContainerNotifier extends ChangeNotifier {
bool _onEnter = false;
_CardContainerNotifier();
set onEnter(bool value) {
if (_onEnter != value) {
_onEnter = value;
notifyListeners();
}
}
bool get onEnter => _onEnter;
}

View File

@ -1,4 +1,4 @@
library docuemnt_plugin;
library document_plugin;
import 'package:app_flowy/generated/locale_keys.g.dart';
import 'package:app_flowy/startup/plugin/plugin.dart';
@ -42,10 +42,10 @@ class DocumentPluginBuilder extends PluginBuilder {
String get menuName => LocaleKeys.document_menuName.tr();
@override
PluginType get pluginType => DefaultPlugin.quill.type();
PluginType get pluginType => PluginType.editor;
@override
ViewDataType get dataType => ViewDataType.TextBlock;
ViewDataTypePB get dataType => ViewDataTypePB.Text;
}
class DocumentPlugin implements Plugin {

View File

@ -11,7 +11,9 @@ import 'package:app_flowy/generated/locale_keys.g.dart';
class DocumentBanner extends StatelessWidget {
final void Function() onRestore;
final void Function() onDelete;
const DocumentBanner({required this.onRestore, required this.onDelete, Key? key}) : super(key: key);
const DocumentBanner(
{required this.onRestore, required this.onDelete, Key? key})
: super(key: key);
@override
Widget build(BuildContext context) {
@ -26,7 +28,8 @@ class DocumentBanner extends StatelessWidget {
fit: BoxFit.scaleDown,
child: Row(
children: [
FlowyText.medium(LocaleKeys.deletePagePrompt_text.tr(), color: Colors.white),
FlowyText.medium(LocaleKeys.deletePagePrompt_text.tr(),
color: Colors.white),
const HSpace(20),
BaseStyledButton(
minWidth: 160,
@ -37,7 +40,10 @@ class DocumentBanner extends StatelessWidget {
downColor: theme.main1,
outlineColor: Colors.white,
borderRadius: Corners.s8Border,
child: FlowyText.medium(LocaleKeys.deletePagePrompt_restore.tr(), color: Colors.white, fontSize: 14),
child: FlowyText.medium(
LocaleKeys.deletePagePrompt_restore.tr(),
color: Colors.white,
fontSize: 14),
onPressed: onRestore),
const HSpace(20),
BaseStyledButton(
@ -49,8 +55,10 @@ class DocumentBanner extends StatelessWidget {
downColor: theme.main1,
outlineColor: Colors.white,
borderRadius: Corners.s8Border,
child: FlowyText.medium(LocaleKeys.deletePagePrompt_deletePermanent.tr(),
color: Colors.white, fontSize: 14),
child: FlowyText.medium(
LocaleKeys.deletePagePrompt_deletePermanent.tr(),
color: Colors.white,
fontSize: 14),
onPressed: onDelete),
],
),

View File

@ -1,19 +1,19 @@
import 'dart:async';
import 'package:app_flowy/plugins/grid/application/grid_service.dart';
import 'package:app_flowy/plugins/grid/application/row/row_service.dart';
import 'package:flowy_sdk/log.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/block_entities.pb.dart';
import '../field/field_cache.dart';
import '../row/row_cache.dart';
import 'block_listener.dart';
/// Read https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/architecture/frontend/grid for more information
class GridBlockCache {
final String gridId;
final GridBlockPB block;
final BlockPB block;
late GridRowCache _rowCache;
late GridBlockListener _listener;
List<GridRowInfo> get rows => _rowCache.rows;
List<RowInfo> get rows => _rowCache.rows;
GridRowCache get rowCache => _rowCache;
GridBlockCache({
@ -24,7 +24,7 @@ class GridBlockCache {
_rowCache = GridRowCache(
gridId: gridId,
block: block,
notifier: GridRowCacheFieldNotifierImpl(fieldCache),
notifier: GridRowFieldNotifierImpl(fieldCache),
);
_listener = GridBlockListener(blockId: block.id);
@ -42,7 +42,7 @@ class GridBlockCache {
}
void addListener({
required void Function(GridRowChangeReason) onChangeReason,
required void Function(RowsChangedReason) onRowsChanged,
bool Function()? listenWhen,
}) {
_rowCache.onRowsChanged((reason) {
@ -50,7 +50,7 @@ class GridBlockCache {
return;
}
onChangeReason(reason);
onRowsChanged(reason);
});
}
}

View File

@ -3,19 +3,22 @@ import 'package:flutter/foundation.dart';
import 'cell_service.dart';
abstract class GridFieldChangedNotifier {
void onFieldChanged(void Function(GridFieldPB) callback);
void dispose();
abstract class IGridCellFieldNotifier {
void onCellFieldChanged(void Function(FieldPB) callback);
void onCellDispose();
}
/// GridPB's cell helper wrapper that enables each cell will get notified when the corresponding field was changed.
/// You Register an onFieldChanged callback to listen to the cell changes, and unregister if you don't want to listen.
class GridCellFieldNotifier {
/// fieldId: {objectId: callback}
final Map<String, Map<String, List<VoidCallback>>> _fieldListenerByFieldId = {};
final IGridCellFieldNotifier notifier;
GridCellFieldNotifier({required GridFieldChangedNotifier notifier}) {
notifier.onFieldChanged(
/// fieldId: {objectId: callback}
final Map<String, Map<String, List<VoidCallback>>> _fieldListenerByFieldId =
{};
GridCellFieldNotifier({required this.notifier}) {
notifier.onCellFieldChanged(
(field) {
final map = _fieldListenerByFieldId[field.id];
if (map != null) {
@ -55,6 +58,7 @@ class GridCellFieldNotifier {
}
Future<void> dispose() async {
notifier.onCellDispose();
_fieldListenerByFieldId.clear();
}
}

View File

@ -1,7 +1,5 @@
import 'dart:async';
import 'dart:collection';
import 'package:app_flowy/plugins/grid/application/grid_service.dart';
import 'package:dartz/dartz.dart';
import 'package:equatable/equatable.dart';
import 'package:flowy_sdk/dispatch/dispatch.dart';
@ -18,7 +16,8 @@ import 'package:app_flowy/plugins/grid/application/cell/cell_listener.dart';
import 'package:app_flowy/plugins/grid/application/field/field_service.dart';
import 'dart:convert' show utf8;
import '../../field/type_option/type_option_service.dart';
import '../../field/field_cache.dart';
import '../../field/type_option/type_option_context.dart';
import 'cell_field_notifier.dart';
part 'cell_service.freezed.dart';
part 'cell_data_loader.dart';
@ -61,7 +60,7 @@ class GridCellIdentifier with _$GridCellIdentifier {
const factory GridCellIdentifier({
required String gridId,
required String rowId,
required GridFieldPB field,
required FieldPB field,
}) = _GridCellIdentifier;
// ignore: unused_element

View File

@ -1,29 +1,32 @@
part of 'cell_service.dart';
typedef GridCellController = IGridCellController<String, String>;
typedef GridCheckboxCellController = IGridCellController<String, String>;
typedef GridNumberCellController = IGridCellController<String, String>;
typedef GridSelectOptionCellController
= IGridCellController<SelectOptionCellDataPB, String>;
typedef GridDateCellController
= IGridCellController<DateCellDataPB, CalendarData>;
typedef GridURLCellController = IGridCellController<URLCellDataPB, String>;
abstract class GridCellControllerBuilderDelegate {
GridCellFieldNotifier buildFieldNotifier();
}
class GridCellControllerBuilder {
final GridCellIdentifier _cellId;
final GridCellCache _cellCache;
final GridFieldCache _fieldCache;
final GridCellControllerBuilderDelegate delegate;
GridCellControllerBuilder({
required this.delegate,
required GridCellIdentifier cellId,
required GridCellCache cellCache,
required GridFieldCache fieldCache,
}) : _cellCache = cellCache,
_fieldCache = fieldCache,
_cellId = cellId;
IGridCellController build() {
final cellFieldNotifier = GridCellFieldNotifier(
notifier: _GridFieldChangedNotifierImpl(_fieldCache));
final cellFieldNotifier = delegate.buildFieldNotifier();
switch (_cellId.fieldType) {
case FieldType.Checkbox:
final cellDataLoader = GridCellDataLoader(
@ -57,7 +60,7 @@ class GridCellControllerBuilder {
parser: StringCellDataParser(),
reloadOnFieldChanged: true,
);
return GridCellController(
return GridNumberCellController(
cellId: _cellId,
cellCache: _cellCache,
cellDataLoader: cellDataLoader,
@ -126,7 +129,7 @@ class IGridCellController<T, D> extends Equatable {
final GridCellDataLoader<T> _cellDataLoader;
final IGridCellDataPersistence<D> _cellDataPersistence;
late final CellListener _cellListener;
CellListener? _cellListener;
ValueNotifier<T?>? _cellDataNotifier;
bool isListening = false;
@ -165,7 +168,7 @@ class IGridCellController<T, D> extends Equatable {
String get fieldId => cellId.field.id;
GridFieldPB get field => cellId.field;
FieldPB get field => cellId.field;
FieldType get fieldType => cellId.field.fieldType;
@ -185,7 +188,7 @@ class IGridCellController<T, D> extends Equatable {
/// For example:
/// user input: 12
/// cell display: $12
_cellListener.start(onCellChanged: (result) {
_cellListener?.start(onCellChanged: (result) {
result.fold(
(_) => _loadData(),
(err) => Log.error(err),
@ -239,14 +242,14 @@ class IGridCellController<T, D> extends Equatable {
.getFieldTypeOptionData(fieldType: fieldType)
.then((result) {
return result.fold(
(data) => parser.fromBuffer(data.typeOptionData),
(data) => left(parser.fromBuffer(data.typeOptionData)),
(err) => right(err),
);
});
}
/// Save the cell data to disk
/// You can set [dedeplicate] to true (default is false) to reduce the save operation.
/// You can set [deduplicate] to true (default is false) to reduce the save operation.
/// It's useful when you call this method when user editing the [TextField].
/// The default debounce interval is 300 milliseconds.
void saveCellData(D data,
@ -288,13 +291,14 @@ class IGridCellController<T, D> extends Equatable {
return;
}
_isDispose = true;
_cellListener.stop();
_cellListener?.stop();
_loadDataOperation?.cancel();
_saveDataOperation?.cancel();
_cellDataNotifier = null;
if (_onFieldChangedFn != null) {
_fieldNotifier.unregister(_cacheKey, _onFieldChangedFn!);
_fieldNotifier.dispose();
_onFieldChangedFn = null;
}
}
@ -304,14 +308,14 @@ class IGridCellController<T, D> extends Equatable {
[_cellsCache.get(_cacheKey) ?? "", cellId.rowId + cellId.field.id];
}
class _GridFieldChangedNotifierImpl extends GridFieldChangedNotifier {
class GridCellFieldNotifierImpl extends IGridCellFieldNotifier {
final GridFieldCache _cache;
FieldChangesetCallback? _onChangesetFn;
_GridFieldChangedNotifierImpl(GridFieldCache cache) : _cache = cache;
GridCellFieldNotifierImpl(GridFieldCache cache) : _cache = cache;
@override
void dispose() {
void onCellDispose() {
if (_onChangesetFn != null) {
_cache.removeListener(onChangesetListener: _onChangesetFn!);
_onChangesetFn = null;
@ -319,8 +323,8 @@ class _GridFieldChangedNotifierImpl extends GridFieldChangedNotifier {
}
@override
void onFieldChanged(void Function(GridFieldPB p1) callback) {
_onChangesetFn = (GridFieldChangesetPB changeset) {
void onCellFieldChanged(void Function(FieldPB p1) callback) {
_onChangesetFn = (FieldChangesetPB changeset) {
for (final updatedField in changeset.updatedFields) {
callback(updatedField);
}

View File

@ -6,13 +6,13 @@ import 'cell_service/cell_service.dart';
part 'checkbox_cell_bloc.freezed.dart';
class CheckboxCellBloc extends Bloc<CheckboxCellEvent, CheckboxCellState> {
final GridCellController cellContext;
final GridCheckboxCellController cellController;
void Function()? _onCellChangedFn;
CheckboxCellBloc({
required CellService service,
required this.cellContext,
}) : super(CheckboxCellState.initial(cellContext)) {
required this.cellController,
}) : super(CheckboxCellState.initial(cellController)) {
on<CheckboxCellEvent>(
(event, emit) async {
await event.when(
@ -33,16 +33,17 @@ class CheckboxCellBloc extends Bloc<CheckboxCellEvent, CheckboxCellState> {
@override
Future<void> close() async {
if (_onCellChangedFn != null) {
cellContext.removeListener(_onCellChangedFn!);
cellController.removeListener(_onCellChangedFn!);
_onCellChangedFn = null;
}
cellContext.dispose();
cellController.dispose();
return super.close();
}
void _startListening() {
_onCellChangedFn = cellContext.startListening(onCellChanged: ((cellData) {
_onCellChangedFn =
cellController.startListening(onCellChanged: ((cellData) {
if (!isClosed) {
add(CheckboxCellEvent.didReceiveCellUpdate(cellData));
}
@ -50,7 +51,7 @@ class CheckboxCellBloc extends Bloc<CheckboxCellEvent, CheckboxCellState> {
}
void _updateCellData() {
cellContext.saveCellData(!state.isSelected ? "Yes" : "No");
cellController.saveCellData(!state.isSelected ? "Yes" : "No");
}
}
@ -58,7 +59,8 @@ class CheckboxCellBloc extends Bloc<CheckboxCellEvent, CheckboxCellState> {
class CheckboxCellEvent with _$CheckboxCellEvent {
const factory CheckboxCellEvent.initial() = _Initial;
const factory CheckboxCellEvent.select() = _Selected;
const factory CheckboxCellEvent.didReceiveCellUpdate(String? cellData) = _DidReceiveCellUpdate;
const factory CheckboxCellEvent.didReceiveCellUpdate(String? cellData) =
_DidReceiveCellUpdate;
}
@freezed

View File

@ -18,14 +18,14 @@ import 'package:fixnum/fixnum.dart' as $fixnum;
part 'date_cal_bloc.freezed.dart';
class DateCalBloc extends Bloc<DateCalEvent, DateCalState> {
final GridDateCellController cellContext;
final GridDateCellController cellController;
void Function()? _onCellChangedFn;
DateCalBloc({
required DateTypeOption dateTypeOption,
required DateTypeOptionPB dateTypeOptionPB,
required DateCellDataPB? cellData,
required this.cellContext,
}) : super(DateCalState.initial(dateTypeOption, cellData)) {
required this.cellController,
}) : super(DateCalState.initial(dateTypeOptionPB, cellData)) {
on<DateCalEvent>(
(event, emit) async {
await event.when(
@ -102,7 +102,7 @@ class DateCalBloc extends Bloc<DateCalEvent, DateCalState> {
}
}
cellContext.saveCellData(newCalData, resultCallback: (result) {
cellController.saveCellData(newCalData, resultCallback: (result) {
result.fold(
() => updateCalData(Some(newCalData), none()),
(err) {
@ -120,7 +120,7 @@ class DateCalBloc extends Bloc<DateCalEvent, DateCalState> {
String timeFormatPrompt(FlowyError error) {
String msg = LocaleKeys.grid_field_invalidTimeFormat.tr() + ". ";
switch (state.dateTypeOption.timeFormat) {
switch (state.dateTypeOptionPB.timeFormat) {
case TimeFormat.TwelveHour:
msg = msg + "e.g. 01: 00 AM";
break;
@ -136,15 +136,15 @@ class DateCalBloc extends Bloc<DateCalEvent, DateCalState> {
@override
Future<void> close() async {
if (_onCellChangedFn != null) {
cellContext.removeListener(_onCellChangedFn!);
cellController.removeListener(_onCellChangedFn!);
_onCellChangedFn = null;
}
cellContext.dispose();
cellController.dispose();
return super.close();
}
void _startListening() {
_onCellChangedFn = cellContext.startListening(
_onCellChangedFn = cellController.startListening(
onCellChanged: ((cell) {
if (!isClosed) {
add(DateCalEvent.didReceiveCellUpdate(cell));
@ -159,8 +159,8 @@ class DateCalBloc extends Bloc<DateCalEvent, DateCalState> {
TimeFormat? timeFormat,
bool? includeTime,
}) async {
state.dateTypeOption.freeze();
final newDateTypeOption = state.dateTypeOption.rebuild((typeOption) {
state.dateTypeOptionPB.freeze();
final newDateTypeOption = state.dateTypeOptionPB.rebuild((typeOption) {
if (dateFormat != null) {
typeOption.dateFormat = dateFormat;
}
@ -175,14 +175,14 @@ class DateCalBloc extends Bloc<DateCalEvent, DateCalState> {
});
final result = await FieldService.updateFieldTypeOption(
gridId: cellContext.gridId,
fieldId: cellContext.field.id,
gridId: cellController.gridId,
fieldId: cellController.field.id,
typeOptionData: newDateTypeOption.writeToBuffer(),
);
result.fold(
(l) => emit(state.copyWith(
dateTypeOption: newDateTypeOption,
dateTypeOptionPB: newDateTypeOption,
timeHintText: _timeHintText(newDateTypeOption))),
(err) => Log.error(err),
);
@ -210,7 +210,7 @@ class DateCalEvent with _$DateCalEvent {
@freezed
class DateCalState with _$DateCalState {
const factory DateCalState({
required DateTypeOption dateTypeOption,
required DateTypeOptionPB dateTypeOptionPB,
required CalendarFormat format,
required DateTime focusedDay,
required Option<String> timeFormatError,
@ -220,24 +220,24 @@ class DateCalState with _$DateCalState {
}) = _DateCalState;
factory DateCalState.initial(
DateTypeOption dateTypeOption,
DateTypeOptionPB dateTypeOptionPB,
DateCellDataPB? cellData,
) {
Option<CalendarData> calData = calDataFromCellData(cellData);
final time = calData.foldRight("", (dateData, previous) => dateData.time);
return DateCalState(
dateTypeOption: dateTypeOption,
dateTypeOptionPB: dateTypeOptionPB,
format: CalendarFormat.month,
focusedDay: DateTime.now(),
time: time,
calData: calData,
timeFormatError: none(),
timeHintText: _timeHintText(dateTypeOption),
timeHintText: _timeHintText(dateTypeOptionPB),
);
}
}
String _timeHintText(DateTypeOption typeOption) {
String _timeHintText(DateTypeOptionPB typeOption) {
switch (typeOption.timeFormat) {
case TimeFormat.TwelveHour:
return LocaleKeys.document_date_timeHintTextInTwelveHour.tr();

View File

@ -7,18 +7,21 @@ import 'cell_service/cell_service.dart';
part 'date_cell_bloc.freezed.dart';
class DateCellBloc extends Bloc<DateCellEvent, DateCellState> {
final GridDateCellController cellContext;
final GridDateCellController cellController;
void Function()? _onCellChangedFn;
DateCellBloc({required this.cellContext}) : super(DateCellState.initial(cellContext)) {
DateCellBloc({required this.cellController})
: super(DateCellState.initial(cellController)) {
on<DateCellEvent>(
(event, emit) async {
event.when(
initial: () => _startListening(),
didReceiveCellUpdate: (DateCellDataPB? cellData) {
emit(state.copyWith(data: cellData, dateStr: _dateStrFromCellData(cellData)));
emit(state.copyWith(
data: cellData, dateStr: _dateStrFromCellData(cellData)));
},
didReceiveFieldUpdate: (GridFieldPB value) => emit(state.copyWith(field: value)),
didReceiveFieldUpdate: (FieldPB value) =>
emit(state.copyWith(field: value)),
);
},
);
@ -27,15 +30,15 @@ class DateCellBloc extends Bloc<DateCellEvent, DateCellState> {
@override
Future<void> close() async {
if (_onCellChangedFn != null) {
cellContext.removeListener(_onCellChangedFn!);
cellController.removeListener(_onCellChangedFn!);
_onCellChangedFn = null;
}
cellContext.dispose();
cellController.dispose();
return super.close();
}
void _startListening() {
_onCellChangedFn = cellContext.startListening(
_onCellChangedFn = cellController.startListening(
onCellChanged: ((data) {
if (!isClosed) {
add(DateCellEvent.didReceiveCellUpdate(data));
@ -48,8 +51,10 @@ class DateCellBloc extends Bloc<DateCellEvent, DateCellState> {
@freezed
class DateCellEvent with _$DateCellEvent {
const factory DateCellEvent.initial() = _InitialCell;
const factory DateCellEvent.didReceiveCellUpdate(DateCellDataPB? data) = _DidReceiveCellUpdate;
const factory DateCellEvent.didReceiveFieldUpdate(GridFieldPB field) = _DidReceiveFieldUpdate;
const factory DateCellEvent.didReceiveCellUpdate(DateCellDataPB? data) =
_DidReceiveCellUpdate;
const factory DateCellEvent.didReceiveFieldUpdate(FieldPB field) =
_DidReceiveFieldUpdate;
}
@freezed
@ -57,7 +62,7 @@ class DateCellState with _$DateCellState {
const factory DateCellState({
required DateCellDataPB? data,
required String dateStr,
required GridFieldPB field,
required FieldPB field,
}) = _DateCellState;
factory DateCellState.initial(GridDateCellController context) {

View File

@ -8,12 +8,12 @@ import 'cell_service/cell_service.dart';
part 'number_cell_bloc.freezed.dart';
class NumberCellBloc extends Bloc<NumberCellEvent, NumberCellState> {
final GridCellController cellContext;
final GridNumberCellController cellController;
void Function()? _onCellChangedFn;
NumberCellBloc({
required this.cellContext,
}) : super(NumberCellState.initial(cellContext)) {
required this.cellController,
}) : super(NumberCellState.initial(cellController)) {
on<NumberCellEvent>(
(event, emit) async {
event.when(
@ -24,11 +24,13 @@ class NumberCellBloc extends Bloc<NumberCellEvent, NumberCellState> {
emit(state.copyWith(content: content));
},
updateCell: (text) {
cellContext.saveCellData(text, resultCallback: (result) {
cellController.saveCellData(text, resultCallback: (result) {
result.fold(
() => null,
(err) {
if (!isClosed) add(NumberCellEvent.didReceiveCellUpdate(right(err)));
if (!isClosed) {
add(NumberCellEvent.didReceiveCellUpdate(right(err)));
}
},
);
});
@ -41,15 +43,15 @@ class NumberCellBloc extends Bloc<NumberCellEvent, NumberCellState> {
@override
Future<void> close() async {
if (_onCellChangedFn != null) {
cellContext.removeListener(_onCellChangedFn!);
cellController.removeListener(_onCellChangedFn!);
_onCellChangedFn = null;
}
cellContext.dispose();
cellController.dispose();
return super.close();
}
void _startListening() {
_onCellChangedFn = cellContext.startListening(
_onCellChangedFn = cellController.startListening(
onCellChanged: ((cellContent) {
if (!isClosed) {
add(NumberCellEvent.didReceiveCellUpdate(left(cellContent ?? "")));
@ -63,7 +65,8 @@ class NumberCellBloc extends Bloc<NumberCellEvent, NumberCellState> {
class NumberCellEvent with _$NumberCellEvent {
const factory NumberCellEvent.initial() = _Initial;
const factory NumberCellEvent.updateCell(String text) = _UpdateCell;
const factory NumberCellEvent.didReceiveCellUpdate(Either<String, FlowyError> cellContent) = _DidReceiveCellUpdate;
const factory NumberCellEvent.didReceiveCellUpdate(
Either<String, FlowyError> cellContent) = _DidReceiveCellUpdate;
}
@freezed

View File

@ -8,12 +8,12 @@ part 'select_option_cell_bloc.freezed.dart';
class SelectOptionCellBloc
extends Bloc<SelectOptionCellEvent, SelectOptionCellState> {
final GridSelectOptionCellController cellContext;
final GridSelectOptionCellController cellController;
void Function()? _onCellChangedFn;
SelectOptionCellBloc({
required this.cellContext,
}) : super(SelectOptionCellState.initial(cellContext)) {
required this.cellController,
}) : super(SelectOptionCellState.initial(cellController)) {
on<SelectOptionCellEvent>(
(event, emit) async {
await event.map(
@ -33,15 +33,15 @@ class SelectOptionCellBloc
@override
Future<void> close() async {
if (_onCellChangedFn != null) {
cellContext.removeListener(_onCellChangedFn!);
cellController.removeListener(_onCellChangedFn!);
_onCellChangedFn = null;
}
cellContext.dispose();
cellController.dispose();
return super.close();
}
void _startListening() {
_onCellChangedFn = cellContext.startListening(
_onCellChangedFn = cellController.startListening(
onCellChanged: ((selectOptionContext) {
if (!isClosed) {
add(SelectOptionCellEvent.didReceiveOptions(

View File

@ -111,7 +111,7 @@ class SelectOptionCellEditorBloc
void _loadOptions() {
_delayOperation?.cancel();
_delayOperation = Timer(const Duration(milliseconds: 10), () {
_selectOptionService.getOpitonContext().then((result) {
_selectOptionService.getOptionContext().then((result) {
if (isClosed) {
return;
}

View File

@ -15,7 +15,7 @@ class SelectOptionService {
String get rowId => cellId.rowId;
Future<Either<Unit, FlowyError>> create({required String name}) {
return TypeOptionService(gridId: gridId, fieldId: fieldId)
return TypeOptionFFIService(gridId: gridId, fieldId: fieldId)
.newOption(name: name)
.then(
(result) {
@ -55,7 +55,7 @@ class SelectOptionService {
return GridEventUpdateSelectOption(payload).send();
}
Future<Either<SelectOptionCellDataPB, FlowyError>> getOpitonContext() {
Future<Either<SelectOptionCellDataPB, FlowyError>> getOptionContext() {
final payload = GridCellIdPB.create()
..gridId = gridId
..fieldId = fieldId

View File

@ -6,11 +6,11 @@ import 'cell_service/cell_service.dart';
part 'text_cell_bloc.freezed.dart';
class TextCellBloc extends Bloc<TextCellEvent, TextCellState> {
final GridCellController cellContext;
final GridCellController cellController;
void Function()? _onCellChangedFn;
TextCellBloc({
required this.cellContext,
}) : super(TextCellState.initial(cellContext)) {
required this.cellController,
}) : super(TextCellState.initial(cellController)) {
on<TextCellEvent>(
(event, emit) async {
await event.when(
@ -18,7 +18,7 @@ class TextCellBloc extends Bloc<TextCellEvent, TextCellState> {
_startListening();
},
updateText: (text) {
cellContext.saveCellData(text);
cellController.saveCellData(text);
emit(state.copyWith(content: text));
},
didReceiveCellUpdate: (content) {
@ -32,15 +32,15 @@ class TextCellBloc extends Bloc<TextCellEvent, TextCellState> {
@override
Future<void> close() async {
if (_onCellChangedFn != null) {
cellContext.removeListener(_onCellChangedFn!);
cellController.removeListener(_onCellChangedFn!);
_onCellChangedFn = null;
}
cellContext.dispose();
cellController.dispose();
return super.close();
}
void _startListening() {
_onCellChangedFn = cellContext.startListening(
_onCellChangedFn = cellController.startListening(
onCellChanged: ((cellContent) {
if (!isClosed) {
add(TextCellEvent.didReceiveCellUpdate(cellContent ?? ""));
@ -53,7 +53,8 @@ class TextCellBloc extends Bloc<TextCellEvent, TextCellState> {
@freezed
class TextCellEvent with _$TextCellEvent {
const factory TextCellEvent.initial() = _InitialCell;
const factory TextCellEvent.didReceiveCellUpdate(String cellContent) = _DidReceiveCellUpdate;
const factory TextCellEvent.didReceiveCellUpdate(String cellContent) =
_DidReceiveCellUpdate;
const factory TextCellEvent.updateText(String text) = _UpdateText;
}

View File

@ -7,11 +7,11 @@ import 'cell_service/cell_service.dart';
part 'url_cell_bloc.freezed.dart';
class URLCellBloc extends Bloc<URLCellEvent, URLCellState> {
final GridURLCellController cellContext;
final GridURLCellController cellController;
void Function()? _onCellChangedFn;
URLCellBloc({
required this.cellContext,
}) : super(URLCellState.initial(cellContext)) {
required this.cellController,
}) : super(URLCellState.initial(cellController)) {
on<URLCellEvent>(
(event, emit) async {
event.when(
@ -25,7 +25,7 @@ class URLCellBloc extends Bloc<URLCellEvent, URLCellState> {
));
},
updateURL: (String url) {
cellContext.saveCellData(url, deduplicate: true);
cellController.saveCellData(url, deduplicate: true);
},
);
},
@ -35,15 +35,15 @@ class URLCellBloc extends Bloc<URLCellEvent, URLCellState> {
@override
Future<void> close() async {
if (_onCellChangedFn != null) {
cellContext.removeListener(_onCellChangedFn!);
cellController.removeListener(_onCellChangedFn!);
_onCellChangedFn = null;
}
cellContext.dispose();
cellController.dispose();
return super.close();
}
void _startListening() {
_onCellChangedFn = cellContext.startListening(
_onCellChangedFn = cellController.startListening(
onCellChanged: ((cellData) {
if (!isClosed) {
add(URLCellEvent.didReceiveCellUpdate(cellData));
@ -57,7 +57,8 @@ class URLCellBloc extends Bloc<URLCellEvent, URLCellState> {
class URLCellEvent with _$URLCellEvent {
const factory URLCellEvent.initial() = _InitialCell;
const factory URLCellEvent.updateURL(String url) = _UpdateURL;
const factory URLCellEvent.didReceiveCellUpdate(URLCellDataPB? cell) = _DidReceiveCellUpdate;
const factory URLCellEvent.didReceiveCellUpdate(URLCellDataPB? cell) =
_DidReceiveCellUpdate;
}
@freezed

View File

@ -7,11 +7,11 @@ import 'cell_service/cell_service.dart';
part 'url_cell_editor_bloc.freezed.dart';
class URLCellEditorBloc extends Bloc<URLCellEditorEvent, URLCellEditorState> {
final GridURLCellController cellContext;
final GridURLCellController cellController;
void Function()? _onCellChangedFn;
URLCellEditorBloc({
required this.cellContext,
}) : super(URLCellEditorState.initial(cellContext)) {
required this.cellController,
}) : super(URLCellEditorState.initial(cellController)) {
on<URLCellEditorEvent>(
(event, emit) async {
event.when(
@ -19,7 +19,7 @@ class URLCellEditorBloc extends Bloc<URLCellEditorEvent, URLCellEditorState> {
_startListening();
},
updateText: (text) {
cellContext.saveCellData(text, deduplicate: true);
cellController.saveCellData(text, deduplicate: true);
emit(state.copyWith(content: text));
},
didReceiveCellUpdate: (cellData) {
@ -33,15 +33,15 @@ class URLCellEditorBloc extends Bloc<URLCellEditorEvent, URLCellEditorState> {
@override
Future<void> close() async {
if (_onCellChangedFn != null) {
cellContext.removeListener(_onCellChangedFn!);
cellController.removeListener(_onCellChangedFn!);
_onCellChangedFn = null;
}
cellContext.dispose();
cellController.dispose();
return super.close();
}
void _startListening() {
_onCellChangedFn = cellContext.startListening(
_onCellChangedFn = cellController.startListening(
onCellChanged: ((cellData) {
if (!isClosed) {
add(URLCellEditorEvent.didReceiveCellUpdate(cellData));
@ -54,7 +54,8 @@ class URLCellEditorBloc extends Bloc<URLCellEditorEvent, URLCellEditorState> {
@freezed
class URLCellEditorEvent with _$URLCellEditorEvent {
const factory URLCellEditorEvent.initial() = _InitialCell;
const factory URLCellEditorEvent.didReceiveCellUpdate(URLCellDataPB? cell) = _DidReceiveCellUpdate;
const factory URLCellEditorEvent.didReceiveCellUpdate(URLCellDataPB? cell) =
_DidReceiveCellUpdate;
const factory URLCellEditorEvent.updateText(String text) = _UpdateText;
}

View File

@ -7,11 +7,13 @@ import 'field_service.dart';
part 'field_action_sheet_bloc.freezed.dart';
class FieldActionSheetBloc extends Bloc<FieldActionSheetEvent, FieldActionSheetState> {
class FieldActionSheetBloc
extends Bloc<FieldActionSheetEvent, FieldActionSheetState> {
final FieldService fieldService;
FieldActionSheetBloc({required GridFieldPB field, required this.fieldService})
: super(FieldActionSheetState.initial(FieldTypeOptionDataPB.create()..field_2 = field)) {
FieldActionSheetBloc({required FieldPB field, required this.fieldService})
: super(FieldActionSheetState.initial(
FieldTypeOptionDataPB.create()..field_2 = field)) {
on<FieldActionSheetEvent>(
(event, emit) async {
await event.map(
@ -57,7 +59,8 @@ class FieldActionSheetBloc extends Bloc<FieldActionSheetEvent, FieldActionSheetS
@freezed
class FieldActionSheetEvent with _$FieldActionSheetEvent {
const factory FieldActionSheetEvent.updateFieldName(String name) = _UpdateFieldName;
const factory FieldActionSheetEvent.updateFieldName(String name) =
_UpdateFieldName;
const factory FieldActionSheetEvent.hideField() = _HideField;
const factory FieldActionSheetEvent.duplicateField() = _DuplicateField;
const factory FieldActionSheetEvent.deleteField() = _DeleteField;
@ -72,7 +75,8 @@ class FieldActionSheetState with _$FieldActionSheetState {
required String fieldName,
}) = _FieldActionSheetState;
factory FieldActionSheetState.initial(FieldTypeOptionDataPB data) => FieldActionSheetState(
factory FieldActionSheetState.initial(FieldTypeOptionDataPB data) =>
FieldActionSheetState(
fieldTypeOptionData: data,
errorText: '',
fieldName: data.field_2.name,

View File

@ -0,0 +1,192 @@
import 'dart:collection';
import 'package:app_flowy/plugins/grid/application/field/grid_listener.dart';
import 'package:flowy_sdk/log.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
import 'package:flutter/foundation.dart';
import '../row/row_cache.dart';
class FieldsNotifier extends ChangeNotifier {
List<FieldPB> _fields = [];
set fields(List<FieldPB> fields) {
_fields = fields;
notifyListeners();
}
List<FieldPB> get fields => _fields;
}
typedef FieldChangesetCallback = void Function(FieldChangesetPB);
typedef FieldsCallback = void Function(List<FieldPB>);
class GridFieldCache {
final String gridId;
final GridFieldsListener _fieldListener;
FieldsNotifier? _fieldNotifier = FieldsNotifier();
final Map<FieldsCallback, VoidCallback> _fieldsCallbackMap = {};
final Map<FieldChangesetCallback, FieldChangesetCallback>
_changesetCallbackMap = {};
GridFieldCache({required this.gridId})
: _fieldListener = GridFieldsListener(gridId: gridId) {
_fieldListener.start(onFieldsChanged: (result) {
result.fold(
(changeset) {
_deleteFields(changeset.deletedFields);
_insertFields(changeset.insertedFields);
_updateFields(changeset.updatedFields);
for (final listener in _changesetCallbackMap.values) {
listener(changeset);
}
},
(err) => Log.error(err),
);
});
}
Future<void> dispose() async {
await _fieldListener.stop();
_fieldNotifier?.dispose();
_fieldNotifier = null;
}
UnmodifiableListView<FieldPB> get unmodifiableFields =>
UnmodifiableListView(_fieldNotifier?.fields ?? []);
List<FieldPB> get fields => [..._fieldNotifier?.fields ?? []];
set fields(List<FieldPB> fields) {
_fieldNotifier?.fields = [...fields];
}
void addListener({
FieldsCallback? onFields,
FieldChangesetCallback? onChangeset,
bool Function()? listenWhen,
}) {
if (onChangeset != null) {
fn(c) {
if (listenWhen != null && listenWhen() == false) {
return;
}
onChangeset(c);
}
_changesetCallbackMap[onChangeset] = fn;
}
if (onFields != null) {
fn() {
if (listenWhen != null && listenWhen() == false) {
return;
}
onFields(fields);
}
_fieldsCallbackMap[onFields] = fn;
_fieldNotifier?.addListener(fn);
}
}
void removeListener({
FieldsCallback? onFieldsListener,
FieldChangesetCallback? onChangesetListener,
}) {
if (onFieldsListener != null) {
final fn = _fieldsCallbackMap.remove(onFieldsListener);
if (fn != null) {
_fieldNotifier?.removeListener(fn);
}
}
if (onChangesetListener != null) {
_changesetCallbackMap.remove(onChangesetListener);
}
}
void _deleteFields(List<FieldIdPB> deletedFields) {
if (deletedFields.isEmpty) {
return;
}
final List<FieldPB> newFields = fields;
final Map<String, FieldIdPB> deletedFieldMap = {
for (var fieldOrder in deletedFields) fieldOrder.fieldId: fieldOrder
};
newFields.retainWhere((field) => (deletedFieldMap[field.id] == null));
_fieldNotifier?.fields = newFields;
}
void _insertFields(List<IndexFieldPB> insertedFields) {
if (insertedFields.isEmpty) {
return;
}
final List<FieldPB> newFields = fields;
for (final indexField in insertedFields) {
if (newFields.length > indexField.index) {
newFields.insert(indexField.index, indexField.field_1);
} else {
newFields.add(indexField.field_1);
}
}
_fieldNotifier?.fields = newFields;
}
void _updateFields(List<FieldPB> updatedFields) {
if (updatedFields.isEmpty) {
return;
}
final List<FieldPB> newFields = fields;
for (final updatedField in updatedFields) {
final index =
newFields.indexWhere((field) => field.id == updatedField.id);
if (index != -1) {
newFields.removeAt(index);
newFields.insert(index, updatedField);
}
}
_fieldNotifier?.fields = newFields;
}
}
class GridRowFieldNotifierImpl extends IGridRowFieldNotifier {
final GridFieldCache _cache;
FieldChangesetCallback? _onChangesetFn;
FieldsCallback? _onFieldFn;
GridRowFieldNotifierImpl(GridFieldCache cache) : _cache = cache;
@override
UnmodifiableListView<FieldPB> get fields => _cache.unmodifiableFields;
@override
void onRowFieldsChanged(VoidCallback callback) {
_onFieldFn = (_) => callback();
_cache.addListener(onFields: _onFieldFn);
}
@override
void onRowFieldChanged(void Function(FieldPB) callback) {
_onChangesetFn = (FieldChangesetPB changeset) {
for (final updatedField in changeset.updatedFields) {
callback(updatedField);
}
};
_cache.addListener(onChangeset: _onChangesetFn);
}
@override
void onRowDispose() {
if (_onFieldFn != null) {
_cache.removeListener(onFieldsListener: _onFieldFn!);
_onFieldFn = null;
}
if (_onChangesetFn != null) {
_cache.removeListener(onChangesetListener: _onChangesetFn!);
_onChangesetFn = null;
}
}
}

View File

@ -63,7 +63,7 @@ class FieldCellBloc extends Bloc<FieldCellEvent, FieldCellState> {
@freezed
class FieldCellEvent with _$FieldCellEvent {
const factory FieldCellEvent.initial() = _InitialCell;
const factory FieldCellEvent.didReceiveFieldUpdate(GridFieldPB field) =
const factory FieldCellEvent.didReceiveFieldUpdate(FieldPB field) =
_DidReceiveFieldUpdate;
const factory FieldCellEvent.startUpdateWidth(double offset) =
_StartUpdateWidth;
@ -74,7 +74,7 @@ class FieldCellEvent with _$FieldCellEvent {
class FieldCellState with _$FieldCellState {
const factory FieldCellState({
required String gridId,
required GridFieldPB field,
required FieldPB field,
required double width,
}) = _FieldCellState;

View File

@ -1,9 +1,12 @@
import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'dart:async';
import 'field_service.dart';
import 'package:dartz/dartz.dart';
import 'type_option/type_option_context.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'type_option/type_option_data_controller.dart';
part 'field_editor_bloc.freezed.dart';
class FieldEditorBloc extends Bloc<FieldEditorEvent, FieldEditorState> {
@ -13,7 +16,8 @@ class FieldEditorBloc extends Bloc<FieldEditorEvent, FieldEditorState> {
required String gridId,
required String fieldName,
required IFieldTypeOptionLoader loader,
}) : dataController = TypeOptionDataController(gridId: gridId, loader: loader),
}) : dataController =
TypeOptionDataController(gridId: gridId, loader: loader),
super(FieldEditorState.initial(gridId, fieldName)) {
on<FieldEditorEvent>(
(event, emit) async {
@ -24,13 +28,13 @@ class FieldEditorBloc extends Bloc<FieldEditorEvent, FieldEditorState> {
add(FieldEditorEvent.didReceiveFieldChanged(field));
}
});
await dataController.loadData();
await dataController.loadTypeOptionData();
},
updateName: (name) {
dataController.fieldName = name;
emit(state.copyWith(name: name));
},
didReceiveFieldChanged: (GridFieldPB field) {
didReceiveFieldChanged: (FieldPB field) {
emit(state.copyWith(field: Some(field)));
},
);
@ -48,7 +52,8 @@ class FieldEditorBloc extends Bloc<FieldEditorEvent, FieldEditorState> {
class FieldEditorEvent with _$FieldEditorEvent {
const factory FieldEditorEvent.initial() = _InitialField;
const factory FieldEditorEvent.updateName(String name) = _UpdateName;
const factory FieldEditorEvent.didReceiveFieldChanged(GridFieldPB field) = _DidReceiveFieldChanged;
const factory FieldEditorEvent.didReceiveFieldChanged(FieldPB field) =
_DidReceiveFieldChanged;
}
@freezed
@ -57,7 +62,7 @@ class FieldEditorState with _$FieldEditorState {
required String gridId,
required String errorText,
required String name,
required Option<GridFieldPB> field,
required Option<FieldPB> field,
}) = _FieldEditorState;
factory FieldEditorState.initial(

View File

@ -7,16 +7,18 @@ import 'dart:async';
import 'dart:typed_data';
import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
typedef UpdateFieldNotifiedValue = Either<GridFieldPB, FlowyError>;
typedef UpdateFieldNotifiedValue = Either<FieldPB, FlowyError>;
class SingleFieldListener {
final String fieldId;
PublishNotifier<UpdateFieldNotifiedValue>? _updateFieldNotifier = PublishNotifier();
PublishNotifier<UpdateFieldNotifiedValue>? _updateFieldNotifier =
PublishNotifier();
GridNotificationListener? _listener;
SingleFieldListener({required this.fieldId});
void start({required void Function(UpdateFieldNotifiedValue) onFieldChanged}) {
void start(
{required void Function(UpdateFieldNotifiedValue) onFieldChanged}) {
_updateFieldNotifier?.addPublishListener(onFieldChanged);
_listener = GridNotificationListener(
objectId: fieldId,
@ -31,7 +33,8 @@ class SingleFieldListener {
switch (ty) {
case GridNotification.DidUpdateField:
result.fold(
(payload) => _updateFieldNotifier?.value = left(GridFieldPB.fromBuffer(payload)),
(payload) =>
_updateFieldNotifier?.value = left(FieldPB.fromBuffer(payload)),
(error) => _updateFieldNotifier?.value = right(error),
);
break;

View File

@ -1,13 +1,10 @@
import 'package:dartz/dartz.dart';
import 'package:flowy_infra/notifier.dart';
import 'package:flowy_sdk/dispatch/dispatch.dart';
import 'package:flowy_sdk/log.dart';
import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/grid_entities.pb.dart';
import 'package:flutter/foundation.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:protobuf/protobuf.dart';
part 'field_service.freezed.dart';
/// FieldService consists of lots of event functions. We define the events in the backend(Rust),
@ -21,14 +18,13 @@ class FieldService {
FieldService({required this.gridId, required this.fieldId});
Future<Either<Unit, FlowyError>> moveField(int fromIndex, int toIndex) {
final payload = MoveItemPayloadPB.create()
final payload = MoveFieldPayloadPB.create()
..gridId = gridId
..itemId = fieldId
..ty = MoveItemTypePB.MoveField
..fieldId = fieldId
..fromIndex = fromIndex
..toIndex = toIndex;
return GridEventMoveItem(payload).send();
return GridEventMoveField(payload).send();
}
Future<Either<Unit, FlowyError>> updateField({
@ -73,7 +69,7 @@ class FieldService {
// Create the field if it does not exist. Otherwise, update the field.
static Future<Either<Unit, FlowyError>> insertField({
required String gridId,
required GridFieldPB field,
required FieldPB field,
List<int>? typeOptionData,
String? startFieldId,
}) {
@ -121,7 +117,7 @@ class FieldService {
Future<Either<FieldTypeOptionDataPB, FlowyError>> getFieldTypeOptionData({
required FieldType fieldType,
}) {
final payload = GridFieldTypeOptionIdPB.create()
final payload = FieldTypeOptionIdPB.create()
..gridId = gridId
..fieldId = fieldId
..fieldType = fieldType;
@ -138,158 +134,6 @@ class FieldService {
class GridFieldCellContext with _$GridFieldCellContext {
const factory GridFieldCellContext({
required String gridId,
required GridFieldPB field,
required FieldPB field,
}) = _GridFieldCellContext;
}
abstract class IFieldTypeOptionLoader {
String get gridId;
Future<Either<FieldTypeOptionDataPB, FlowyError>> load();
Future<Either<FieldTypeOptionDataPB, FlowyError>> switchToField(String fieldId, FieldType fieldType) {
final payload = EditFieldPayloadPB.create()
..gridId = gridId
..fieldId = fieldId
..fieldType = fieldType;
return GridEventSwitchToField(payload).send();
}
}
class NewFieldTypeOptionLoader extends IFieldTypeOptionLoader {
@override
final String gridId;
NewFieldTypeOptionLoader({
required this.gridId,
});
@override
Future<Either<FieldTypeOptionDataPB, FlowyError>> load() {
final payload = CreateFieldPayloadPB.create()
..gridId = gridId
..fieldType = FieldType.RichText;
return GridEventCreateFieldTypeOption(payload).send();
}
}
class FieldTypeOptionLoader extends IFieldTypeOptionLoader {
@override
final String gridId;
final GridFieldPB field;
FieldTypeOptionLoader({
required this.gridId,
required this.field,
});
@override
Future<Either<FieldTypeOptionDataPB, FlowyError>> load() {
final payload = GridFieldTypeOptionIdPB.create()
..gridId = gridId
..fieldId = field.id
..fieldType = field.fieldType;
return GridEventGetFieldTypeOption(payload).send();
}
}
class TypeOptionDataController {
final String gridId;
final IFieldTypeOptionLoader _loader;
late FieldTypeOptionDataPB _data;
final PublishNotifier<GridFieldPB> _fieldNotifier = PublishNotifier();
TypeOptionDataController({
required this.gridId,
required IFieldTypeOptionLoader loader,
}) : _loader = loader;
Future<Either<Unit, FlowyError>> loadData() async {
final result = await _loader.load();
return result.fold(
(data) {
data.freeze();
_data = data;
_fieldNotifier.value = data.field_2;
return left(unit);
},
(err) {
Log.error(err);
return right(err);
},
);
}
GridFieldPB get field => _data.field_2;
set field(GridFieldPB field) {
_updateData(newField: field);
}
List<int> get typeOptionData => _data.typeOptionData;
set fieldName(String name) {
_updateData(newName: name);
}
set typeOptionData(List<int> typeOptionData) {
_updateData(newTypeOptionData: typeOptionData);
}
void _updateData({String? newName, GridFieldPB? newField, List<int>? newTypeOptionData}) {
_data = _data.rebuild((rebuildData) {
if (newName != null) {
rebuildData.field_2 = rebuildData.field_2.rebuild((rebuildField) {
rebuildField.name = newName;
});
}
if (newField != null) {
rebuildData.field_2 = newField;
}
if (newTypeOptionData != null) {
rebuildData.typeOptionData = newTypeOptionData;
}
});
_fieldNotifier.value = _data.field_2;
FieldService.insertField(
gridId: gridId,
field: field,
typeOptionData: typeOptionData,
);
}
Future<void> switchToField(FieldType newFieldType) {
return _loader.switchToField(field.id, newFieldType).then((result) {
return result.fold(
(fieldTypeOptionData) {
_updateData(
newField: fieldTypeOptionData.field_2,
newTypeOptionData: fieldTypeOptionData.typeOptionData,
);
},
(err) {
Log.error(err);
},
);
});
}
void Function() addFieldListener(void Function(GridFieldPB) callback) {
listener() {
callback(field);
}
_fieldNotifier.addListener(listener);
return listener;
}
void removeFieldListener(void Function() listener) {
_fieldNotifier.removeListener(listener);
}
}

View File

@ -3,11 +3,11 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'dart:async';
import 'field_service.dart';
import 'type_option/type_option_data_controller.dart';
part 'field_type_option_edit_bloc.freezed.dart';
class FieldTypeOptionEditBloc extends Bloc<FieldTypeOptionEditEvent, FieldTypeOptionEditState> {
class FieldTypeOptionEditBloc
extends Bloc<FieldTypeOptionEditEvent, FieldTypeOptionEditState> {
final TypeOptionDataController _dataController;
void Function()? _fieldListenFn;
@ -42,16 +42,19 @@ class FieldTypeOptionEditBloc extends Bloc<FieldTypeOptionEditEvent, FieldTypeOp
@freezed
class FieldTypeOptionEditEvent with _$FieldTypeOptionEditEvent {
const factory FieldTypeOptionEditEvent.initial() = _Initial;
const factory FieldTypeOptionEditEvent.didReceiveFieldUpdated(GridFieldPB field) = _DidReceiveFieldUpdated;
const factory FieldTypeOptionEditEvent.didReceiveFieldUpdated(FieldPB field) =
_DidReceiveFieldUpdated;
}
@freezed
class FieldTypeOptionEditState with _$FieldTypeOptionEditState {
const factory FieldTypeOptionEditState({
required GridFieldPB field,
required FieldPB field,
}) = _FieldTypeOptionEditState;
factory FieldTypeOptionEditState.initial(TypeOptionDataController fieldContext) => FieldTypeOptionEditState(
factory FieldTypeOptionEditState.initial(
TypeOptionDataController fieldContext) =>
FieldTypeOptionEditState(
field: fieldContext.field,
);
}

View File

@ -7,15 +7,17 @@ import 'dart:async';
import 'dart:typed_data';
import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
typedef UpdateFieldNotifiedValue = Either<GridFieldChangesetPB, FlowyError>;
typedef UpdateFieldNotifiedValue = Either<FieldChangesetPB, FlowyError>;
class GridFieldsListener {
final String gridId;
PublishNotifier<UpdateFieldNotifiedValue>? updateFieldsNotifier = PublishNotifier();
PublishNotifier<UpdateFieldNotifiedValue>? updateFieldsNotifier =
PublishNotifier();
GridNotificationListener? _listener;
GridFieldsListener({required this.gridId});
void start({required void Function(UpdateFieldNotifiedValue) onFieldsChanged}) {
void start(
{required void Function(UpdateFieldNotifiedValue) onFieldsChanged}) {
updateFieldsNotifier?.addPublishListener(onFieldsChanged);
_listener = GridNotificationListener(
objectId: gridId,
@ -27,7 +29,8 @@ class GridFieldsListener {
switch (ty) {
case GridNotification.DidUpdateGridField:
result.fold(
(payload) => updateFieldsNotifier?.value = left(GridFieldChangesetPB.fromBuffer(payload)),
(payload) => updateFieldsNotifier?.value =
left(FieldChangesetPB.fromBuffer(payload)),
(error) => updateFieldsNotifier?.value = right(error),
);
break;

View File

@ -1,21 +1,13 @@
import 'package:app_flowy/plugins/grid/application/field/type_option/type_option_service.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/date_type_option.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/date_type_option_entities.pb.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'dart:async';
import 'package:protobuf/protobuf.dart';
import 'type_option_context.dart';
part 'date_bloc.freezed.dart';
typedef DateTypeOptionContext = TypeOptionWidgetContext<DateTypeOption>;
class DateTypeOptionDataParser extends TypeOptionDataParser<DateTypeOption> {
@override
DateTypeOption fromBuffer(List<int> buffer) {
return DateTypeOption.fromBuffer(buffer);
}
}
class DateTypeOptionBloc
extends Bloc<DateTypeOptionEvent, DateTypeOptionState> {
DateTypeOptionBloc({required DateTypeOptionContext typeOptionContext})
@ -40,7 +32,7 @@ class DateTypeOptionBloc
);
}
DateTypeOption _updateTypeOption({
DateTypeOptionPB _updateTypeOption({
DateFormat? dateFormat,
TimeFormat? timeFormat,
bool? includeTime,
@ -80,9 +72,9 @@ class DateTypeOptionEvent with _$DateTypeOptionEvent {
@freezed
class DateTypeOptionState with _$DateTypeOptionState {
const factory DateTypeOptionState({
required DateTypeOption typeOption,
required DateTypeOptionPB typeOption,
}) = _DateTypeOptionState;
factory DateTypeOptionState.initial(DateTypeOption typeOption) =>
factory DateTypeOptionState.initial(DateTypeOptionPB typeOption) =>
DateTypeOptionState(typeOption: typeOption);
}

View File

@ -1,25 +1,32 @@
import 'package:app_flowy/plugins/grid/application/field/field_service.dart';
import 'package:flowy_sdk/log.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/multi_select_type_option.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/select_option.pb.dart';
import 'dart:async';
import 'package:protobuf/protobuf.dart';
import 'select_option_type_option_bloc.dart';
import 'type_option_context.dart';
import 'type_option_service.dart';
import 'package:protobuf/protobuf.dart';
class MultiSelectTypeOptionContext
extends TypeOptionWidgetContext<MultiSelectTypeOption>
with SelectOptionTypeOptionAction {
final TypeOptionService service;
class MultiSelectAction with ISelectOptionAction {
final String gridId;
final String fieldId;
final TypeOptionFFIService service;
final MultiSelectTypeOptionContext typeOptionContext;
MultiSelectTypeOptionContext({
required MultiSelectTypeOptionWidgetDataParser dataBuilder,
required TypeOptionDataController dataController,
}) : service = TypeOptionService(
gridId: dataController.gridId,
fieldId: dataController.field.id,
),
super(dataParser: dataBuilder, dataController: dataController);
MultiSelectAction({
required this.gridId,
required this.fieldId,
required this.typeOptionContext,
}) : service = TypeOptionFFIService(
gridId: gridId,
fieldId: fieldId,
);
MultiSelectTypeOptionPB get typeOption => typeOptionContext.typeOption;
set typeOption(MultiSelectTypeOptionPB newTypeOption) {
typeOptionContext.typeOption = newTypeOption;
}
@override
List<SelectOptionPB> Function(SelectOptionPB) get deleteOption {
@ -59,7 +66,7 @@ class MultiSelectTypeOptionContext
}
@override
List<SelectOptionPB> Function(SelectOptionPB) get udpateOption {
List<SelectOptionPB> Function(SelectOptionPB) get updateOption {
return (SelectOptionPB option) {
typeOption.freeze();
typeOption = typeOption.rebuild((typeOption) {
@ -73,11 +80,3 @@ class MultiSelectTypeOptionContext
};
}
}
class MultiSelectTypeOptionWidgetDataParser
extends TypeOptionDataParser<MultiSelectTypeOption> {
@override
MultiSelectTypeOption fromBuffer(List<int> buffer) {
return MultiSelectTypeOption.fromBuffer(buffer);
}
}

View File

@ -1,23 +1,13 @@
import 'package:app_flowy/plugins/grid/application/field/type_option/type_option_service.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/format.pbenum.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/number_type_option.pb.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'dart:async';
import 'package:protobuf/protobuf.dart';
import 'type_option_context.dart';
part 'number_bloc.freezed.dart';
typedef NumberTypeOptionContext = TypeOptionWidgetContext<NumberTypeOption>;
class NumberTypeOptionWidgetDataParser
extends TypeOptionDataParser<NumberTypeOption> {
@override
NumberTypeOption fromBuffer(List<int> buffer) {
return NumberTypeOption.fromBuffer(buffer);
}
}
class NumberTypeOptionBloc
extends Bloc<NumberTypeOptionEvent, NumberTypeOptionState> {
NumberTypeOptionBloc({required NumberTypeOptionContext typeOptionContext})
@ -33,7 +23,7 @@ class NumberTypeOptionBloc
);
}
NumberTypeOption _updateNumberFormat(NumberFormat format) {
NumberTypeOptionPB _updateNumberFormat(NumberFormat format) {
state.typeOption.freeze();
return state.typeOption.rebuild((typeOption) {
typeOption.format = format;
@ -55,10 +45,10 @@ class NumberTypeOptionEvent with _$NumberTypeOptionEvent {
@freezed
class NumberTypeOptionState with _$NumberTypeOptionState {
const factory NumberTypeOptionState({
required NumberTypeOption typeOption,
required NumberTypeOptionPB typeOption,
}) = _NumberTypeOptionState;
factory NumberTypeOptionState.initial(NumberTypeOption typeOption) =>
factory NumberTypeOptionState.initial(NumberTypeOptionPB typeOption) =>
NumberTypeOptionState(
typeOption: typeOption,
);

View File

@ -5,16 +5,17 @@ import 'dart:async';
import 'package:dartz/dartz.dart';
part 'select_option_type_option_bloc.freezed.dart';
abstract class SelectOptionTypeOptionAction {
abstract class ISelectOptionAction {
Future<List<SelectOptionPB>> Function(String) get insertOption;
List<SelectOptionPB> Function(SelectOptionPB) get deleteOption;
List<SelectOptionPB> Function(SelectOptionPB) get udpateOption;
List<SelectOptionPB> Function(SelectOptionPB) get updateOption;
}
class SelectOptionTypeOptionBloc extends Bloc<SelectOptionTypeOptionEvent, SelectOptionTypeOptionState> {
final SelectOptionTypeOptionAction typeOptionAction;
class SelectOptionTypeOptionBloc
extends Bloc<SelectOptionTypeOptionEvent, SelectOptionTypeOptionState> {
final ISelectOptionAction typeOptionAction;
SelectOptionTypeOptionBloc({
required List<SelectOptionPB> options,
@ -24,7 +25,8 @@ class SelectOptionTypeOptionBloc extends Bloc<SelectOptionTypeOptionEvent, Selec
(event, emit) async {
await event.when(
createOption: (optionName) async {
final List<SelectOptionPB> options = await typeOptionAction.insertOption(optionName);
final List<SelectOptionPB> options =
await typeOptionAction.insertOption(optionName);
emit(state.copyWith(options: options));
},
addingOption: () {
@ -34,11 +36,13 @@ class SelectOptionTypeOptionBloc extends Bloc<SelectOptionTypeOptionEvent, Selec
emit(state.copyWith(isEditingOption: false, newOptionName: none()));
},
updateOption: (option) {
final List<SelectOptionPB> options = typeOptionAction.udpateOption(option);
final List<SelectOptionPB> options =
typeOptionAction.updateOption(option);
emit(state.copyWith(options: options));
},
deleteOption: (option) {
final List<SelectOptionPB> options = typeOptionAction.deleteOption(option);
final List<SelectOptionPB> options =
typeOptionAction.deleteOption(option);
emit(state.copyWith(options: options));
},
);
@ -54,11 +58,15 @@ class SelectOptionTypeOptionBloc extends Bloc<SelectOptionTypeOptionEvent, Selec
@freezed
class SelectOptionTypeOptionEvent with _$SelectOptionTypeOptionEvent {
const factory SelectOptionTypeOptionEvent.createOption(String optionName) = _CreateOption;
const factory SelectOptionTypeOptionEvent.createOption(String optionName) =
_CreateOption;
const factory SelectOptionTypeOptionEvent.addingOption() = _AddingOption;
const factory SelectOptionTypeOptionEvent.endAddingOption() = _EndAddingOption;
const factory SelectOptionTypeOptionEvent.updateOption(SelectOptionPB option) = _UpdateOption;
const factory SelectOptionTypeOptionEvent.deleteOption(SelectOptionPB option) = _DeleteOption;
const factory SelectOptionTypeOptionEvent.endAddingOption() =
_EndAddingOption;
const factory SelectOptionTypeOptionEvent.updateOption(
SelectOptionPB option) = _UpdateOption;
const factory SelectOptionTypeOptionEvent.deleteOption(
SelectOptionPB option) = _DeleteOption;
}
@freezed
@ -67,9 +75,10 @@ class SelectOptionTypeOptionState with _$SelectOptionTypeOptionState {
required List<SelectOptionPB> options,
required bool isEditingOption,
required Option<String> newOptionName,
}) = _SelectOptionTyepOptionState;
}) = _SelectOptionTypeOptionState;
factory SelectOptionTypeOptionState.initial(List<SelectOptionPB> options) => SelectOptionTypeOptionState(
factory SelectOptionTypeOptionState.initial(List<SelectOptionPB> options) =>
SelectOptionTypeOptionState(
options: options,
isEditingOption: false,
newOptionName: none(),

View File

@ -1,25 +1,29 @@
import 'package:app_flowy/plugins/grid/application/field/field_service.dart';
import 'package:flowy_sdk/log.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/select_option.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/single_select_type_option.pb.dart';
import 'dart:async';
import 'package:protobuf/protobuf.dart';
import 'select_option_type_option_bloc.dart';
import 'type_option_context.dart';
import 'type_option_service.dart';
class SingleSelectTypeOptionContext
extends TypeOptionWidgetContext<SingleSelectTypeOptionPB>
with SelectOptionTypeOptionAction {
final TypeOptionService service;
class SingleSelectAction with ISelectOptionAction {
final String gridId;
final String fieldId;
final SingleSelectTypeOptionContext typeOptionContext;
final TypeOptionFFIService service;
SingleSelectTypeOptionContext({
required SingleSelectTypeOptionWidgetDataParser dataBuilder,
required TypeOptionDataController fieldContext,
}) : service = TypeOptionService(
gridId: fieldContext.gridId,
fieldId: fieldContext.field.id,
),
super(dataParser: dataBuilder, dataController: fieldContext);
SingleSelectAction({
required this.gridId,
required this.fieldId,
required this.typeOptionContext,
}) : service = TypeOptionFFIService(gridId: gridId, fieldId: fieldId);
SingleSelectTypeOptionPB get typeOption => typeOptionContext.typeOption;
set typeOption(SingleSelectTypeOptionPB newTypeOption) {
typeOptionContext.typeOption = newTypeOption;
}
@override
List<SelectOptionPB> Function(SelectOptionPB) get deleteOption {
@ -59,7 +63,7 @@ class SingleSelectTypeOptionContext
}
@override
List<SelectOptionPB> Function(SelectOptionPB) get udpateOption {
List<SelectOptionPB> Function(SelectOptionPB) get updateOption {
return (SelectOptionPB option) {
typeOption.freeze();
typeOption = typeOption.rebuild((typeOption) {
@ -73,11 +77,3 @@ class SingleSelectTypeOptionContext
};
}
}
class SingleSelectTypeOptionWidgetDataParser
extends TypeOptionDataParser<SingleSelectTypeOptionPB> {
@override
SingleSelectTypeOptionPB fromBuffer(List<int> buffer) {
return SingleSelectTypeOptionPB.fromBuffer(buffer);
}
}

View File

@ -0,0 +1,195 @@
import 'package:flowy_sdk/dispatch/dispatch.dart';
import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/checkbox_type_option.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/date_type_option.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
import 'package:dartz/dartz.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/multi_select_type_option.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/number_type_option.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/single_select_type_option.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/text_type_option.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/url_type_option.pb.dart';
import 'package:protobuf/protobuf.dart';
import 'type_option_data_controller.dart';
abstract class TypeOptionDataParser<T> {
T fromBuffer(List<int> buffer);
}
// Number
typedef NumberTypeOptionContext = TypeOptionContext<NumberTypeOptionPB>;
class NumberTypeOptionWidgetDataParser
extends TypeOptionDataParser<NumberTypeOptionPB> {
@override
NumberTypeOptionPB fromBuffer(List<int> buffer) {
return NumberTypeOptionPB.fromBuffer(buffer);
}
}
// RichText
typedef RichTextTypeOptionContext = TypeOptionContext<RichTextTypeOptionPB>;
class RichTextTypeOptionWidgetDataParser
extends TypeOptionDataParser<RichTextTypeOptionPB> {
@override
RichTextTypeOptionPB fromBuffer(List<int> buffer) {
return RichTextTypeOptionPB.fromBuffer(buffer);
}
}
// Checkbox
typedef CheckboxTypeOptionContext = TypeOptionContext<CheckboxTypeOptionPB>;
class CheckboxTypeOptionWidgetDataParser
extends TypeOptionDataParser<CheckboxTypeOptionPB> {
@override
CheckboxTypeOptionPB fromBuffer(List<int> buffer) {
return CheckboxTypeOptionPB.fromBuffer(buffer);
}
}
// URL
typedef URLTypeOptionContext = TypeOptionContext<URLTypeOptionPB>;
class URLTypeOptionWidgetDataParser
extends TypeOptionDataParser<URLTypeOptionPB> {
@override
URLTypeOptionPB fromBuffer(List<int> buffer) {
return URLTypeOptionPB.fromBuffer(buffer);
}
}
// Date
typedef DateTypeOptionContext = TypeOptionContext<DateTypeOptionPB>;
class DateTypeOptionDataParser extends TypeOptionDataParser<DateTypeOptionPB> {
@override
DateTypeOptionPB fromBuffer(List<int> buffer) {
return DateTypeOptionPB.fromBuffer(buffer);
}
}
// SingleSelect
typedef SingleSelectTypeOptionContext
= TypeOptionContext<SingleSelectTypeOptionPB>;
class SingleSelectTypeOptionWidgetDataParser
extends TypeOptionDataParser<SingleSelectTypeOptionPB> {
@override
SingleSelectTypeOptionPB fromBuffer(List<int> buffer) {
return SingleSelectTypeOptionPB.fromBuffer(buffer);
}
}
// Multi-select
typedef MultiSelectTypeOptionContext
= TypeOptionContext<MultiSelectTypeOptionPB>;
class MultiSelectTypeOptionWidgetDataParser
extends TypeOptionDataParser<MultiSelectTypeOptionPB> {
@override
MultiSelectTypeOptionPB fromBuffer(List<int> buffer) {
return MultiSelectTypeOptionPB.fromBuffer(buffer);
}
}
class TypeOptionContext<T extends GeneratedMessage> {
T? _typeOptionObject;
final TypeOptionDataParser<T> dataParser;
final TypeOptionDataController _dataController;
TypeOptionContext({
required this.dataParser,
required TypeOptionDataController dataController,
}) : _dataController = dataController;
String get gridId => _dataController.gridId;
String get fieldId => _dataController.field.id;
Future<void> loadTypeOptionData({
required void Function(T) onCompleted,
required void Function(FlowyError) onError,
}) async {
await _dataController.loadTypeOptionData().then((result) {
result.fold((l) => null, (err) => onError(err));
});
onCompleted(typeOption);
}
T get typeOption {
if (_typeOptionObject != null) {
return _typeOptionObject!;
}
final T object = _dataController.getTypeOption(dataParser);
_typeOptionObject = object;
return object;
}
set typeOption(T typeOption) {
_dataController.typeOptionData = typeOption.writeToBuffer();
_typeOptionObject = typeOption;
}
}
abstract class TypeOptionFieldDelegate {
void onFieldChanged(void Function(String) callback);
void dispose();
}
abstract class IFieldTypeOptionLoader {
String get gridId;
Future<Either<FieldTypeOptionDataPB, FlowyError>> load();
Future<Either<FieldTypeOptionDataPB, FlowyError>> switchToField(
String fieldId, FieldType fieldType) {
final payload = EditFieldPayloadPB.create()
..gridId = gridId
..fieldId = fieldId
..fieldType = fieldType;
return GridEventSwitchToField(payload).send();
}
}
class NewFieldTypeOptionLoader extends IFieldTypeOptionLoader {
@override
final String gridId;
NewFieldTypeOptionLoader({
required this.gridId,
});
@override
Future<Either<FieldTypeOptionDataPB, FlowyError>> load() {
final payload = CreateFieldPayloadPB.create()
..gridId = gridId
..fieldType = FieldType.RichText;
return GridEventCreateFieldTypeOption(payload).send();
}
}
class FieldTypeOptionLoader extends IFieldTypeOptionLoader {
@override
final String gridId;
final FieldPB field;
FieldTypeOptionLoader({
required this.gridId,
required this.field,
});
@override
Future<Either<FieldTypeOptionDataPB, FlowyError>> load() {
final payload = FieldTypeOptionIdPB.create()
..gridId = gridId
..fieldId = field.id
..fieldType = field.fieldType;
return GridEventGetFieldTypeOption(payload).send();
}
}

View File

@ -0,0 +1,123 @@
import 'package:flowy_infra/notifier.dart';
import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
import 'package:app_flowy/plugins/grid/application/field/field_service.dart';
import 'package:dartz/dartz.dart';
import 'package:protobuf/protobuf.dart';
import 'package:flowy_sdk/log.dart';
import 'type_option_context.dart';
class TypeOptionDataController {
final String gridId;
final IFieldTypeOptionLoader loader;
late FieldTypeOptionDataPB _data;
final PublishNotifier<FieldPB> _fieldNotifier = PublishNotifier();
TypeOptionDataController({
required this.gridId,
required this.loader,
FieldPB? field,
}) {
if (field != null) {
_data = FieldTypeOptionDataPB.create()
..gridId = gridId
..field_2 = field;
}
}
Future<Either<Unit, FlowyError>> loadTypeOptionData() async {
final result = await loader.load();
return result.fold(
(data) {
data.freeze();
_data = data;
_fieldNotifier.value = data.field_2;
return left(unit);
},
(err) {
Log.error(err);
return right(err);
},
);
}
FieldPB get field {
return _data.field_2;
}
set field(FieldPB field) {
_updateData(newField: field);
}
T getTypeOption<T>(TypeOptionDataParser<T> parser) {
return parser.fromBuffer(_data.typeOptionData);
}
set fieldName(String name) {
_updateData(newName: name);
}
set typeOptionData(List<int> typeOptionData) {
_updateData(newTypeOptionData: typeOptionData);
}
void _updateData({
String? newName,
FieldPB? newField,
List<int>? newTypeOptionData,
}) {
_data = _data.rebuild((rebuildData) {
if (newName != null) {
rebuildData.field_2 = rebuildData.field_2.rebuild((rebuildField) {
rebuildField.name = newName;
});
}
if (newField != null) {
rebuildData.field_2 = newField;
}
if (newTypeOptionData != null) {
rebuildData.typeOptionData = newTypeOptionData;
}
});
_fieldNotifier.value = _data.field_2;
FieldService.insertField(
gridId: gridId,
field: field,
typeOptionData: _data.typeOptionData,
);
}
Future<void> switchToField(FieldType newFieldType) {
return loader.switchToField(field.id, newFieldType).then((result) {
return result.fold(
(fieldTypeOptionData) {
_updateData(
newField: fieldTypeOptionData.field_2,
newTypeOptionData: fieldTypeOptionData.typeOptionData,
);
},
(err) {
Log.error(err);
},
);
});
}
void Function() addFieldListener(void Function(FieldPB) callback) {
listener() {
callback(field);
}
_fieldNotifier.addListener(listener);
return listener;
}
void removeFieldListener(void Function() listener) {
_fieldNotifier.removeListener(listener);
}
}

View File

@ -1,19 +1,14 @@
import 'dart:typed_data';
import 'package:app_flowy/plugins/grid/application/field/field_service.dart';
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-grid/cell_entities.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/select_option.pb.dart';
import 'package:protobuf/protobuf.dart';
class TypeOptionService {
class TypeOptionFFIService {
final String gridId;
final String fieldId;
TypeOptionService({
TypeOptionFFIService({
required this.gridId,
required this.fieldId,
});
@ -29,79 +24,3 @@ class TypeOptionService {
return GridEventNewSelectOption(payload).send();
}
}
abstract class TypeOptionDataParser<T> {
T fromBuffer(List<int> buffer);
}
class TypeOptionWidgetContext<T extends GeneratedMessage> {
T? _typeOptionObject;
final TypeOptionDataController _dataController;
final TypeOptionDataParser<T> dataParser;
TypeOptionWidgetContext({
required this.dataParser,
required TypeOptionDataController dataController,
}) : _dataController = dataController;
String get gridId => _dataController.gridId;
GridFieldPB get field => _dataController.field;
T get typeOption {
if (_typeOptionObject != null) {
return _typeOptionObject!;
}
final T object = dataParser.fromBuffer(_dataController.typeOptionData);
_typeOptionObject = object;
return object;
}
set typeOption(T typeOption) {
_dataController.typeOptionData = typeOption.writeToBuffer();
_typeOptionObject = typeOption;
}
}
abstract class TypeOptionFieldDelegate {
void onFieldChanged(void Function(String) callback);
void dispose();
}
class TypeOptionContext2<T> {
final String gridId;
final GridFieldPB field;
final FieldService _fieldService;
T? _data;
final TypeOptionDataParser dataBuilder;
TypeOptionContext2({
required this.gridId,
required this.field,
required this.dataBuilder,
Uint8List? data,
}) : _fieldService = FieldService(gridId: gridId, fieldId: field.id) {
if (data != null) {
_data = dataBuilder.fromBuffer(data);
}
}
Future<Either<T, FlowyError>> typeOptionData() {
if (_data != null) {
return Future(() => left(_data!));
}
return _fieldService
.getFieldTypeOptionData(fieldType: field.fieldType)
.then((result) {
return result.fold(
(data) {
_data = dataBuilder.fromBuffer(data.typeOptionData);
return left(_data!);
},
(err) => right(err),
);
});
}
}

View File

@ -1,40 +1,23 @@
import 'dart:async';
import 'package:dartz/dartz.dart';
import 'package:equatable/equatable.dart';
import 'package:flowy_sdk/log.dart';
import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/protobuf.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'block/block_cache.dart';
import 'grid_service.dart';
import 'row/row_service.dart';
import 'grid_data_controller.dart';
import 'row/row_cache.dart';
import 'dart:collection';
part 'grid_bloc.freezed.dart';
class GridBloc extends Bloc<GridEvent, GridState> {
final String gridId;
final GridService _gridService;
final GridFieldCache fieldCache;
// key: the block id
final LinkedHashMap<String, GridBlockCache> _blocks;
List<GridRowInfo> get rowInfos {
final List<GridRowInfo> rows = [];
for (var block in _blocks.values) {
rows.addAll(block.rows);
}
return rows;
}
final GridDataController dataController;
GridBloc({required ViewPB view})
: gridId = view.id,
_blocks = LinkedHashMap.identity(),
_gridService = GridService(gridId: view.id),
fieldCache = GridFieldCache(gridId: view.id),
: dataController = GridDataController(view: view),
super(GridState.initial(view.id)) {
on<GridEvent>(
(event, emit) async {
@ -44,13 +27,21 @@ class GridBloc extends Bloc<GridEvent, GridState> {
await _loadGrid(emit);
},
createRow: () {
_gridService.createRow();
dataController.createRow();
},
didReceiveRowUpdate: (newRowInfos, reason) {
emit(state.copyWith(rowInfos: newRowInfos, reason: reason));
didReceiveGridUpdate: (grid) {
emit(state.copyWith(grid: Some(grid)));
},
didReceiveFieldUpdate: (fields) {
emit(state.copyWith(rowInfos: rowInfos, fields: GridFieldEquatable(fields)));
emit(state.copyWith(
fields: GridFieldEquatable(fields),
));
},
didReceiveRowUpdate: (newRowInfos, reason) {
emit(state.copyWith(
rowInfos: newRowInfos,
reason: reason,
));
},
);
},
@ -59,89 +50,63 @@ class GridBloc extends Bloc<GridEvent, GridState> {
@override
Future<void> close() async {
await _gridService.closeGrid();
await fieldCache.dispose();
for (final blockCache in _blocks.values) {
blockCache.dispose();
}
await dataController.dispose();
return super.close();
}
GridRowCache? getRowCache(String blockId, String rowId) {
final GridBlockCache? blockCache = _blocks[blockId];
final GridBlockCache? blockCache = dataController.blocks[blockId];
return blockCache?.rowCache;
}
void _startListening() {
fieldCache.addListener(
listenWhen: () => !isClosed,
onFields: (fields) => add(GridEvent.didReceiveFieldUpdate(fields)),
dataController.addListener(
onGridChanged: (grid) {
if (!isClosed) {
add(GridEvent.didReceiveGridUpdate(grid));
}
},
onRowsChanged: (rowInfos, reason) {
if (!isClosed) {
add(GridEvent.didReceiveRowUpdate(rowInfos, reason));
}
},
onFieldsChanged: (fields) {
if (!isClosed) {
add(GridEvent.didReceiveFieldUpdate(fields));
}
},
);
}
Future<void> _loadGrid(Emitter<GridState> emit) async {
final result = await _gridService.loadGrid();
return Future(
() => result.fold(
(grid) async {
_initialBlocks(grid.blocks);
await _loadFields(grid, emit);
},
(err) => emit(state.copyWith(loadingState: GridLoadingState.finish(right(err)))),
final result = await dataController.loadData();
result.fold(
(grid) => emit(
state.copyWith(loadingState: GridLoadingState.finish(left(unit))),
),
(err) => emit(
state.copyWith(loadingState: GridLoadingState.finish(right(err))),
),
);
}
Future<void> _loadFields(GridPB grid, Emitter<GridState> emit) async {
final result = await _gridService.getFields(fieldIds: grid.fields);
return Future(
() => result.fold(
(fields) {
fieldCache.fields = fields.items;
emit(state.copyWith(
grid: Some(grid),
fields: GridFieldEquatable(fieldCache.fields),
rowInfos: rowInfos,
loadingState: GridLoadingState.finish(left(unit)),
));
},
(err) => emit(state.copyWith(loadingState: GridLoadingState.finish(right(err)))),
),
);
}
void _initialBlocks(List<GridBlockPB> blocks) {
for (final block in blocks) {
if (_blocks[block.id] != null) {
Log.warn("Intial duplicate block's cache: ${block.id}");
return;
}
final cache = GridBlockCache(
gridId: gridId,
block: block,
fieldCache: fieldCache,
);
cache.addListener(
listenWhen: () => !isClosed,
onChangeReason: (reason) => add(GridEvent.didReceiveRowUpdate(rowInfos, reason)),
);
_blocks[block.id] = cache;
}
}
}
@freezed
class GridEvent with _$GridEvent {
const factory GridEvent.initial() = InitialGrid;
const factory GridEvent.createRow() = _CreateRow;
const factory GridEvent.didReceiveRowUpdate(List<GridRowInfo> rows, GridRowChangeReason listState) =
_DidReceiveRowUpdate;
const factory GridEvent.didReceiveFieldUpdate(List<GridFieldPB> fields) = _DidReceiveFieldUpdate;
const factory GridEvent.didReceiveRowUpdate(
List<RowInfo> rows,
RowsChangedReason listState,
) = _DidReceiveRowUpdate;
const factory GridEvent.didReceiveFieldUpdate(
UnmodifiableListView<FieldPB> fields,
) = _DidReceiveFieldUpdate;
const factory GridEvent.didReceiveGridUpdate(
GridPB grid,
) = _DidReceiveGridUpdate;
}
@freezed
@ -150,13 +115,13 @@ class GridState with _$GridState {
required String gridId,
required Option<GridPB> grid,
required GridFieldEquatable fields,
required List<GridRowInfo> rowInfos,
required List<RowInfo> rowInfos,
required GridLoadingState loadingState,
required GridRowChangeReason reason,
required RowsChangedReason reason,
}) = _GridState;
factory GridState.initial(String gridId) => GridState(
fields: const GridFieldEquatable([]),
fields: GridFieldEquatable(UnmodifiableListView([])),
rowInfos: [],
grid: none(),
gridId: gridId,
@ -168,20 +133,29 @@ class GridState with _$GridState {
@freezed
class GridLoadingState with _$GridLoadingState {
const factory GridLoadingState.loading() = _Loading;
const factory GridLoadingState.finish(Either<Unit, FlowyError> successOrFail) = _Finish;
const factory GridLoadingState.finish(
Either<Unit, FlowyError> successOrFail) = _Finish;
}
class GridFieldEquatable extends Equatable {
final List<GridFieldPB> _fields;
const GridFieldEquatable(List<GridFieldPB> fields) : _fields = fields;
final UnmodifiableListView<FieldPB> _fields;
const GridFieldEquatable(
UnmodifiableListView<FieldPB> fields,
) : _fields = fields;
@override
List<Object?> get props {
if (_fields.isEmpty) {
return [];
}
return [
_fields.length,
_fields.map((field) => field.width).reduce((value, element) => value + element),
_fields
.map((field) => field.width)
.reduce((value, element) => value + element),
];
}
UnmodifiableListView<GridFieldPB> get value => UnmodifiableListView(_fields);
UnmodifiableListView<FieldPB> get value => UnmodifiableListView(_fields);
}

View File

@ -0,0 +1,130 @@
import 'dart:collection';
import 'package:flowy_sdk/log.dart';
import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/block_entities.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/grid_entities.pb.dart';
import 'dart:async';
import 'package:dartz/dartz.dart';
import 'block/block_cache.dart';
import 'field/field_cache.dart';
import 'prelude.dart';
import 'row/row_cache.dart';
typedef OnFieldsChanged = void Function(UnmodifiableListView<FieldPB>);
typedef OnGridChanged = void Function(GridPB);
typedef OnRowsChanged = void Function(
List<RowInfo> rowInfos,
RowsChangedReason,
);
typedef ListenOnRowChangedCondition = bool Function();
class GridDataController {
final String gridId;
final GridFFIService _gridFFIService;
final GridFieldCache fieldCache;
// key: the block id
final LinkedHashMap<String, GridBlockCache> _blocks;
UnmodifiableMapView<String, GridBlockCache> get blocks =>
UnmodifiableMapView(_blocks);
OnRowsChanged? _onRowChanged;
OnFieldsChanged? _onFieldsChanged;
OnGridChanged? _onGridChanged;
List<RowInfo> get rowInfos {
final List<RowInfo> rows = [];
for (var block in _blocks.values) {
rows.addAll(block.rows);
}
return rows;
}
GridDataController({required ViewPB view})
: gridId = view.id,
_blocks = LinkedHashMap.new(),
_gridFFIService = GridFFIService(gridId: view.id),
fieldCache = GridFieldCache(gridId: view.id);
void addListener({
required OnGridChanged onGridChanged,
required OnRowsChanged onRowsChanged,
required OnFieldsChanged onFieldsChanged,
}) {
_onGridChanged = onGridChanged;
_onRowChanged = onRowsChanged;
_onFieldsChanged = onFieldsChanged;
fieldCache.addListener(onFields: (fields) {
_onFieldsChanged?.call(UnmodifiableListView(fields));
});
}
Future<Either<Unit, FlowyError>> loadData() async {
final result = await _gridFFIService.loadGrid();
return Future(
() => result.fold(
(grid) async {
_initialBlocks(grid.blocks);
_onGridChanged?.call(grid);
return await _loadFields(grid);
},
(err) => right(err),
),
);
}
void createRow() {
_gridFFIService.createRow();
}
Future<void> dispose() async {
await _gridFFIService.closeGrid();
await fieldCache.dispose();
for (final blockCache in _blocks.values) {
blockCache.dispose();
}
}
void _initialBlocks(List<BlockPB> blocks) {
for (final block in blocks) {
if (_blocks[block.id] != null) {
Log.warn("Initial duplicate block's cache: ${block.id}");
return;
}
final cache = GridBlockCache(
gridId: gridId,
block: block,
fieldCache: fieldCache,
);
cache.addListener(
onRowsChanged: (reason) {
_onRowChanged?.call(rowInfos, reason);
},
);
_blocks[block.id] = cache;
}
}
Future<Either<Unit, FlowyError>> _loadFields(GridPB grid) async {
final result = await _gridFFIService.getFields(fieldIds: grid.fields);
return Future(
() => result.fold(
(fields) {
fieldCache.fields = fields.items;
_onFieldsChanged?.call(UnmodifiableListView(fieldCache.fields));
return left(unit);
},
(err) => right(err),
),
);
}
}

View File

@ -4,7 +4,8 @@ import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'dart:async';
import 'grid_service.dart';
import 'field/field_cache.dart';
part 'grid_header_bloc.freezed.dart';
@ -35,7 +36,7 @@ class GridHeaderBloc extends Bloc<GridHeaderEvent, GridHeaderState> {
Future<void> _moveField(
_MoveField value, Emitter<GridHeaderState> emit) async {
final fields = List<GridFieldPB>.from(state.fields);
final fields = List<FieldPB>.from(state.fields);
fields.insert(value.toIndex, fields.removeAt(value.fromIndex));
emit(state.copyWith(fields: fields));
@ -63,19 +64,19 @@ class GridHeaderBloc extends Bloc<GridHeaderEvent, GridHeaderState> {
@freezed
class GridHeaderEvent with _$GridHeaderEvent {
const factory GridHeaderEvent.initial() = _InitialHeader;
const factory GridHeaderEvent.didReceiveFieldUpdate(
List<GridFieldPB> fields) = _DidReceiveFieldUpdate;
const factory GridHeaderEvent.didReceiveFieldUpdate(List<FieldPB> fields) =
_DidReceiveFieldUpdate;
const factory GridHeaderEvent.moveField(
GridFieldPB field, int fromIndex, int toIndex) = _MoveField;
FieldPB field, int fromIndex, int toIndex) = _MoveField;
}
@freezed
class GridHeaderState with _$GridHeaderState {
const factory GridHeaderState({required List<GridFieldPB> fields}) =
const factory GridHeaderState({required List<FieldPB> fields}) =
_GridHeaderState;
factory GridHeaderState.initial(List<GridFieldPB> fields) {
// final List<GridFieldPB> newFields = List.from(fields);
factory GridHeaderState.initial(List<FieldPB> fields) {
// final List<FieldPB> newFields = List.from(fields);
// newFields.retainWhere((field) => field.visibility);
return GridHeaderState(fields: fields);
}

View File

@ -1,21 +1,17 @@
import 'dart:collection';
import 'package:app_flowy/plugins/grid/application/field/grid_listener.dart';
import 'package:dartz/dartz.dart';
import 'package:flowy_sdk/dispatch/dispatch.dart';
import 'package:flowy_sdk/log.dart';
import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/block_entities.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/board_card.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/grid_entities.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/group.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/row_entities.pb.dart';
import 'package:flutter/foundation.dart';
import 'row/row_service.dart';
class GridService {
class GridFFIService {
final String gridId;
GridService({
GridFFIService({
required this.gridId,
});
@ -26,18 +22,24 @@ class GridService {
return GridEventGetGrid(payload).send();
}
Future<Either<GridRowPB, FlowyError>> createRow(
{Option<String>? startRowId}) {
CreateRowPayloadPB payload = CreateRowPayloadPB.create()..gridId = gridId;
Future<Either<RowPB, FlowyError>> createRow({Option<String>? startRowId}) {
var payload = CreateTableRowPayloadPB.create()..gridId = gridId;
startRowId?.fold(() => null, (id) => payload.startRowId = id);
return GridEventCreateRow(payload).send();
return GridEventCreateTableRow(payload).send();
}
Future<Either<RepeatedGridFieldPB, FlowyError>> getFields(
{required List<GridFieldIdPB> fieldIds}) {
Future<Either<RowPB, FlowyError>> createBoardCard(String groupId) {
CreateBoardCardPayloadPB payload = CreateBoardCardPayloadPB.create()
..gridId = gridId
..groupId = groupId;
return GridEventCreateBoardCard(payload).send();
}
Future<Either<RepeatedFieldPB, FlowyError>> getFields(
{required List<FieldIdPB> fieldIds}) {
final payload = QueryFieldPayloadPB.create()
..gridId = gridId
..fieldIds = RepeatedGridFieldIdPB(items: fieldIds);
..fieldIds = RepeatedFieldIdPB(items: fieldIds);
return GridEventGetFields(payload).send();
}
@ -45,188 +47,9 @@ class GridService {
final request = ViewIdPB(value: gridId);
return FolderEventCloseView(request).send();
}
}
class FieldsNotifier extends ChangeNotifier {
List<GridFieldPB> _fields = [];
set fields(List<GridFieldPB> fields) {
_fields = fields;
notifyListeners();
}
List<GridFieldPB> get fields => _fields;
}
typedef FieldChangesetCallback = void Function(GridFieldChangesetPB);
typedef FieldsCallback = void Function(List<GridFieldPB>);
class GridFieldCache {
final String gridId;
final GridFieldsListener _fieldListener;
FieldsNotifier? _fieldNotifier = FieldsNotifier();
final Map<FieldsCallback, VoidCallback> _fieldsCallbackMap = {};
final Map<FieldChangesetCallback, FieldChangesetCallback>
_changesetCallbackMap = {};
GridFieldCache({required this.gridId})
: _fieldListener = GridFieldsListener(gridId: gridId) {
_fieldListener.start(onFieldsChanged: (result) {
result.fold(
(changeset) {
_deleteFields(changeset.deletedFields);
_insertFields(changeset.insertedFields);
_updateFields(changeset.updatedFields);
for (final listener in _changesetCallbackMap.values) {
listener(changeset);
}
},
(err) => Log.error(err),
);
});
}
Future<void> dispose() async {
await _fieldListener.stop();
_fieldNotifier?.dispose();
_fieldNotifier = null;
}
UnmodifiableListView<GridFieldPB> get unmodifiableFields =>
UnmodifiableListView(_fieldNotifier?.fields ?? []);
List<GridFieldPB> get fields => [..._fieldNotifier?.fields ?? []];
set fields(List<GridFieldPB> fields) {
_fieldNotifier?.fields = [...fields];
}
void addListener({
FieldsCallback? onFields,
FieldChangesetCallback? onChangeset,
bool Function()? listenWhen,
}) {
if (onChangeset != null) {
fn(c) {
if (listenWhen != null && listenWhen() == false) {
return;
}
onChangeset(c);
}
_changesetCallbackMap[onChangeset] = fn;
}
if (onFields != null) {
fn() {
if (listenWhen != null && listenWhen() == false) {
return;
}
onFields(fields);
}
_fieldsCallbackMap[onFields] = fn;
_fieldNotifier?.addListener(fn);
}
}
void removeListener({
FieldsCallback? onFieldsListener,
FieldChangesetCallback? onChangesetListener,
}) {
if (onFieldsListener != null) {
final fn = _fieldsCallbackMap.remove(onFieldsListener);
if (fn != null) {
_fieldNotifier?.removeListener(fn);
}
}
if (onChangesetListener != null) {
_changesetCallbackMap.remove(onChangesetListener);
}
}
void _deleteFields(List<GridFieldIdPB> deletedFields) {
if (deletedFields.isEmpty) {
return;
}
final List<GridFieldPB> newFields = fields;
final Map<String, GridFieldIdPB> deletedFieldMap = {
for (var fieldOrder in deletedFields) fieldOrder.fieldId: fieldOrder
};
newFields.retainWhere((field) => (deletedFieldMap[field.id] == null));
_fieldNotifier?.fields = newFields;
}
void _insertFields(List<IndexFieldPB> insertedFields) {
if (insertedFields.isEmpty) {
return;
}
final List<GridFieldPB> newFields = fields;
for (final indexField in insertedFields) {
if (newFields.length > indexField.index) {
newFields.insert(indexField.index, indexField.field_1);
} else {
newFields.add(indexField.field_1);
}
}
_fieldNotifier?.fields = newFields;
}
void _updateFields(List<GridFieldPB> updatedFields) {
if (updatedFields.isEmpty) {
return;
}
final List<GridFieldPB> newFields = fields;
for (final updatedField in updatedFields) {
final index =
newFields.indexWhere((field) => field.id == updatedField.id);
if (index != -1) {
newFields.removeAt(index);
newFields.insert(index, updatedField);
}
}
_fieldNotifier?.fields = newFields;
}
}
class GridRowCacheFieldNotifierImpl extends GridRowCacheFieldNotifier {
final GridFieldCache _cache;
FieldChangesetCallback? _onChangesetFn;
FieldsCallback? _onFieldFn;
GridRowCacheFieldNotifierImpl(GridFieldCache cache) : _cache = cache;
@override
UnmodifiableListView<GridFieldPB> get fields => _cache.unmodifiableFields;
@override
void onFieldsChanged(VoidCallback callback) {
_onFieldFn = (_) => callback();
_cache.addListener(onFields: _onFieldFn);
}
@override
void onFieldChanged(void Function(GridFieldPB) callback) {
_onChangesetFn = (GridFieldChangesetPB changeset) {
for (final updatedField in changeset.updatedFields) {
callback(updatedField);
}
};
_cache.addListener(onChangeset: _onChangesetFn);
}
@override
void dispose() {
if (_onFieldFn != null) {
_cache.removeListener(onFieldsListener: _onFieldFn!);
_onFieldFn = null;
}
if (_onChangesetFn != null) {
_cache.removeListener(onChangesetListener: _onChangesetFn!);
_onChangesetFn = null;
}
Future<Either<RepeatedGridGroupPB, FlowyError>> loadGroups() {
final payload = GridIdPB(value: gridId);
return GridEventGetGroup(payload).send();
}
}

View File

@ -4,13 +4,13 @@ export 'row/row_service.dart';
export 'grid_service.dart';
export 'grid_header_bloc.dart';
// GridFieldPB
// FieldPB
export 'field/field_service.dart';
export 'field/field_action_sheet_bloc.dart';
export 'field/field_editor_bloc.dart';
export 'field/field_type_option_edit_bloc.dart';
// GridFieldPB Type Option
// FieldPB Type Option
export 'field/type_option/date_bloc.dart';
export 'field/type_option/number_bloc.dart';
export 'field/type_option/single_select_type_option.dart';

View File

@ -6,28 +6,30 @@ import 'package:freezed_annotation/freezed_annotation.dart';
import 'dart:async';
import 'package:dartz/dartz.dart';
import 'row_cache.dart';
part 'row_action_sheet_bloc.freezed.dart';
class RowActionSheetBloc
extends Bloc<RowActionSheetEvent, RowActionSheetState> {
final RowService _rowService;
final RowFFIService _rowService;
RowActionSheetBloc({required GridRowInfo rowData})
: _rowService = RowService(
gridId: rowData.gridId,
blockId: rowData.blockId,
rowId: rowData.id,
RowActionSheetBloc({required RowInfo rowInfo})
: _rowService = RowFFIService(
gridId: rowInfo.gridId,
blockId: rowInfo.rowPB.blockId,
),
super(RowActionSheetState.initial(rowData)) {
super(RowActionSheetState.initial(rowInfo)) {
on<RowActionSheetEvent>(
(event, emit) async {
await event.map(
deleteRow: (_DeleteRow value) async {
final result = await _rowService.deleteRow();
final result = await _rowService.deleteRow(state.rowData.rowPB.id);
logResult(result);
},
duplicateRow: (_DuplicateRow value) async {
final result = await _rowService.duplicateRow();
final result =
await _rowService.duplicateRow(state.rowData.rowPB.id);
logResult(result);
},
);
@ -54,11 +56,10 @@ class RowActionSheetEvent with _$RowActionSheetEvent {
@freezed
class RowActionSheetState with _$RowActionSheetState {
const factory RowActionSheetState({
required GridRowInfo rowData,
required RowInfo rowData,
}) = _RowActionSheetState;
factory RowActionSheetState.initial(GridRowInfo rowData) =>
RowActionSheetState(
factory RowActionSheetState.initial(RowInfo rowData) => RowActionSheetState(
rowData: rowData,
);
}

View File

@ -5,25 +5,25 @@ import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'dart:async';
import 'row_cache.dart';
import 'row_data_controller.dart';
import 'row_service.dart';
part 'row_bloc.freezed.dart';
class RowBloc extends Bloc<RowEvent, RowState> {
final RowService _rowService;
final GridRowCache _rowCache;
void Function()? _rowListenFn;
final RowFFIService _rowService;
final GridRowDataController _dataController;
RowBloc({
required GridRowInfo rowInfo,
required GridRowCache rowCache,
}) : _rowService = RowService(
required RowInfo rowInfo,
required GridRowDataController dataController,
}) : _rowService = RowFFIService(
gridId: rowInfo.gridId,
blockId: rowInfo.blockId,
rowId: rowInfo.id,
blockId: rowInfo.rowPB.blockId,
),
_rowCache = rowCache,
super(RowState.initial(rowInfo, rowCache.loadGridCells(rowInfo.id))) {
_dataController = dataController,
super(RowState.initial(rowInfo, dataController.loadData())) {
on<RowEvent>(
(event, emit) async {
await event.map(
@ -31,16 +31,15 @@ class RowBloc extends Bloc<RowEvent, RowState> {
await _startListening();
},
createRow: (_CreateRow value) {
_rowService.createRow();
_rowService.createRow(rowInfo.rowPB.id);
},
didReceiveCellDatas: (_DidReceiveCellDatas value) async {
final fields = value.gridCellMap.values
didReceiveCells: (_DidReceiveCells value) async {
final cells = value.gridCellMap.values
.map((e) => GridCellEquatable(e.field))
.toList();
final snapshots = UnmodifiableListView(fields);
emit(state.copyWith(
gridCellMap: value.gridCellMap,
snapshots: snapshots,
cells: UnmodifiableListView(cells),
changeReason: value.reason,
));
},
@ -51,19 +50,17 @@ class RowBloc extends Bloc<RowEvent, RowState> {
@override
Future<void> close() async {
if (_rowListenFn != null) {
_rowCache.removeRowListener(_rowListenFn!);
}
_dataController.dispose();
return super.close();
}
Future<void> _startListening() async {
_rowListenFn = _rowCache.addListener(
rowId: state.rowInfo.id,
onCellUpdated: (cellDatas, reason) =>
add(RowEvent.didReceiveCellDatas(cellDatas, reason)),
listenWhen: () => !isClosed,
_dataController.addListener(
onRowChanged: (cells, reason) {
if (!isClosed) {
add(RowEvent.didReceiveCells(cells, reason));
}
},
);
}
}
@ -72,33 +69,33 @@ class RowBloc extends Bloc<RowEvent, RowState> {
class RowEvent with _$RowEvent {
const factory RowEvent.initial() = _InitialRow;
const factory RowEvent.createRow() = _CreateRow;
const factory RowEvent.didReceiveCellDatas(
GridCellMap gridCellMap, GridRowChangeReason reason) =
_DidReceiveCellDatas;
const factory RowEvent.didReceiveCells(
GridCellMap gridCellMap, RowsChangedReason reason) = _DidReceiveCells;
}
@freezed
class RowState with _$RowState {
const factory RowState({
required GridRowInfo rowInfo,
required RowInfo rowInfo,
required GridCellMap gridCellMap,
required UnmodifiableListView<GridCellEquatable> snapshots,
GridRowChangeReason? changeReason,
required UnmodifiableListView<GridCellEquatable> cells,
RowsChangedReason? changeReason,
}) = _RowState;
factory RowState.initial(GridRowInfo rowInfo, GridCellMap cellDataMap) =>
factory RowState.initial(RowInfo rowInfo, GridCellMap cellDataMap) =>
RowState(
rowInfo: rowInfo,
gridCellMap: cellDataMap,
snapshots: UnmodifiableListView(
cellDataMap.values.map((e) => GridCellEquatable(e.field)).toList()),
cells: UnmodifiableListView(
cellDataMap.values.map((e) => GridCellEquatable(e.field)).toList(),
),
);
}
class GridCellEquatable extends Equatable {
final GridFieldPB _field;
final FieldPB _field;
const GridCellEquatable(GridFieldPB field) : _field = field;
const GridCellEquatable(FieldPB field) : _field = field;
@override
List<Object?> get props => [

View File

@ -0,0 +1,328 @@
import 'dart:collection';
import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart';
import 'package:flowy_sdk/dispatch/dispatch.dart';
import 'package:flowy_sdk/log.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/block_entities.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/row_entities.pb.dart';
import 'package:flutter/foundation.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'row_cache.freezed.dart';
typedef RowUpdateCallback = void Function();
abstract class IGridRowFieldNotifier {
UnmodifiableListView<FieldPB> get fields;
void onRowFieldsChanged(VoidCallback callback);
void onRowFieldChanged(void Function(FieldPB) callback);
void onRowDispose();
}
/// Cache the rows in memory
/// Insert / delete / update row
///
/// Read https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/architecture/frontend/grid for more information.
class GridRowCache {
final String gridId;
final BlockPB block;
/// _rows containers the current block's rows
/// Use List to reverse the order of the GridRow.
List<RowInfo> _rowInfos = [];
/// Use Map for faster access the raw row data.
final HashMap<String, RowPB> _rowByRowId;
final GridCellCache _cellCache;
final IGridRowFieldNotifier _fieldNotifier;
final _RowChangesetNotifier _rowChangeReasonNotifier;
UnmodifiableListView<RowInfo> get rows => UnmodifiableListView(_rowInfos);
GridCellCache get cellCache => _cellCache;
GridRowCache({
required this.gridId,
required this.block,
required IGridRowFieldNotifier notifier,
}) : _cellCache = GridCellCache(gridId: gridId),
_rowByRowId = HashMap(),
_rowChangeReasonNotifier = _RowChangesetNotifier(),
_fieldNotifier = notifier {
//
notifier.onRowFieldsChanged(() => _rowChangeReasonNotifier
.receive(const RowsChangedReason.fieldDidChange()));
notifier.onRowFieldChanged((field) => _cellCache.remove(field.id));
_rowInfos = block.rows.map((rowPB) => buildGridRow(rowPB)).toList();
}
Future<void> dispose() async {
_fieldNotifier.onRowDispose();
_rowChangeReasonNotifier.dispose();
await _cellCache.dispose();
}
void applyChangesets(List<GridBlockChangesetPB> changesets) {
for (final changeset in changesets) {
_deleteRows(changeset.deletedRows);
_insertRows(changeset.insertedRows);
_updateRows(changeset.updatedRows);
_hideRows(changeset.hideRows);
_showRows(changeset.visibleRows);
}
}
void _deleteRows(List<String> deletedRows) {
if (deletedRows.isEmpty) {
return;
}
final List<RowInfo> newRows = [];
final DeletedIndexs deletedIndex = [];
final Map<String, String> deletedRowByRowId = {
for (var rowId in deletedRows) rowId: rowId
};
_rowInfos.asMap().forEach((index, RowInfo rowInfo) {
if (deletedRowByRowId[rowInfo.rowPB.id] == null) {
newRows.add(rowInfo);
} else {
_rowByRowId.remove(rowInfo.rowPB.id);
deletedIndex.add(DeletedIndex(index: index, row: rowInfo));
}
});
_rowInfos = newRows;
_rowChangeReasonNotifier.receive(RowsChangedReason.delete(deletedIndex));
}
void _insertRows(List<InsertedRowPB> insertRows) {
if (insertRows.isEmpty) {
return;
}
InsertedIndexs insertIndexs = [];
for (final InsertedRowPB insertRow in insertRows) {
final insertIndex = InsertedIndex(
index: insertRow.index,
rowId: insertRow.row.id,
);
insertIndexs.add(insertIndex);
_rowInfos.insert(
insertRow.index,
(buildGridRow(insertRow.row)),
);
}
_rowChangeReasonNotifier.receive(RowsChangedReason.insert(insertIndexs));
}
void _updateRows(List<RowPB> updatedRows) {
if (updatedRows.isEmpty) {
return;
}
final UpdatedIndexs updatedIndexs = UpdatedIndexs();
for (final RowPB updatedRow in updatedRows) {
final rowId = updatedRow.id;
final index = _rowInfos.indexWhere(
(rowInfo) => rowInfo.rowPB.id == rowId,
);
if (index != -1) {
_rowByRowId[rowId] = updatedRow;
_rowInfos.removeAt(index);
_rowInfos.insert(index, buildGridRow(updatedRow));
updatedIndexs[rowId] = UpdatedIndex(index: index, rowId: rowId);
}
}
_rowChangeReasonNotifier.receive(RowsChangedReason.update(updatedIndexs));
}
void _hideRows(List<String> hideRows) {}
void _showRows(List<String> visibleRows) {}
void onRowsChanged(
void Function(RowsChangedReason) onRowChanged,
) {
_rowChangeReasonNotifier.addListener(() {
onRowChanged(_rowChangeReasonNotifier.reason);
});
}
RowUpdateCallback addListener({
required String rowId,
void Function(GridCellMap, RowsChangedReason)? onCellUpdated,
bool Function()? listenWhen,
}) {
listenerHandler() async {
if (listenWhen != null && listenWhen() == false) {
return;
}
notifyUpdate() {
if (onCellUpdated != null) {
final row = _rowByRowId[rowId];
if (row != null) {
final GridCellMap cellDataMap = _makeGridCells(rowId, row);
onCellUpdated(cellDataMap, _rowChangeReasonNotifier.reason);
}
}
}
_rowChangeReasonNotifier.reason.whenOrNull(
update: (indexs) {
if (indexs[rowId] != null) notifyUpdate();
},
fieldDidChange: () => notifyUpdate(),
);
}
_rowChangeReasonNotifier.addListener(listenerHandler);
return listenerHandler;
}
void removeRowListener(VoidCallback callback) {
_rowChangeReasonNotifier.removeListener(callback);
}
GridCellMap loadGridCells(String rowId) {
final RowPB? data = _rowByRowId[rowId];
if (data == null) {
_loadRow(rowId);
}
return _makeGridCells(rowId, data);
}
Future<void> _loadRow(String rowId) async {
final payload = RowIdPB.create()
..gridId = gridId
..blockId = block.id
..rowId = rowId;
final result = await GridEventGetRow(payload).send();
result.fold(
(optionRow) => _refreshRow(optionRow),
(err) => Log.error(err),
);
}
GridCellMap _makeGridCells(String rowId, RowPB? row) {
var cellDataMap = GridCellMap.new();
for (final field in _fieldNotifier.fields) {
if (field.visibility) {
cellDataMap[field.id] = GridCellIdentifier(
rowId: rowId,
gridId: gridId,
field: field,
);
}
}
return cellDataMap;
}
void _refreshRow(OptionalRowPB optionRow) {
if (!optionRow.hasRow()) {
return;
}
final updatedRow = optionRow.row;
updatedRow.freeze();
_rowByRowId[updatedRow.id] = updatedRow;
final index =
_rowInfos.indexWhere((rowInfo) => rowInfo.rowPB.id == updatedRow.id);
if (index != -1) {
// update the corresponding row in _rows if they are not the same
if (_rowInfos[index].rowPB != updatedRow) {
final rowInfo = _rowInfos.removeAt(index).copyWith(rowPB: updatedRow);
_rowInfos.insert(index, rowInfo);
// Calculate the update index
final UpdatedIndexs updatedIndexs = UpdatedIndexs();
updatedIndexs[rowInfo.rowPB.id] = UpdatedIndex(
index: index,
rowId: rowInfo.rowPB.id,
);
//
_rowChangeReasonNotifier
.receive(RowsChangedReason.update(updatedIndexs));
}
}
}
RowInfo buildGridRow(RowPB rowPB) {
return RowInfo(
gridId: gridId,
fields: _fieldNotifier.fields,
rowPB: rowPB,
);
}
}
class _RowChangesetNotifier extends ChangeNotifier {
RowsChangedReason reason = const InitialListState();
_RowChangesetNotifier();
void receive(RowsChangedReason newReason) {
reason = newReason;
reason.map(
insert: (_) => notifyListeners(),
delete: (_) => notifyListeners(),
update: (_) => notifyListeners(),
fieldDidChange: (_) => notifyListeners(),
initial: (_) {},
);
}
}
@freezed
class RowInfo with _$RowInfo {
const factory RowInfo({
required String gridId,
required UnmodifiableListView<FieldPB> fields,
required RowPB rowPB,
}) = _RowInfo;
}
typedef InsertedIndexs = List<InsertedIndex>;
typedef DeletedIndexs = List<DeletedIndex>;
typedef UpdatedIndexs = LinkedHashMap<String, UpdatedIndex>;
@freezed
class RowsChangedReason with _$RowsChangedReason {
const factory RowsChangedReason.insert(InsertedIndexs items) = _Insert;
const factory RowsChangedReason.delete(DeletedIndexs items) = _Delete;
const factory RowsChangedReason.update(UpdatedIndexs indexs) = _Update;
const factory RowsChangedReason.fieldDidChange() = _FieldDidChange;
const factory RowsChangedReason.initial() = InitialListState;
}
class InsertedIndex {
final int index;
final String rowId;
InsertedIndex({
required this.index,
required this.rowId,
});
}
class DeletedIndex {
final int index;
final RowInfo row;
DeletedIndex({
required this.index,
required this.row,
});
}
class UpdatedIndex {
final int index;
final String rowId;
UpdatedIndex({
required this.index,
required this.rowId,
});
}

View File

@ -0,0 +1,49 @@
import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_field_notifier.dart';
import 'package:flutter/material.dart';
import '../../presentation/widgets/cell/cell_builder.dart';
import '../cell/cell_service/cell_service.dart';
import '../field/field_cache.dart';
import 'row_cache.dart';
typedef OnRowChanged = void Function(GridCellMap, RowsChangedReason);
class GridRowDataController extends GridCellBuilderDelegate {
final RowInfo rowInfo;
final List<VoidCallback> _onRowChangedListeners = [];
final GridFieldCache _fieldCache;
final GridRowCache _rowCache;
GridRowDataController({
required this.rowInfo,
required GridFieldCache fieldCache,
required GridRowCache rowCache,
}) : _fieldCache = fieldCache,
_rowCache = rowCache;
GridCellMap loadData() {
return _rowCache.loadGridCells(rowInfo.rowPB.id);
}
void addListener({OnRowChanged? onRowChanged}) {
_onRowChangedListeners.add(_rowCache.addListener(
rowId: rowInfo.rowPB.id,
onCellUpdated: onRowChanged,
));
}
void dispose() {
for (final fn in _onRowChangedListeners) {
_rowCache.removeRowListener(fn);
}
}
// GridCellBuilderDelegate implementation
@override
GridCellFieldNotifier buildFieldNotifier() {
return GridCellFieldNotifier(
notifier: GridCellFieldNotifierImpl(_fieldCache));
}
@override
GridCellCache get cellCache => _rowCache.cellCache;
}

View File

@ -2,26 +2,24 @@ import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_servic
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'dart:async';
import 'row_service.dart';
import 'row_data_controller.dart';
part 'row_detail_bloc.freezed.dart';
class RowDetailBloc extends Bloc<RowDetailEvent, RowDetailState> {
final GridRowInfo rowInfo;
final GridRowCache _rowCache;
void Function()? _rowListenFn;
final GridRowDataController dataController;
RowDetailBloc({
required this.rowInfo,
required GridRowCache rowCache,
}) : _rowCache = rowCache,
super(RowDetailState.initial()) {
required this.dataController,
}) : super(RowDetailState.initial()) {
on<RowDetailEvent>(
(event, emit) async {
await event.map(
initial: (_Initial value) async {
await _startListening();
_loadCellData();
final cells = dataController.loadData();
if (!isClosed) {
add(RowDetailEvent.didReceiveCellDatas(cells.values.toList()));
}
},
didReceiveCellDatas: (_DidReceiveCellDatas value) {
emit(state.copyWith(gridCells: value.gridCells));
@ -33,27 +31,19 @@ class RowDetailBloc extends Bloc<RowDetailEvent, RowDetailState> {
@override
Future<void> close() async {
if (_rowListenFn != null) {
_rowCache.removeRowListener(_rowListenFn!);
}
dataController.dispose();
return super.close();
}
Future<void> _startListening() async {
_rowListenFn = _rowCache.addListener(
rowId: rowInfo.id,
onCellUpdated: (cellDatas, reason) =>
add(RowDetailEvent.didReceiveCellDatas(cellDatas.values.toList())),
listenWhen: () => !isClosed,
dataController.addListener(
onRowChanged: (cells, reason) {
if (!isClosed) {
add(RowDetailEvent.didReceiveCellDatas(cells.values.toList()));
}
},
);
}
Future<void> _loadCellData() async {
final cellDataMap = _rowCache.loadGridCells(rowInfo.id);
if (!isClosed) {
add(RowDetailEvent.didReceiveCellDatas(cellDataMap.values.toList()));
}
}
}
@freezed

View File

@ -8,12 +8,13 @@ import 'dart:typed_data';
import 'package:dartz/dartz.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
typedef UpdateRowNotifiedValue = Either<GridRowPB, FlowyError>;
typedef UpdateFieldNotifiedValue = Either<List<GridFieldPB>, FlowyError>;
typedef UpdateRowNotifiedValue = Either<RowPB, FlowyError>;
typedef UpdateFieldNotifiedValue = Either<List<FieldPB>, FlowyError>;
class RowListener {
final String rowId;
PublishNotifier<UpdateRowNotifiedValue>? updateRowNotifier = PublishNotifier();
PublishNotifier<UpdateRowNotifiedValue>? updateRowNotifier =
PublishNotifier();
GridNotificationListener? _listener;
RowListener({required this.rowId});
@ -26,7 +27,8 @@ class RowListener {
switch (ty) {
case GridNotification.DidUpdateRow:
result.fold(
(payload) => updateRowNotifier?.value = left(GridRowPB.fromBuffer(payload)),
(payload) =>
updateRowNotifier?.value = left(RowPB.fromBuffer(payload)),
(error) => updateRowNotifier?.value = right(error),
);
break;

View File

@ -1,314 +1,29 @@
import 'dart:collection';
import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart';
import 'package:dartz/dartz.dart';
import 'package:flowy_sdk/dispatch/dispatch.dart';
import 'package:flowy_sdk/log.dart';
import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/block_entities.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/grid_entities.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/row_entities.pb.dart';
import 'package:flutter/foundation.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'row_service.freezed.dart';
typedef RowUpdateCallback = void Function();
abstract class GridRowCacheFieldNotifier {
UnmodifiableListView<GridFieldPB> get fields;
void onFieldsChanged(VoidCallback callback);
void onFieldChanged(void Function(GridFieldPB) callback);
void dispose();
}
/// Cache the rows in memory
/// Insert / delete / update row
///
/// Read https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/architecture/frontend/grid for more information.
class GridRowCache {
final String gridId;
final GridBlockPB block;
/// _rows containers the current block's rows
/// Use List to reverse the order of the GridRow.
List<GridRowInfo> _rowInfos = [];
/// Use Map for faster access the raw row data.
final HashMap<String, GridRowPB> _rowByRowId;
final GridCellCache _cellCache;
final GridRowCacheFieldNotifier _fieldNotifier;
final _GridRowChangesetNotifier _rowChangeReasonNotifier;
UnmodifiableListView<GridRowInfo> get rows => UnmodifiableListView(_rowInfos);
GridCellCache get cellCache => _cellCache;
GridRowCache({
required this.gridId,
required this.block,
required GridRowCacheFieldNotifier notifier,
}) : _cellCache = GridCellCache(gridId: gridId),
_rowByRowId = HashMap(),
_rowChangeReasonNotifier = _GridRowChangesetNotifier(),
_fieldNotifier = notifier {
//
notifier.onFieldsChanged(() => _rowChangeReasonNotifier
.receive(const GridRowChangeReason.fieldDidChange()));
notifier.onFieldChanged((field) => _cellCache.remove(field.id));
_rowInfos = block.rows
.map((rowInfo) => buildGridRow(rowInfo.id, rowInfo.height.toDouble()))
.toList();
}
Future<void> dispose() async {
_fieldNotifier.dispose();
_rowChangeReasonNotifier.dispose();
await _cellCache.dispose();
}
void applyChangesets(List<GridBlockChangesetPB> changesets) {
for (final changeset in changesets) {
_deleteRows(changeset.deletedRows);
_insertRows(changeset.insertedRows);
_updateRows(changeset.updatedRows);
_hideRows(changeset.hideRows);
_showRows(changeset.visibleRows);
}
}
void _deleteRows(List<String> deletedRows) {
if (deletedRows.isEmpty) {
return;
}
final List<GridRowInfo> newRows = [];
final DeletedIndexs deletedIndex = [];
final Map<String, String> deletedRowByRowId = {
for (var rowId in deletedRows) rowId: rowId
};
_rowInfos.asMap().forEach((index, row) {
if (deletedRowByRowId[row.id] == null) {
newRows.add(row);
} else {
_rowByRowId.remove(row.id);
deletedIndex.add(DeletedIndex(index: index, row: row));
}
});
_rowInfos = newRows;
_rowChangeReasonNotifier.receive(GridRowChangeReason.delete(deletedIndex));
}
void _insertRows(List<InsertedRowPB> insertRows) {
if (insertRows.isEmpty) {
return;
}
InsertedIndexs insertIndexs = [];
for (final insertRow in insertRows) {
final insertIndex = InsertedIndex(
index: insertRow.index,
rowId: insertRow.rowId,
);
insertIndexs.add(insertIndex);
_rowInfos.insert(insertRow.index,
(buildGridRow(insertRow.rowId, insertRow.height.toDouble())));
}
_rowChangeReasonNotifier.receive(GridRowChangeReason.insert(insertIndexs));
}
void _updateRows(List<UpdatedRowPB> updatedRows) {
if (updatedRows.isEmpty) {
return;
}
final UpdatedIndexs updatedIndexs = UpdatedIndexs();
for (final updatedRow in updatedRows) {
final rowId = updatedRow.rowId;
final index = _rowInfos.indexWhere((row) => row.id == rowId);
if (index != -1) {
_rowByRowId[rowId] = updatedRow.row;
_rowInfos.removeAt(index);
_rowInfos.insert(
index, buildGridRow(rowId, updatedRow.row.height.toDouble()));
updatedIndexs[rowId] = UpdatedIndex(index: index, rowId: rowId);
}
}
_rowChangeReasonNotifier.receive(GridRowChangeReason.update(updatedIndexs));
}
void _hideRows(List<String> hideRows) {}
void _showRows(List<String> visibleRows) {}
void onRowsChanged(
void Function(GridRowChangeReason) onRowChanged,
) {
_rowChangeReasonNotifier.addListener(() {
onRowChanged(_rowChangeReasonNotifier.reason);
});
}
RowUpdateCallback addListener({
required String rowId,
void Function(GridCellMap, GridRowChangeReason)? onCellUpdated,
bool Function()? listenWhen,
}) {
listenrHandler() async {
if (listenWhen != null && listenWhen() == false) {
return;
}
notifyUpdate() {
if (onCellUpdated != null) {
final row = _rowByRowId[rowId];
if (row != null) {
final GridCellMap cellDataMap = _makeGridCells(rowId, row);
onCellUpdated(cellDataMap, _rowChangeReasonNotifier.reason);
}
}
}
_rowChangeReasonNotifier.reason.whenOrNull(
update: (indexs) {
if (indexs[rowId] != null) notifyUpdate();
},
fieldDidChange: () => notifyUpdate(),
);
}
_rowChangeReasonNotifier.addListener(listenrHandler);
return listenrHandler;
}
void removeRowListener(VoidCallback callback) {
_rowChangeReasonNotifier.removeListener(callback);
}
GridCellMap loadGridCells(String rowId) {
final GridRowPB? data = _rowByRowId[rowId];
if (data == null) {
_loadRow(rowId);
}
return _makeGridCells(rowId, data);
}
Future<void> _loadRow(String rowId) async {
final payload = GridRowIdPB.create()
..gridId = gridId
..blockId = block.id
..rowId = rowId;
final result = await GridEventGetRow(payload).send();
result.fold(
(optionRow) => _refreshRow(optionRow),
(err) => Log.error(err),
);
}
GridCellMap _makeGridCells(String rowId, GridRowPB? row) {
var cellDataMap = GridCellMap.new();
for (final field in _fieldNotifier.fields) {
if (field.visibility) {
cellDataMap[field.id] = GridCellIdentifier(
rowId: rowId,
gridId: gridId,
field: field,
);
}
}
return cellDataMap;
}
void _refreshRow(OptionalRowPB optionRow) {
if (!optionRow.hasRow()) {
return;
}
final updatedRow = optionRow.row;
updatedRow.freeze();
_rowByRowId[updatedRow.id] = updatedRow;
final index =
_rowInfos.indexWhere((gridRow) => gridRow.id == updatedRow.id);
if (index != -1) {
// update the corresponding row in _rows if they are not the same
if (_rowInfos[index].rawRow != updatedRow) {
final row = _rowInfos.removeAt(index).copyWith(rawRow: updatedRow);
_rowInfos.insert(index, row);
// Calculate the update index
final UpdatedIndexs updatedIndexs = UpdatedIndexs();
updatedIndexs[row.id] = UpdatedIndex(index: index, rowId: row.id);
//
_rowChangeReasonNotifier
.receive(GridRowChangeReason.update(updatedIndexs));
}
}
}
GridRowInfo buildGridRow(String rowId, double rowHeight) {
return GridRowInfo(
gridId: gridId,
blockId: block.id,
fields: _fieldNotifier.fields,
id: rowId,
height: rowHeight,
);
}
}
class _GridRowChangesetNotifier extends ChangeNotifier {
GridRowChangeReason reason = const InitialListState();
_GridRowChangesetNotifier();
void receive(GridRowChangeReason newReason) {
reason = newReason;
reason.map(
insert: (_) => notifyListeners(),
delete: (_) => notifyListeners(),
update: (_) => notifyListeners(),
fieldDidChange: (_) => notifyListeners(),
initial: (_) {},
);
}
}
class RowService {
class RowFFIService {
final String gridId;
final String blockId;
final String rowId;
RowService(
{required this.gridId, required this.blockId, required this.rowId});
RowFFIService({
required this.gridId,
required this.blockId,
});
Future<Either<GridRowPB, FlowyError>> createRow() {
CreateRowPayloadPB payload = CreateRowPayloadPB.create()
Future<Either<RowPB, FlowyError>> createRow(String rowId) {
final payload = CreateTableRowPayloadPB.create()
..gridId = gridId
..startRowId = rowId;
return GridEventCreateRow(payload).send();
return GridEventCreateTableRow(payload).send();
}
Future<Either<Unit, FlowyError>> moveRow(
String rowId, int fromIndex, int toIndex) {
final payload = MoveItemPayloadPB.create()
..gridId = gridId
..itemId = rowId
..ty = MoveItemTypePB.MoveRow
..fromIndex = fromIndex
..toIndex = toIndex;
return GridEventMoveItem(payload).send();
}
Future<Either<OptionalRowPB, FlowyError>> getRow() {
final payload = GridRowIdPB.create()
Future<Either<OptionalRowPB, FlowyError>> getRow(String rowId) {
final payload = RowIdPB.create()
..gridId = gridId
..blockId = blockId
..rowId = rowId;
@ -316,8 +31,8 @@ class RowService {
return GridEventGetRow(payload).send();
}
Future<Either<Unit, FlowyError>> deleteRow() {
final payload = GridRowIdPB.create()
Future<Either<Unit, FlowyError>> deleteRow(String rowId) {
final payload = RowIdPB.create()
..gridId = gridId
..blockId = blockId
..rowId = rowId;
@ -325,8 +40,8 @@ class RowService {
return GridEventDeleteRow(payload).send();
}
Future<Either<Unit, FlowyError>> duplicateRow() {
final payload = GridRowIdPB.create()
Future<Either<Unit, FlowyError>> duplicateRow(String rowId) {
final payload = RowIdPB.create()
..gridId = gridId
..blockId = blockId
..rowId = rowId;
@ -335,54 +50,22 @@ class RowService {
}
}
@freezed
class GridRowInfo with _$GridRowInfo {
const factory GridRowInfo({
required String gridId,
required String blockId,
required String id,
required UnmodifiableListView<GridFieldPB> fields,
required double height,
GridRowPB? rawRow,
}) = _GridRowInfo;
}
class MoveRowFFIService {
final String gridId;
typedef InsertedIndexs = List<InsertedIndex>;
typedef DeletedIndexs = List<DeletedIndex>;
typedef UpdatedIndexs = LinkedHashMap<String, UpdatedIndex>;
@freezed
class GridRowChangeReason with _$GridRowChangeReason {
const factory GridRowChangeReason.insert(InsertedIndexs items) = _Insert;
const factory GridRowChangeReason.delete(DeletedIndexs items) = _Delete;
const factory GridRowChangeReason.update(UpdatedIndexs indexs) = _Update;
const factory GridRowChangeReason.fieldDidChange() = _FieldDidChange;
const factory GridRowChangeReason.initial() = InitialListState;
}
class InsertedIndex {
final int index;
final String rowId;
InsertedIndex({
required this.index,
required this.rowId,
MoveRowFFIService({
required this.gridId,
});
}
class DeletedIndex {
final int index;
final GridRowInfo row;
DeletedIndex({
required this.index,
required this.row,
});
}
Future<Either<Unit, FlowyError>> moveRow({
required String fromRowId,
required String toRowId,
}) {
var payload = MoveRowPayloadPB.create()
..viewId = gridId
..fromRowId = fromRowId
..toRowId = toRowId;
class UpdatedIndex {
final int index;
final String rowId;
UpdatedIndex({
required this.index,
required this.rowId,
});
return GridEventMoveRow(payload).send();
}
}

View File

@ -1,16 +1,17 @@
import 'package:app_flowy/plugins/grid/application/field/field_service.dart';
import 'package:app_flowy/plugins/grid/application/grid_service.dart';
import 'package:flowy_sdk/log.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'dart:async';
import '../field/field_cache.dart';
part 'property_bloc.freezed.dart';
class GridPropertyBloc extends Bloc<GridPropertyEvent, GridPropertyState> {
final GridFieldCache _fieldCache;
Function(List<GridFieldPB>)? _onFieldsFn;
Function(List<FieldPB>)? _onFieldsFn;
GridPropertyBloc({required String gridId, required GridFieldCache fieldCache})
: _fieldCache = fieldCache,
@ -66,8 +67,8 @@ class GridPropertyEvent with _$GridPropertyEvent {
const factory GridPropertyEvent.initial() = _Initial;
const factory GridPropertyEvent.setFieldVisibility(
String fieldId, bool visibility) = _SetFieldVisibility;
const factory GridPropertyEvent.didReceiveFieldUpdate(
List<GridFieldPB> fields) = _DidReceiveFieldUpdate;
const factory GridPropertyEvent.didReceiveFieldUpdate(List<FieldPB> fields) =
_DidReceiveFieldUpdate;
const factory GridPropertyEvent.moveField(int fromIndex, int toIndex) =
_MoveField;
}
@ -76,10 +77,10 @@ class GridPropertyEvent with _$GridPropertyEvent {
class GridPropertyState with _$GridPropertyState {
const factory GridPropertyState({
required String gridId,
required List<GridFieldPB> fields,
required List<FieldPB> fields,
}) = _GridPropertyState;
factory GridPropertyState.initial(String gridId, List<GridFieldPB> fields) =>
factory GridPropertyState.initial(String gridId, List<FieldPB> fields) =>
GridPropertyState(
gridId: gridId,
fields: fields,

View File

@ -22,10 +22,13 @@ class GridPluginBuilder implements PluginBuilder {
String get menuName => LocaleKeys.grid_menuName.tr();
@override
PluginType get pluginType => DefaultPlugin.grid.type();
PluginType get pluginType => PluginType.grid;
@override
ViewDataType get dataType => ViewDataType.Grid;
ViewDataTypePB get dataType => ViewDataTypePB.Database;
@override
ViewLayoutTypePB? get subDataType => ViewLayoutTypePB.Grid;
}
class GridPluginConfig implements PluginConfig {

View File

@ -2,19 +2,20 @@ import 'package:flutter/material.dart';
import 'package:linked_scroll_controller/linked_scroll_controller.dart';
class GridScrollController {
final LinkedScrollControllerGroup _scrollGroupContorller;
final LinkedScrollControllerGroup _scrollGroupController;
final ScrollController verticalController;
final ScrollController horizontalController;
final List<ScrollController> _linkHorizontalControllers = [];
GridScrollController({required LinkedScrollControllerGroup scrollGroupContorller})
: _scrollGroupContorller = scrollGroupContorller,
GridScrollController(
{required LinkedScrollControllerGroup scrollGroupController})
: _scrollGroupController = scrollGroupController,
verticalController = ScrollController(),
horizontalController = scrollGroupContorller.addAndGet();
horizontalController = scrollGroupController.addAndGet();
ScrollController linkHorizontalController() {
final controller = _scrollGroupContorller.addAndGet();
final controller = _scrollGroupController.addAndGet();
_linkHorizontalControllers.add(controller);
return controller;
}

View File

@ -1,6 +1,7 @@
import 'package:app_flowy/plugins/grid/application/field/field_cache.dart';
import 'package:app_flowy/plugins/grid/application/row/row_data_controller.dart';
import 'package:app_flowy/startup/startup.dart';
import 'package:app_flowy/plugins/grid/application/grid_bloc.dart';
import 'package:app_flowy/plugins/grid/application/row/row_service.dart';
import 'package:flowy_infra/theme.dart';
import 'package:flowy_infra_ui/style_widget/scrolling/styled_list.dart';
import 'package:flowy_infra_ui/style_widget/scrolling/styled_scroll_bar.dart';
@ -11,12 +12,15 @@ import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter/material.dart';
import 'package:linked_scroll_controller/linked_scroll_controller.dart';
import '../application/row/row_cache.dart';
import 'controller/grid_scroll.dart';
import 'layout/layout.dart';
import 'layout/sizes.dart';
import 'widgets/cell/cell_builder.dart';
import 'widgets/row/grid_row.dart';
import 'widgets/footer/grid_footer.dart';
import 'widgets/header/grid_header.dart';
import 'widgets/row/row_detail.dart';
import 'widgets/shortcuts.dart';
import 'widgets/toolbar/grid_toolbar.dart';
@ -79,7 +83,7 @@ class FlowyGrid extends StatefulWidget {
class _FlowyGridState extends State<FlowyGrid> {
final _scrollController = GridScrollController(
scrollGroupContorller: LinkedScrollControllerGroup());
scrollGroupController: LinkedScrollControllerGroup());
late ScrollController headerScrollController;
@override
@ -153,7 +157,7 @@ class _FlowyGridState extends State<FlowyGrid> {
}
Widget _gridHeader(BuildContext context, String gridId) {
final fieldCache = context.read<GridBloc>().fieldCache;
final fieldCache = context.read<GridBloc>().dataController.fieldCache;
return GridHeaderSliverAdaptor(
gridId: gridId,
fieldCache: fieldCache,
@ -169,7 +173,7 @@ class _GridToolbarAdaptor extends StatelessWidget {
Widget build(BuildContext context) {
return BlocSelector<GridBloc, GridState, GridToolbarContext>(
selector: (state) {
final fieldCache = context.read<GridBloc>().fieldCache;
final fieldCache = context.read<GridBloc>().dataController.fieldCache;
return GridToolbarContext(
gridId: state.gridId,
fieldCache: fieldCache,
@ -221,7 +225,7 @@ class _GridRowsState extends State<_GridRows> {
initialItemCount: context.read<GridBloc>().state.rowInfos.length,
itemBuilder:
(BuildContext context, int index, Animation<double> animation) {
final GridRowInfo rowInfo =
final RowInfo rowInfo =
context.read<GridBloc>().state.rowInfos[index];
return _renderRow(context, rowInfo, animation);
},
@ -232,25 +236,61 @@ class _GridRowsState extends State<_GridRows> {
Widget _renderRow(
BuildContext context,
GridRowInfo rowInfo,
RowInfo rowInfo,
Animation<double> animation,
) {
final rowCache =
context.read<GridBloc>().getRowCache(rowInfo.blockId, rowInfo.id);
final fieldCache = context.read<GridBloc>().fieldCache;
if (rowCache != null) {
return SizeTransition(
sizeFactor: animation,
child: GridRowWidget(
rowData: rowInfo,
rowCache: rowCache,
fieldCache: fieldCache,
key: ValueKey(rowInfo.id),
),
);
} else {
return const SizedBox();
}
final rowCache = context.read<GridBloc>().getRowCache(
rowInfo.rowPB.blockId,
rowInfo.rowPB.id,
);
/// Return placeholder widget if the rowCache is null.
if (rowCache == null) return const SizedBox();
final fieldCache = context.read<GridBloc>().dataController.fieldCache;
final dataController = GridRowDataController(
rowInfo: rowInfo,
fieldCache: fieldCache,
rowCache: rowCache,
);
return SizeTransition(
sizeFactor: animation,
child: GridRowWidget(
rowInfo: rowInfo,
dataController: dataController,
cellBuilder: GridCellBuilder(delegate: dataController),
openDetailPage: (context, cellBuilder) {
_openRowDetailPage(
context,
rowInfo,
fieldCache,
rowCache,
cellBuilder,
);
},
key: ValueKey(rowInfo.rowPB.id),
),
);
}
void _openRowDetailPage(
BuildContext context,
RowInfo rowInfo,
GridFieldCache fieldCache,
GridRowCache rowCache,
GridCellBuilder cellBuilder,
) {
final dataController = GridRowDataController(
rowInfo: rowInfo,
fieldCache: fieldCache,
rowCache: rowCache,
);
RowDetailPage(
cellBuilder: cellBuilder,
dataController: dataController,
).show(context);
}
}

View File

@ -2,11 +2,15 @@ import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
import 'sizes.dart';
class GridLayout {
static double headerWidth(List<GridFieldPB> fields) {
static double headerWidth(List<FieldPB> fields) {
if (fields.isEmpty) return 0;
final fieldsWidth = fields.map((field) => field.width.toDouble()).reduce((value, element) => value + element);
final fieldsWidth = fields
.map((field) => field.width.toDouble())
.reduce((value, element) => value + element);
return fieldsWidth + GridSize.leadingHeaderPadding + GridSize.trailHeaderPadding;
return fieldsWidth +
GridSize.leadingHeaderPadding +
GridSize.trailHeaderPadding;
}
}

View File

@ -8,6 +8,8 @@ import 'package:styled_widget/styled_widget.dart';
import 'package:app_flowy/generated/locale_keys.g.dart';
import 'package:easy_localization/easy_localization.dart';
import 'cell_builder.dart';
class GridCellAccessoryBuildContext {
final BuildContext anchorContext;
final bool isCellEditing;
@ -57,18 +59,6 @@ class PrimaryCellAccessory extends StatelessWidget with GridCellAccessory {
bool enable() => !isCellEditing;
}
typedef AccessoryBuilder = List<GridCellAccessory> Function(
GridCellAccessoryBuildContext buildContext);
abstract class CellAccessory extends Widget {
const CellAccessory({Key? key}) : super(key: key);
// The hover will show if the isHover's value is true
ValueNotifier<bool>? get onAccessoryHover;
AccessoryBuilder? get accessoryBuilder;
}
class AccessoryHover extends StatefulWidget {
final CellAccessory child;
final EdgeInsets contentPadding;

View File

@ -1,5 +1,4 @@
import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart';
import 'package:app_flowy/plugins/grid/application/grid_service.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
@ -13,53 +12,66 @@ import 'select_option_cell/select_option_cell.dart';
import 'text_cell.dart';
import 'url_cell/url_cell.dart';
abstract class GridCellBuilderDelegate
extends GridCellControllerBuilderDelegate {
GridCellCache get cellCache;
}
class GridCellBuilder {
final GridCellCache cellCache;
final GridFieldCache fieldCache;
final GridCellBuilderDelegate delegate;
GridCellBuilder({
required this.cellCache,
required this.fieldCache,
required this.delegate,
});
GridCellWidget build(GridCellIdentifier cell, {GridCellStyle? style}) {
GridCellWidget build(GridCellIdentifier cellId, {GridCellStyle? style}) {
final cellControllerBuilder = GridCellControllerBuilder(
cellId: cell,
cellCache: cellCache,
fieldCache: fieldCache,
cellId: cellId,
cellCache: delegate.cellCache,
delegate: delegate,
);
final key = cell.key();
switch (cell.fieldType) {
final key = cellId.key();
switch (cellId.fieldType) {
case FieldType.Checkbox:
return GridCheckboxCell(
cellControllerBuilder: cellControllerBuilder, key: key);
cellControllerBuilder: cellControllerBuilder,
key: key,
);
case FieldType.DateTime:
return GridDateCell(
cellControllerBuilder: cellControllerBuilder,
key: key,
style: style);
cellControllerBuilder: cellControllerBuilder,
key: key,
style: style,
);
case FieldType.SingleSelect:
return GridSingleSelectCell(
cellContorllerBuilder: cellControllerBuilder,
style: style,
key: key);
cellControllerBuilder: cellControllerBuilder,
style: style,
key: key,
);
case FieldType.MultiSelect:
return GridMultiSelectCell(
cellContorllerBuilder: cellControllerBuilder,
style: style,
key: key);
cellControllerBuilder: cellControllerBuilder,
style: style,
key: key,
);
case FieldType.Number:
return GridNumberCell(
cellContorllerBuilder: cellControllerBuilder, key: key);
cellControllerBuilder: cellControllerBuilder,
key: key,
);
case FieldType.RichText:
return GridTextCell(
cellContorllerBuilder: cellControllerBuilder,
style: style,
key: key);
cellControllerBuilder: cellControllerBuilder,
style: style,
key: key,
);
case FieldType.URL:
return GridURLCell(
cellContorllerBuilder: cellControllerBuilder,
style: style,
key: key);
cellControllerBuilder: cellControllerBuilder,
style: style,
key: key,
);
}
throw UnimplementedError;
}
@ -82,6 +94,18 @@ abstract class CellEditable {
ValueNotifier<bool> get onCellEditing;
}
typedef AccessoryBuilder = List<GridCellAccessory> Function(
GridCellAccessoryBuildContext buildContext);
abstract class CellAccessory extends Widget {
const CellAccessory({Key? key}) : super(key: key);
// The hover will show if the isHover's value is true
ValueNotifier<bool>? get onAccessoryHover;
AccessoryBuilder? get accessoryBuilder;
}
abstract class GridCellWidget extends StatefulWidget
implements CellAccessory, CellEditable, CellShortcuts {
GridCellWidget({Key? key}) : super(key: key) {
@ -93,7 +117,7 @@ abstract class GridCellWidget extends StatefulWidget
@override
final ValueNotifier<bool> onCellFocus = ValueNotifier<bool>(false);
// When the cell is focused, we assume that the accessory alse be hovered.
// When the cell is focused, we assume that the accessory also be hovered.
@override
ValueNotifier<bool> get onAccessoryHover => onCellFocus;
@ -150,7 +174,7 @@ abstract class GridCellState<T extends GridCellWidget> extends State<T> {
abstract class GridFocusNodeCellState<T extends GridCellWidget>
extends GridCellState<T> {
SingleListenrFocusNode focusNode = SingleListenrFocusNode();
SingleListenerFocusNode focusNode = SingleListenerFocusNode();
@override
void initState() {
@ -219,7 +243,7 @@ class GridCellFocusListener extends ChangeNotifier {
abstract class GridCellStyle {}
class SingleListenrFocusNode extends FocusNode {
class SingleListenerFocusNode extends FocusNode {
VoidCallback? _listener;
void setListener(VoidCallback listener) {

View File

@ -25,24 +25,28 @@ class CellContainer extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ChangeNotifierProxyProvider<RegionStateNotifier,
CellContainerNotifier>(
create: (_) => CellContainerNotifier(child),
_CellContainerNotifier>(
create: (_) => _CellContainerNotifier(child),
update: (_, rowStateNotifier, cellStateNotifier) =>
cellStateNotifier!..onEnter = rowStateNotifier.onEnter,
child: Selector<CellContainerNotifier, bool>(
child: Selector<_CellContainerNotifier, bool>(
selector: (context, notifier) => notifier.isFocus,
builder: (context, isFocus, _) {
Widget container = Center(child: GridCellShortcuts(child: child));
if (accessoryBuilder != null) {
final accessories = accessoryBuilder!(GridCellAccessoryBuildContext(
anchorContext: context,
isCellEditing: isFocus,
));
final accessories = accessoryBuilder!(
GridCellAccessoryBuildContext(
anchorContext: context,
isCellEditing: isFocus,
),
);
if (accessories.isNotEmpty) {
container =
CellEnterRegion(child: container, accessories: accessories);
container = _GridCellEnterRegion(
child: container,
accessories: accessories,
);
}
}
@ -74,16 +78,16 @@ class CellContainer extends StatelessWidget {
}
}
class CellEnterRegion extends StatelessWidget {
class _GridCellEnterRegion extends StatelessWidget {
final Widget child;
final List<GridCellAccessory> accessories;
const CellEnterRegion(
const _GridCellEnterRegion(
{required this.child, required this.accessories, Key? key})
: super(key: key);
@override
Widget build(BuildContext context) {
return Selector<CellContainerNotifier, bool>(
return Selector<_CellContainerNotifier, bool>(
selector: (context, notifier) => notifier.onEnter,
builder: (context, onEnter, _) {
List<Widget> children = [child];
@ -95,10 +99,10 @@ class CellEnterRegion extends StatelessWidget {
return MouseRegion(
cursor: SystemMouseCursors.click,
onEnter: (p) =>
Provider.of<CellContainerNotifier>(context, listen: false)
Provider.of<_CellContainerNotifier>(context, listen: false)
.onEnter = true,
onExit: (p) =>
Provider.of<CellContainerNotifier>(context, listen: false)
Provider.of<_CellContainerNotifier>(context, listen: false)
.onEnter = false,
child: Stack(
alignment: AlignmentDirectional.center,
@ -111,13 +115,13 @@ class CellEnterRegion extends StatelessWidget {
}
}
class CellContainerNotifier extends ChangeNotifier {
class _CellContainerNotifier extends ChangeNotifier {
final CellEditable cellEditable;
VoidCallback? _onCellFocusListener;
bool _isFocus = false;
bool _onEnter = false;
CellContainerNotifier(this.cellEditable) {
_CellContainerNotifier(this.cellEditable) {
_onCellFocusListener = () => isFocus = cellEditable.onCellFocus.value;
cellEditable.onCellFocus.addListener(_onCellFocusListener!);
}

View File

@ -24,14 +24,16 @@ class GridCellShortcuts extends StatelessWidget {
return Shortcuts(
shortcuts: {
LogicalKeySet(LogicalKeyboardKey.enter): const GridCellEnterIdent(),
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.keyC): const GridCellCopyIntent(),
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.keyV): const GridCellInsertIntent(),
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.keyC):
const GridCellCopyIntent(),
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.keyV):
const GridCellPasteIntent(),
},
child: Actions(
actions: {
GridCellEnterIdent: GridCellEnterAction(child: child),
GridCellCopyIntent: GridCellCopyAction(child: child),
GridCellInsertIntent: GridCellInsertAction(child: child),
GridCellPasteIntent: GridCellPasteAction(child: child),
},
child: child,
),
@ -78,16 +80,16 @@ class GridCellCopyAction extends Action<GridCellCopyIntent> {
}
}
class GridCellInsertIntent extends Intent {
const GridCellInsertIntent();
class GridCellPasteIntent extends Intent {
const GridCellPasteIntent();
}
class GridCellInsertAction extends Action<GridCellInsertIntent> {
class GridCellPasteAction extends Action<GridCellPasteIntent> {
final CellShortcuts child;
GridCellInsertAction({required this.child});
GridCellPasteAction({required this.child});
@override
void invoke(covariant GridCellInsertIntent intent) {
void invoke(covariant GridCellPasteIntent intent) {
final callback = child.shortcutHandlers[CellKeyboardKey.onInsert];
if (callback != null) {
callback();

View File

@ -22,8 +22,9 @@ class _CheckboxCellState extends GridCellState<GridCheckboxCell> {
@override
void initState() {
final cellContext = widget.cellControllerBuilder.build();
_cellBloc = getIt<CheckboxCellBloc>(param1: cellContext)
final cellController =
widget.cellControllerBuilder.build() as GridCheckboxCellController;
_cellBloc = getIt<CheckboxCellBloc>(param1: cellController)
..add(const CheckboxCellEvent.initial());
super.initState();
}

View File

@ -43,8 +43,8 @@ class _DateCellState extends GridCellState<GridDateCell> {
@override
void initState() {
final cellContext = widget.cellControllerBuilder.build();
_cellBloc = getIt<DateCellBloc>(param1: cellContext)
final cellController = widget.cellControllerBuilder.build();
_cellBloc = getIt<DateCellBloc>(param1: cellController)
..add(const DateCellEvent.initial());
super.initState();
}
@ -84,7 +84,7 @@ class _DateCellState extends GridCellState<GridDateCell> {
DateCellEditor(onDismissed: () => widget.onCellEditing.value = false);
calendar.show(
context,
cellController: bloc.cellContext.clone(),
cellController: bloc.cellController.clone(),
);
}

View File

@ -1,5 +1,6 @@
import 'package:app_flowy/generated/locale_keys.g.dart';
import 'package:app_flowy/plugins/grid/application/cell/date_cal_bloc.dart';
import 'package:app_flowy/plugins/grid/application/field/type_option/type_option_context.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/image.dart';
import 'package:flowy_infra/theme.dart';
@ -14,7 +15,6 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:table_calendar/table_calendar.dart';
import 'package:app_flowy/plugins/grid/application/prelude.dart';
import '../../../layout/sizes.dart';
import '../../header/type_option/date.dart';
@ -38,11 +38,12 @@ class DateCellEditor with FlowyOverlayDelegate {
final result =
await cellController.getFieldTypeOption(DateTypeOptionDataParser());
result.fold(
(dateTypeOption) {
(dateTypeOptionPB) {
final calendar = _CellCalendarWidget(
cellContext: cellController,
dateTypeOption: dateTypeOption,
dateTypeOptionPB: dateTypeOptionPB,
);
FlowyOverlay.of(context).insertWithAnchor(
@ -78,11 +79,11 @@ class DateCellEditor with FlowyOverlayDelegate {
class _CellCalendarWidget extends StatelessWidget {
final GridDateCellController cellContext;
final DateTypeOption dateTypeOption;
final DateTypeOptionPB dateTypeOptionPB;
const _CellCalendarWidget({
required this.cellContext,
required this.dateTypeOption,
required this.dateTypeOptionPB,
Key? key,
}) : super(key: key);
@ -92,9 +93,9 @@ class _CellCalendarWidget extends StatelessWidget {
return BlocProvider(
create: (context) {
return DateCalBloc(
dateTypeOption: dateTypeOption,
dateTypeOptionPB: dateTypeOptionPB,
cellData: cellContext.getCellData(),
cellContext: cellContext,
cellController: cellContext,
)..add(const DateCalEvent.initial());
},
child: BlocBuilder<DateCalBloc, DateCalState>(
@ -196,7 +197,7 @@ class _IncludeTimeButton extends StatelessWidget {
Widget build(BuildContext context) {
final theme = context.watch<AppTheme>();
return BlocSelector<DateCalBloc, DateCalState, bool>(
selector: (state) => state.dateTypeOption.includeTime,
selector: (state) => state.dateTypeOptionPB.includeTime,
builder: (context, includeTime) {
return SizedBox(
height: 50,
@ -243,7 +244,7 @@ class _TimeTextFieldState extends State<_TimeTextField> {
void initState() {
_focusNode = FocusNode();
_controller = TextEditingController(text: widget.bloc.state.time);
if (widget.bloc.state.dateTypeOption.includeTime) {
if (widget.bloc.state.dateTypeOptionPB.includeTime) {
_focusNode.addListener(() {
if (mounted) {
_CalDateTimeSetting.hide(context);
@ -264,7 +265,7 @@ class _TimeTextFieldState extends State<_TimeTextField> {
},
listenWhen: (p, c) => p.time != c.time,
builder: (context, state) {
if (state.dateTypeOption.includeTime) {
if (state.dateTypeOptionPB.includeTime) {
return Padding(
padding: kMargin,
child: RoundedInputField(
@ -306,23 +307,24 @@ class _DateTypeOptionButton extends StatelessWidget {
final title = LocaleKeys.grid_field_dateFormat.tr() +
" &" +
LocaleKeys.grid_field_timeFormat.tr();
return BlocSelector<DateCalBloc, DateCalState, DateTypeOption>(
selector: (state) => state.dateTypeOption,
builder: (context, dateTypeOption) {
return BlocSelector<DateCalBloc, DateCalState, DateTypeOptionPB>(
selector: (state) => state.dateTypeOptionPB,
builder: (context, dateTypeOptionPB) {
return FlowyButton(
text: FlowyText.medium(title, fontSize: 12),
hoverColor: theme.hover,
margin: kMargin,
onTap: () => _showTimeSetting(dateTypeOption, context),
onTap: () => _showTimeSetting(dateTypeOptionPB, context),
rightIcon: svgWidget("grid/more", color: theme.iconColor),
);
},
);
}
void _showTimeSetting(DateTypeOption dateTypeOption, BuildContext context) {
void _showTimeSetting(
DateTypeOptionPB dateTypeOptionPB, BuildContext context) {
final setting = _CalDateTimeSetting(
dateTypeOption: dateTypeOption,
dateTypeOptionPB: dateTypeOptionPB,
onEvent: (event) => context.read<DateCalBloc>().add(event),
);
setting.show(context);
@ -330,10 +332,10 @@ class _DateTypeOptionButton extends StatelessWidget {
}
class _CalDateTimeSetting extends StatefulWidget {
final DateTypeOption dateTypeOption;
final DateTypeOptionPB dateTypeOptionPB;
final Function(DateCalEvent) onEvent;
const _CalDateTimeSetting(
{required this.dateTypeOption, required this.onEvent, Key? key})
{required this.dateTypeOptionPB, required this.onEvent, Key? key})
: super(key: key);
@override
@ -370,17 +372,17 @@ class _CalDateTimeSettingState extends State<_CalDateTimeSetting> {
List<Widget> children = [
DateFormatButton(onTap: () {
final list = DateFormatList(
selectedFormat: widget.dateTypeOption.dateFormat,
selectedFormat: widget.dateTypeOptionPB.dateFormat,
onSelected: (format) =>
widget.onEvent(DateCalEvent.setDateFormat(format)),
);
_showOverlay(context, list);
}),
TimeFormatButton(
timeFormat: widget.dateTypeOption.timeFormat,
timeFormat: widget.dateTypeOptionPB.timeFormat,
onTap: () {
final list = TimeFormatList(
selectedFormat: widget.dateTypeOption.timeFormat,
selectedFormat: widget.dateTypeOptionPB.timeFormat,
onSelected: (format) =>
widget.onEvent(DateCalEvent.setTimeFormat(format)),
);

View File

@ -7,10 +7,10 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'cell_builder.dart';
class GridNumberCell extends GridCellWidget {
final GridCellControllerBuilder cellContorllerBuilder;
final GridCellControllerBuilder cellControllerBuilder;
GridNumberCell({
required this.cellContorllerBuilder,
required this.cellControllerBuilder,
Key? key,
}) : super(key: key);
@ -25,8 +25,8 @@ class _NumberCellState extends GridFocusNodeCellState<GridNumberCell> {
@override
void initState() {
final cellContext = widget.cellContorllerBuilder.build();
_cellBloc = getIt<NumberCellBloc>(param1: cellContext)
final cellController = widget.cellControllerBuilder.build();
_cellBloc = getIt<NumberCellBloc>(param1: cellController)
..add(const NumberCellEvent.initial());
_controller =
TextEditingController(text: contentFromState(_cellBloc.state));
@ -49,8 +49,10 @@ class _NumberCellState extends GridFocusNodeCellState<GridNumberCell> {
controller: _controller,
focusNode: focusNode,
onEditingComplete: () => focusNode.unfocus(),
maxLines: null,
onSubmitted: (_) => focusNode.unfocus(),
maxLines: 1,
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
textInputAction: TextInputAction.done,
decoration: const InputDecoration(
contentPadding: EdgeInsets.zero,
border: InputBorder.none,
@ -63,7 +65,7 @@ class _NumberCellState extends GridFocusNodeCellState<GridNumberCell> {
@override
Future<void> dispose() async {
_delayOperation?.cancel();
_delayOperation = null;
_cellBloc.close();
super.dispose();
}
@ -72,7 +74,7 @@ class _NumberCellState extends GridFocusNodeCellState<GridNumberCell> {
Future<void> focusChanged() async {
if (mounted) {
_delayOperation?.cancel();
_delayOperation = Timer(const Duration(milliseconds: 300), () {
_delayOperation = Timer(const Duration(milliseconds: 30), () {
if (_cellBloc.isClosed == false &&
_controller.text != contentFromState(_cellBloc.state)) {
_cellBloc.add(NumberCellEvent.updateCell(_controller.text));

View File

@ -73,7 +73,7 @@ class SelectOptionTag extends StatelessWidget {
Key? key,
}) : super(key: key);
factory SelectOptionTag.fromSelectOption({
factory SelectOptionTag.fromOption({
required BuildContext context,
required SelectOptionPB option,
VoidCallback? onSelected,
@ -91,7 +91,8 @@ class SelectOptionTag extends StatelessWidget {
Widget build(BuildContext context) {
return ChoiceChip(
pressElevation: 1,
label: FlowyText.medium(name, fontSize: 12, overflow: TextOverflow.ellipsis),
label:
FlowyText.medium(name, fontSize: 12, overflow: TextOverflow.ellipsis),
selectedColor: color,
backgroundColor: color,
labelPadding: const EdgeInsets.symmetric(horizontal: 6),
@ -133,7 +134,7 @@ class SelectOptionTagCell extends StatelessWidget {
Flexible(
fit: FlexFit.loose,
flex: 2,
child: SelectOptionTag.fromSelectOption(
child: SelectOptionTag.fromOption(
context: context,
option: option,
onSelected: () => onSelected(option),

Some files were not shown because too many files have changed in this diff Show More