mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
Merge branch 'main' into add_translated_titles_304
This commit is contained in:
commit
827e542c6e
3
.github/workflows/ci.yaml
vendored
3
.github/workflows/ci.yaml
vendored
@ -18,7 +18,7 @@ jobs:
|
||||
- os: ubuntu-latest
|
||||
flutter_profile: development-linux-x86
|
||||
- os: macos-latest
|
||||
flutter_profile: development-mac
|
||||
flutter_profile: development-mac-x86_64
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
steps:
|
||||
@ -34,6 +34,7 @@ jobs:
|
||||
with:
|
||||
channel: 'stable'
|
||||
cache: true
|
||||
flutter-version: '3.0.0'
|
||||
|
||||
- name: Cache Cargo
|
||||
uses: actions/cache@v2
|
||||
|
2
.github/workflows/dart_lint.yml
vendored
2
.github/workflows/dart_lint.yml
vendored
@ -23,7 +23,7 @@ jobs:
|
||||
uses: actions/checkout@v2
|
||||
- uses: subosito/flutter-action@v1
|
||||
with:
|
||||
flutter-version: '2.10.0'
|
||||
flutter-version: '3.0.0'
|
||||
channel: "stable"
|
||||
- name: Deps Flutter
|
||||
run: flutter packages pub get
|
||||
|
1
.github/workflows/dart_test.yml
vendored
1
.github/workflows/dart_test.yml
vendored
@ -25,6 +25,7 @@ jobs:
|
||||
- uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: 'stable'
|
||||
flutter-version: '3.0.0'
|
||||
cache: true
|
||||
|
||||
- name: Cache Cargo
|
||||
|
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
@ -50,6 +50,7 @@ jobs:
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: 'stable'
|
||||
flutter-version: '3.0.0'
|
||||
|
||||
- name: Pre build
|
||||
working-directory: frontend
|
||||
@ -98,6 +99,7 @@ jobs:
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: 'stable'
|
||||
flutter-version: '3.0.0'
|
||||
|
||||
- name: Pre build
|
||||
working-directory: frontend
|
||||
@ -111,7 +113,7 @@ jobs:
|
||||
working-directory: frontend
|
||||
run: |
|
||||
flutter config --enable-macos-desktop
|
||||
cargo make --env APP_VERSION=${{ github.ref_name }} --profile production-mac-x86 appflowy
|
||||
cargo make --env APP_VERSION=${{ github.ref_name }} --profile production-mac-x86_64 appflowy
|
||||
|
||||
- name: Archive macOS app
|
||||
working-directory: ${{ env.MACOS_APP_RELEASE_PATH }}
|
||||
|
35
CHANGELOG.md
35
CHANGELOG.md
@ -1,5 +1,40 @@
|
||||
# Release Notes
|
||||
|
||||
## Version 0.0.4 - 2022-06-06
|
||||
- Drag to adjust the width of a column
|
||||
- Upgrade to Flutter 3.0
|
||||
- Native support for M1 chip
|
||||
- Date supports time formats
|
||||
- New property: URL
|
||||
- Keyboard shortcuts support for Grid: press Enter to leave the edit mode; control c/v to copy-paste cell values
|
||||
|
||||
### Bug Fixes
|
||||
- Fixed some bugs
|
||||
|
||||
|
||||
## Version 0.0.4 - beta.3 - 2022-05-02
|
||||
- Drag to reorder app/ view/ field
|
||||
- Row record open as a page
|
||||
- Auto resize the height of the row in the grid
|
||||
- Support more number formats
|
||||
- Search column options, supporting Single select, Multi-select, and number format
|
||||
|
||||
![May-03-2022 10-03-00](https://user-images.githubusercontent.com/86001920/166394640-a8f1f3bc-5f20-4033-93e9-16bc308d7005.gif)
|
||||
|
||||
|
||||
### Bug Fixes & Improvements
|
||||
- Improved row/cell data cache
|
||||
- Fixed some bugs
|
||||
|
||||
|
||||
## Version 0.0.4 - beta.2 - 2022-04-11
|
||||
|
||||
- Support properties: Text, Number, Date, Checkbox, Select, Multi-select
|
||||
- Insert / delete rows
|
||||
- Add / delete / hide columns
|
||||
- Edit property
|
||||
![](https://user-images.githubusercontent.com/12026239/162753644-bf2f4e7a-2367-4d48-87e6-35e244e83a5b.png)
|
||||
|
||||
## Version 0.0.4 - beta.1 - 2022-04-08
|
||||
v0.0.4 - beta.1 is pre-release
|
||||
|
||||
|
48
frontend/.vscode/launch.json
vendored
48
frontend/.vscode/launch.json
vendored
@ -5,40 +5,60 @@
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "app_flowy",
|
||||
// This task builds the Rust and Dart code of AppFlowy.
|
||||
"name": "AF: Build All",
|
||||
"request": "launch",
|
||||
"program": "./lib/main.dart",
|
||||
"type": "dart",
|
||||
"preLaunchTask": "build_flowy_sdk",
|
||||
"preLaunchTask": "AF: build_flowy_sdk",
|
||||
"env":{
|
||||
"RUST_LOG":"info"
|
||||
},
|
||||
"cwd": "${workspaceRoot}/app_flowy"
|
||||
},
|
||||
{
|
||||
"name": "app_flowy(trace)",
|
||||
// This task only builds the Dart code of AppFlowy.
|
||||
"name": "AF: Build Dart Only",
|
||||
"request": "launch",
|
||||
"program": "${workspaceRoot}/lib/main.dart",
|
||||
"type": "dart",
|
||||
"env": {
|
||||
"RUST_LOG": "debug"
|
||||
},
|
||||
"cwd": "${workspaceRoot}"
|
||||
},
|
||||
{
|
||||
// This task builds will:
|
||||
// - call the clean task,
|
||||
// - rebuild all the generated Files (including freeze and language files)
|
||||
// - rebuild the the Rust and Dart code of AppFlowy.
|
||||
"name": "AF: Clean + Rebuild All",
|
||||
"request": "launch",
|
||||
"program": "./lib/main.dart",
|
||||
"type": "dart",
|
||||
"preLaunchTask": "build_flowy_sdk",
|
||||
"preLaunchTask": "AF: Clean + Rebuild All",
|
||||
"env":{
|
||||
"RUST_LOG":"info"
|
||||
},
|
||||
"cwd": "${workspaceRoot}/app_flowy"
|
||||
},
|
||||
|
||||
{
|
||||
"name": "AF: Build All (rustlog: trace)",
|
||||
"request": "launch",
|
||||
"program": "./lib/main.dart",
|
||||
"type": "dart",
|
||||
"preLaunchTask": "AF: build_flowy_sdk",
|
||||
"env":{
|
||||
"RUST_LOG":"trace"
|
||||
},
|
||||
"cwd": "${workspaceRoot}/app_flowy"
|
||||
},
|
||||
{
|
||||
"name": "app_flowy (profile mode)",
|
||||
"name": "AF: app_flowy (profile mode)",
|
||||
"request": "launch",
|
||||
"type": "dart",
|
||||
"flutterMode": "profile"
|
||||
},
|
||||
{
|
||||
"name": "Generate Language Files",
|
||||
"request": "launch",
|
||||
"program": "./lib/main.dart",
|
||||
"type": "dart",
|
||||
"preLaunchTask": "Generate Language Files",
|
||||
"cwd": "${workspaceRoot}/app_flowy/"
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
|
104
frontend/.vscode/tasks.json
vendored
104
frontend/.vscode/tasks.json
vendored
@ -10,13 +10,35 @@
|
||||
// ${cwd}: the current working directory of the spawned process
|
||||
"tasks": [
|
||||
{
|
||||
"label": "build_flowy_sdk",
|
||||
"label": "AF: Clean + Rebuild All",
|
||||
"type": "shell",
|
||||
"dependsOrder": "sequence",
|
||||
"dependsOn": [
|
||||
"AF: Clean",
|
||||
"AF: Flutter Pub",
|
||||
"AF: Flutter Package Get",
|
||||
"AF: Generate Language Files",
|
||||
"AF: Generate Freezed Files",
|
||||
"AF: build_flowy_sdk"
|
||||
],
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true,
|
||||
},
|
||||
"presentation": {
|
||||
"reveal": "always",
|
||||
"panel": "new"
|
||||
}
|
||||
|
||||
},
|
||||
{
|
||||
"label": "AF: build_flowy_sdk",
|
||||
"type": "shell",
|
||||
"command": "sh ./scripts/build_sdk.sh",
|
||||
"windows": {
|
||||
"options": {
|
||||
"env": {
|
||||
"FLOWY_DEV_ENV": "Windows",
|
||||
"FLOWY_DEV_ENV": "Windows"
|
||||
},
|
||||
"shell": {
|
||||
"executable": "cmd.exe",
|
||||
@ -31,27 +53,67 @@
|
||||
"linux": {
|
||||
"options": {
|
||||
"env": {
|
||||
"FLOWY_DEV_ENV": "Linux-x86",
|
||||
"FLOWY_DEV_ENV": "Linux-x86"
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
"osx": {
|
||||
"options": {
|
||||
"env": {
|
||||
"FLOWY_DEV_ENV": "macOS",
|
||||
"FLOWY_DEV_ENV": "macOS"
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
"group": "build",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}"
|
||||
},
|
||||
// "problemMatcher": [
|
||||
// "$rustc"
|
||||
// ],
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Generate Language Files",
|
||||
"label": "AF: Code Gen",
|
||||
"type": "shell",
|
||||
"dependsOrder": "sequence",
|
||||
"dependsOn": [
|
||||
"AF: Flutter Pub",
|
||||
"AF: Flutter Package Get",
|
||||
"AF: Generate Language Files",
|
||||
"AF: Generate Freezed Files"
|
||||
],
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true,
|
||||
},
|
||||
"presentation": {
|
||||
"reveal": "always",
|
||||
"panel": "new"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "AF: Flutter Pub",
|
||||
"type": "shell",
|
||||
"command": "flutter pub get",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/app_flowy"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "AF: Flutter Package Get",
|
||||
"type": "shell",
|
||||
"command": "flutter packages pub get",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/app_flowy"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "AF: Generate Freezed Files",
|
||||
"type": "shell",
|
||||
"command": "flutter pub run build_runner build --delete-conflicting-outputs",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/app_flowy"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "AF: Generate Language Files",
|
||||
"type": "shell",
|
||||
"command": "sh ./scripts/generate_language_files.sh",
|
||||
"windows": {
|
||||
@ -69,10 +131,10 @@
|
||||
"group": "build",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}"
|
||||
},
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Clean",
|
||||
"label": "AF: Clean",
|
||||
"type": "shell",
|
||||
"command": "sh ./scripts/clean.sh",
|
||||
"windows": {
|
||||
@ -90,7 +152,19 @@
|
||||
"group": "build",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}"
|
||||
},
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "AF: flutter build aar",
|
||||
"type": "flutter",
|
||||
"command": "flutter",
|
||||
"args": [
|
||||
"build",
|
||||
"aar"
|
||||
],
|
||||
"group": "build",
|
||||
"problemMatcher": [],
|
||||
"detail": "app_flowy"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ extend = [
|
||||
{ path = "scripts/makefile/docker.toml" },
|
||||
{ path = "scripts/makefile/env.toml" },
|
||||
{ path = "scripts/makefile/flutter.toml" },
|
||||
{ path = "scripts/makefile/tool.toml" },
|
||||
]
|
||||
|
||||
[config]
|
||||
@ -44,7 +45,15 @@ APP_ENVIRONMENT = "local"
|
||||
FLUTTER_FLOWY_SDK_PATH="app_flowy/packages/flowy_sdk"
|
||||
PROTOBUF_DERIVE_CACHE="../shared-lib/flowy-derive/src/derive_cache/derive_cache.rs"
|
||||
|
||||
[env.development-mac]
|
||||
[env.development-mac-arm64]
|
||||
RUST_LOG = "info"
|
||||
TARGET_OS = "macos"
|
||||
RUST_COMPILE_TARGET = "aarch64-apple-darwin"
|
||||
BUILD_FLAG = "debug"
|
||||
FLUTTER_OUTPUT_DIR = "Debug"
|
||||
PRODUCT_EXT = "app"
|
||||
|
||||
[env.development-mac-x86_64]
|
||||
RUST_LOG = "info"
|
||||
TARGET_OS = "macos"
|
||||
RUST_COMPILE_TARGET = "x86_64-apple-darwin"
|
||||
@ -52,21 +61,23 @@ BUILD_FLAG = "debug"
|
||||
FLUTTER_OUTPUT_DIR = "Debug"
|
||||
PRODUCT_EXT = "app"
|
||||
|
||||
[env.production-mac-aarch64]
|
||||
[env.production-mac-arm64]
|
||||
BUILD_FLAG = "release"
|
||||
TARGET_OS = "macos"
|
||||
RUST_COMPILE_TARGET = "aarch64-apple-darwin"
|
||||
FLUTTER_OUTPUT_DIR = "Release"
|
||||
PRODUCT_EXT = "app"
|
||||
APP_ENVIRONMENT = "production"
|
||||
BUILD_ARCHS = "arm64"
|
||||
|
||||
[env.production-mac-x86]
|
||||
[env.production-mac-x86_64]
|
||||
BUILD_FLAG = "release"
|
||||
TARGET_OS = "macos"
|
||||
RUST_COMPILE_TARGET = "x86_64-apple-darwin"
|
||||
FLUTTER_OUTPUT_DIR = "Release"
|
||||
PRODUCT_EXT = "app"
|
||||
APP_ENVIRONMENT = "production"
|
||||
BUILD_ARCHS = "x86_64"
|
||||
|
||||
[env.development-windows-x86]
|
||||
TARGET_OS = "windows"
|
||||
@ -137,6 +148,7 @@ script = [
|
||||
echo PRODUCT_EXT: ${PRODUCT_EXT}
|
||||
echo APP_ENVIRONMENT: ${APP_ENVIRONMENT}
|
||||
echo ${platforms}
|
||||
echo ${BUILD_ARCHS}
|
||||
'''
|
||||
]
|
||||
script_runner = "@shell"
|
||||
|
36
frontend/app_flowy/.vscode/launch.json
vendored
36
frontend/app_flowy/.vscode/launch.json
vendored
@ -1,36 +0,0 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "app_flowy",
|
||||
"request": "launch",
|
||||
"program": "${workspaceRoot}/lib/main.dart",
|
||||
"type": "dart",
|
||||
"preLaunchTask": "build_flowy_sdk",
|
||||
"env": {
|
||||
"RUST_LOG": "debug"
|
||||
},
|
||||
"cwd": "${workspaceRoot}"
|
||||
},
|
||||
{
|
||||
"name": "app_flowy(trace)",
|
||||
"request": "launch",
|
||||
"program": "${workspaceRoot}/lib/main.dart",
|
||||
"type": "dart",
|
||||
"preLaunchTask": "build_flowy_sdk",
|
||||
"env": {
|
||||
"RUST_LOG": "trace"
|
||||
},
|
||||
"cwd": "${workspaceRoot}"
|
||||
},
|
||||
{
|
||||
"name": "app_flowy (profile mode)",
|
||||
"request": "launch",
|
||||
"type": "dart",
|
||||
"flutterMode": "profile"
|
||||
},
|
||||
]
|
||||
}
|
26
frontend/app_flowy/.vscode/settings.json
vendored
26
frontend/app_flowy/.vscode/settings.json
vendored
@ -1,26 +0,0 @@
|
||||
{
|
||||
"[dart]": {
|
||||
"editor.formatOnSave": true,
|
||||
"editor.formatOnType": true,
|
||||
"editor.rulers": [
|
||||
120
|
||||
],
|
||||
"editor.selectionHighlight": false,
|
||||
"editor.suggest.snippetsPreventQuickSuggestions": false,
|
||||
"editor.suggestSelection": "first",
|
||||
"editor.tabCompletion": "onlySnippets",
|
||||
"editor.wordBasedSuggestions": false
|
||||
},
|
||||
"svgviewer.enableautopreview": true,
|
||||
"svgviewer.previewcolumn": "Active",
|
||||
"svgviewer.showzoominout": true,
|
||||
"editor.wordWrapColumn": 120,
|
||||
"editor.minimap.maxColumn": 140,
|
||||
"prettier.printWidth": 140,
|
||||
"editor.wordWrap": "wordWrapColumn",
|
||||
"dart.lineLength": 120,
|
||||
"files.associations": {
|
||||
"*.log.*": "log"
|
||||
},
|
||||
"editor.formatOnSave": true,
|
||||
}
|
129
frontend/app_flowy/.vscode/tasks.json
vendored
129
frontend/app_flowy/.vscode/tasks.json
vendored
@ -1,129 +0,0 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
// https://code.visualstudio.com/docs/editor/tasks
|
||||
// https://gist.github.com/deadalusai/9e13e36d61ec7fb72148
|
||||
// ${workspaceRoot}: the root folder of the team
|
||||
// ${file}: the current opened file
|
||||
// ${fileBasename}: the current opened file's basename
|
||||
// ${fileDirname}: the current opened file's dirname
|
||||
// ${fileExtname}: the current opened file's extension
|
||||
// ${cwd}: the current working directory of the spawned process
|
||||
"tasks": [
|
||||
{
|
||||
"label": "build_flowy_sdk",
|
||||
"type": "shell",
|
||||
"command": "sh ./scripts/build_sdk.sh",
|
||||
"windows": {
|
||||
"options": {
|
||||
"env": {
|
||||
"FLOWY_DEV_ENV": "Windows",
|
||||
},
|
||||
"shell": {
|
||||
"executable": "cmd.exe",
|
||||
"args": [
|
||||
"/d",
|
||||
"/c",
|
||||
".\\scripts\\build_sdk.cmd"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"linux": {
|
||||
"options": {
|
||||
"env": {
|
||||
"FLOWY_DEV_ENV": "Linux-x86",
|
||||
}
|
||||
},
|
||||
},
|
||||
"osx": {
|
||||
"options": {
|
||||
"env": {
|
||||
"FLOWY_DEV_ENV": "macOS",
|
||||
}
|
||||
},
|
||||
},
|
||||
"group": "build",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/../"
|
||||
},
|
||||
// "problemMatcher": [
|
||||
// "$rustc"
|
||||
// ],
|
||||
},
|
||||
{
|
||||
"label": "Code Gen",
|
||||
"type": "shell",
|
||||
"dependsOn": [
|
||||
"Flutter Pub",
|
||||
"Flutter Package Get",
|
||||
"Generate Language Files",
|
||||
"Generate Freezed Files"
|
||||
],
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true,
|
||||
},
|
||||
"dependsOrder": "sequence",
|
||||
"presentation": {
|
||||
"reveal": "always",
|
||||
"panel": "new"
|
||||
},
|
||||
},
|
||||
{
|
||||
"label": "Flutter Pub",
|
||||
"type": "shell",
|
||||
"command": "flutter pub get",
|
||||
},
|
||||
{
|
||||
"label": "Flutter Package Get",
|
||||
"type": "shell",
|
||||
"command": "flutter packages pub get",
|
||||
},
|
||||
{
|
||||
"label": "Generate Freezed Files",
|
||||
"type": "shell",
|
||||
"command": "flutter pub run build_runner build --delete-conflicting-outputs",
|
||||
},
|
||||
{
|
||||
"label": "Generate Language Files",
|
||||
"type": "shell",
|
||||
"command": "sh ./scripts/generate_language_files.sh",
|
||||
"windows": {
|
||||
"options": {
|
||||
"shell": {
|
||||
"executable": "cmd.exe",
|
||||
"args": [
|
||||
"/d",
|
||||
"/c",
|
||||
".\\scripts\\generate_language_files.cmd"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"group": "build",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/../"
|
||||
},
|
||||
},
|
||||
{
|
||||
"label": "Clean",
|
||||
"type": "shell",
|
||||
"command": "sh ./scripts/clean.sh",
|
||||
"windows": {
|
||||
"options": {
|
||||
"shell": {
|
||||
"executable": "cmd.exe",
|
||||
"args": [
|
||||
"/d",
|
||||
"/c",
|
||||
".\\scripts\\clean.cmd"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/../"
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
3
frontend/app_flowy/assets/images/grid/field/url.svg
Normal file
3
frontend/app_flowy/assets/images/grid/field/url.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M13 7.688L8.27223 12.1469C7.69304 12.6931 6.90749 13 6.0884 13C5.26931 13 4.48376 12.6931 3.90457 12.1469C3.32538 11.6006 3 10.8598 3 10.0873C3 9.31474 3.32538 8.57387 3.90457 8.02763L8.63234 3.56875C9.01847 3.20459 9.54216 3 10.0882 3C10.6343 3 11.158 3.20459 11.5441 3.56875C11.9302 3.93291 12.1472 4.42683 12.1472 4.94183C12.1472 5.45684 11.9302 5.95075 11.5441 6.31491L6.8112 10.7738C6.61814 10.9559 6.35629 11.0582 6.08326 11.0582C5.81022 11.0582 5.54838 10.9559 5.35531 10.7738C5.16225 10.5917 5.05379 10.3448 5.05379 10.0873C5.05379 9.82975 5.16225 9.58279 5.35531 9.40071L9.72297 5.28632" stroke="#333333" stroke-width="0.9989" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
After Width: | Height: | Size: 797 B |
@ -96,6 +96,12 @@
|
||||
"lightMode": "Switch to Light mode",
|
||||
"darkMode": "Switch to Dark mode"
|
||||
},
|
||||
"notifications": {
|
||||
"export": {
|
||||
"markdown": "Exported Note To Markdown",
|
||||
"path": "Documents/flowy"
|
||||
}
|
||||
},
|
||||
"contactsPage": {
|
||||
"title": "Contacts",
|
||||
"whatsHappening": "What's happening this week?",
|
||||
@ -160,6 +166,7 @@
|
||||
"numberFieldName": "Numbers",
|
||||
"singleSelectFieldName": "Select",
|
||||
"multiSelectFieldName": "Multiselect",
|
||||
"urlFieldName": "URL",
|
||||
"numberFormat": " Number format",
|
||||
"dateFormat": " Date format",
|
||||
"includeTime": " Include time",
|
||||
@ -168,6 +175,7 @@
|
||||
"dateFormatLocal": "Month/Month/Day",
|
||||
"dateFormatUS": "Month/Month/Day",
|
||||
"timeFormat": " Time format",
|
||||
"invalidTimeFormat": "Invalid format",
|
||||
"timeFormatTwelveHour": "12 hour",
|
||||
"timeFormatTwentyFourHour": "24 hour",
|
||||
"addSelectOption": "Add an option",
|
||||
@ -178,7 +186,8 @@
|
||||
"row": {
|
||||
"duplicate": "Duplicate",
|
||||
"delete": "Delete",
|
||||
"textPlaceholder": "Empty"
|
||||
"textPlaceholder": "Empty",
|
||||
"copyProperty": "Copied property to clipboard"
|
||||
},
|
||||
"selectOption": {
|
||||
"create": "Create",
|
||||
@ -200,5 +209,9 @@
|
||||
},
|
||||
"document":{
|
||||
"menuName":"Doc"
|
||||
"date": {
|
||||
"timeHintTextInTwelveHour": "12:00 AM",
|
||||
"timeHintTextInTwentyFourHour": "12:00"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,11 +7,11 @@
|
||||
"letsGoButtonText": "Vamos lá",
|
||||
"title": "Título",
|
||||
"signUp": {
|
||||
"buttonText": "Inscreve-se",
|
||||
"title": "Inscrever-se @:appName",
|
||||
"buttonText": "Se inscreva",
|
||||
"title": "Se inscreva no @:appName",
|
||||
"getStartedText": "Começar",
|
||||
"emptyPasswordError": "Senha não pode ser em branco.",
|
||||
"repeatPasswordEmptyError": "Confirmar a senha não pode ser em branco.",
|
||||
"emptyPasswordError": "Senha não pode estar em branco.",
|
||||
"repeatPasswordEmptyError": "Confirmar a senha não pode estar em branco.",
|
||||
"unmatchedPasswordError": "As senhas não conferem.",
|
||||
"alreadyHaveAnAccount": "Já possui uma conta?",
|
||||
"emailHint": "Email",
|
||||
@ -19,14 +19,14 @@
|
||||
"repeatPasswordHint": "Confirme a senha"
|
||||
},
|
||||
"signIn": {
|
||||
"loginTitle": "Login to @:appName",
|
||||
"loginTitle": "Entre no @:appName",
|
||||
"loginButtonText": "Login",
|
||||
"buttonText": "Entre",
|
||||
"forgotPassword": "Esqueceu a senha?",
|
||||
"emailHint": "Email",
|
||||
"passwordHint": "Senha",
|
||||
"dontHaveAnAccount": "Não possui uma conta?",
|
||||
"repeatPasswordEmptyError": "Confirmar a senha não pode ser em branco.",
|
||||
"repeatPasswordEmptyError": "Confirmar a senha não pode estar em branco.",
|
||||
"unmatchedPasswordError": "As senhas não conferem."
|
||||
},
|
||||
"workspace": {
|
||||
@ -67,7 +67,7 @@
|
||||
"whatsNew": "O que há de novo?",
|
||||
"help": "Ajuda & Suporte",
|
||||
"debug": {
|
||||
"name": "Informação de debug",
|
||||
"name": "Informação de depuração",
|
||||
"success": "Copiar informação de debug para o clipboard!",
|
||||
"fail": "Falha em copiar a informação de debug para o clipboard"
|
||||
}
|
||||
@ -104,7 +104,7 @@
|
||||
},
|
||||
"button": {
|
||||
"OK": "OK",
|
||||
"Cancel": "Canelar",
|
||||
"Cancel": "Cancelar",
|
||||
"signIn": "Entrar",
|
||||
"signOut": "Sair",
|
||||
"complete": "Completar",
|
||||
@ -143,4 +143,5 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
146
frontend/app_flowy/assets/translations/pt-PT.json
Normal file
146
frontend/app_flowy/assets/translations/pt-PT.json
Normal file
@ -0,0 +1,146 @@
|
||||
{
|
||||
"appName": "AppFlowy",
|
||||
"defaultUsername": "Me",
|
||||
"welcomeText": "Bem vindo ao @:appName",
|
||||
"githubStarText": "Star on GitHub",
|
||||
"subscribeNewsletterText": "Inscreve-te ao Newsletter",
|
||||
"letsGoButtonText": "Bora",
|
||||
"title": "Título",
|
||||
"signUp": {
|
||||
"buttonText": "Inscreve-te",
|
||||
"title": "Inscreve-te ao @:appName",
|
||||
"getStartedText": "Começar",
|
||||
"emptyPasswordError": "A palavra-passe não pode estar em branco.",
|
||||
"repeatPasswordEmptyError": "Confirmar a palavra-passe não pode estar em branco.",
|
||||
"unmatchedPasswordError": "As palavras-passes não coincidem.",
|
||||
"alreadyHaveAnAccount": "Já possuis uma conta?",
|
||||
"emailHint": "Email",
|
||||
"passwordHint": "Password",
|
||||
"repeatPasswordHint": "Confirma a tua password"
|
||||
},
|
||||
"signIn": {
|
||||
"loginTitle": "Entre no @:appName",
|
||||
"loginButtonText": "Login",
|
||||
"buttonText": "Entre",
|
||||
"forgotPassword": "Esqueceste-te da tua palavra-passe?",
|
||||
"emailHint": "Email",
|
||||
"passwordHint": "Palavra-passe",
|
||||
"dontHaveAnAccount": "Não possuis uma conta?",
|
||||
"repeatPasswordEmptyError": "Confirmar a palavra-passe não pode estar em branco.",
|
||||
"unmatchedPasswordError": "As palavras-passes não conferem."
|
||||
},
|
||||
"workspace": {
|
||||
"create": "Cria um ambiente de trabalho",
|
||||
"hint": "ambiente de trabalho",
|
||||
"notFoundError": "Ambiente de trabalho não encontrada"
|
||||
},
|
||||
"shareAction": {
|
||||
"buttonText": "Partilhar",
|
||||
"workInProgress": "Em breve",
|
||||
"markdown": "Markdown",
|
||||
"copyLink": "Copiar o link"
|
||||
},
|
||||
"disclosureAction": {
|
||||
"rename": "Renomear",
|
||||
"delete": "Apagar",
|
||||
"duplicate": "Duplicar"
|
||||
},
|
||||
"blankPageTitle": "Página em branco",
|
||||
"newPageText": "Nova página",
|
||||
"trash": {
|
||||
"text": "Lixo",
|
||||
"restoreAll": "Restaurar todos",
|
||||
"deleteAll": "Apagar todos",
|
||||
"pageHeader": {
|
||||
"fileName": "Nome do ficheiro",
|
||||
"lastModified": "Última modificação",
|
||||
"created": "Criado"
|
||||
}
|
||||
},
|
||||
"deletePagePrompt": {
|
||||
"text": "Esta página está no lixo",
|
||||
"restore": "Restaurar a página",
|
||||
"deletePermanent": "Apagar permanentemente"
|
||||
},
|
||||
"dialogCreatePageNameHint": "Nome da página",
|
||||
"questionBubble": {
|
||||
"whatsNew": "O que há de novo?",
|
||||
"help": "Ajuda & Suporte",
|
||||
"debug": {
|
||||
"name": "Informação de depuração",
|
||||
"success": "Copiar informação de depuração para o clipboard!",
|
||||
"fail": "Falha em copiar a informação de depuração para o clipboard"
|
||||
}
|
||||
},
|
||||
"menuAppHeader": {
|
||||
"addPageTooltip": "Adiciona uma nova página.",
|
||||
"defaultNewPageName": "Sem título",
|
||||
"renameDialog": "Renomear"
|
||||
},
|
||||
"toolbar": {
|
||||
"undo": "Desfazer",
|
||||
"redo": "Refazer",
|
||||
"bold": "Negrito",
|
||||
"italic": "Itálico",
|
||||
"underline": "Sublinhado",
|
||||
"strike": "Riscado",
|
||||
"numList": "Lista numerada",
|
||||
"bulletList": "Lista com marcadores",
|
||||
"checkList": "Lista de verificação",
|
||||
"inlineCode": "Embutir código",
|
||||
"quote": "Citação em bloco",
|
||||
"header": "Cabeçalho",
|
||||
"highlight": "Realçar"
|
||||
},
|
||||
"tooltip": {
|
||||
"lightMode": "Mudar para o modo Claro.",
|
||||
"darkMode": "Mudar para o modo Escuro."
|
||||
},
|
||||
"contactsPage": {
|
||||
"title": "Conctatos",
|
||||
"whatsHappening": "O que está a acontecer nesta semana?",
|
||||
"addContact": "Adicionar um conctato",
|
||||
"editContact": "Editar um conctato"
|
||||
},
|
||||
"button": {
|
||||
"OK": "OK",
|
||||
"Cancel": "Cancelar",
|
||||
"signIn": "Entrar",
|
||||
"signOut": "Sair",
|
||||
"complete": "Completar",
|
||||
"save": "Guardar"
|
||||
},
|
||||
"label": {
|
||||
"welcome": "Bem vindo!",
|
||||
"firstName": "Nome",
|
||||
"middleName": "Nome do Meio",
|
||||
"lastName": "Apelido",
|
||||
"stepX": "Passo {X}"
|
||||
},
|
||||
"oAuth": {
|
||||
"err": {
|
||||
"failedTitle": "Erro ao conectar à sua conta.",
|
||||
"failedMsg": "Verifica se concluiste o processo de login no teu navegador."
|
||||
},
|
||||
"google": {
|
||||
"title": "GOOGLE SIGN-IN",
|
||||
"instruction1": "Para importar os teus Conctatos do Google, tens de autorizar esta aplicação usando o teu navegador web.",
|
||||
"instruction2": "Copia este código para a tua área de transferências clicando no ícone ou selecionando o texto:",
|
||||
"instruction3": "Navega até o link a seguir no seu navegador e digite o código acima:",
|
||||
"instruction4": "Clica no botão abaixo ao concluir a inscrição:"
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"title": "Definições",
|
||||
"menu": {
|
||||
"appearance": "Aparência",
|
||||
"language": "Idioma",
|
||||
"open": "Abrir as Definições"
|
||||
},
|
||||
"appearance": {
|
||||
"lightLabel": "Modo Claro",
|
||||
"darkLabel": "Modo Escuro"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -141,6 +141,68 @@
|
||||
"lightLabel": "Светлая тема",
|
||||
"darkLabel": "Тёмная тема"
|
||||
}
|
||||
},
|
||||
"grid": {
|
||||
"settings": {
|
||||
"filter": "Фильтр",
|
||||
"sortBy": "Сортировать",
|
||||
"Properties": "Свойства"
|
||||
},
|
||||
"field": {
|
||||
"hide": "Скрыть",
|
||||
"insertLeft": "Вставить слева",
|
||||
"insertRight": "Вставить справа",
|
||||
"duplicate": "Дублировать",
|
||||
"delete": "Удалить",
|
||||
"textFieldName": "Текст",
|
||||
"checkboxFieldName": "Checkbox",
|
||||
"dateFieldName": "Дата",
|
||||
"numberFieldName": "Число",
|
||||
"singleSelectFieldName": "Выбор",
|
||||
"multiSelectFieldName": "Выбор многих",
|
||||
"urlFieldName": "URL",
|
||||
"numberFormat": " Формат числа",
|
||||
"dateFormat": " Формат даты",
|
||||
"includeTime": " Время",
|
||||
"dateFormatFriendly": "День Месяц, Год",
|
||||
"dateFormatISO": "Год-Месяц-День",
|
||||
"dateFormatLocal": "Год/Месяц/День",
|
||||
"dateFormatUS": "Год/Месяц/День",
|
||||
"timeFormat": " Форматировать время",
|
||||
"invalidTimeFormat": "Неверный формат",
|
||||
"timeFormatTwelveHour": "12 часов",
|
||||
"timeFormatTwentyFourHour": "24 часа",
|
||||
"addSelectOption": "Добавить вариант",
|
||||
"optionTitle": "Варианты",
|
||||
"addOption": "Добавить",
|
||||
"editProperty": "Редактировать свойство"
|
||||
},
|
||||
"row": {
|
||||
"duplicate": "Дублировать",
|
||||
"delete": "Удалить",
|
||||
"textPlaceholder": "Пусто",
|
||||
"copyProperty": "Свойство скопировано"
|
||||
},
|
||||
"selectOption": {
|
||||
"create": "Создать",
|
||||
"purpleColor": "Фиолетовый",
|
||||
"pinkColor": "Розовый",
|
||||
"lightPinkColor": "Светло-розовый",
|
||||
"orangeColor": "Оранжевый",
|
||||
"yellowColor": "Желтый",
|
||||
"limeColor": "Ярко-зелёный",
|
||||
"greenColor": "Зелёный",
|
||||
"aquaColor": "Морской волны",
|
||||
"blueColor": "Синий",
|
||||
"deleteTag": "Удалить вариант",
|
||||
"colorPannelTitle": "Цвета",
|
||||
"pannelTitle": "Выберите или создайте вариант",
|
||||
"searchOption": "Поиск"
|
||||
},
|
||||
"date": {
|
||||
"timeHintTextInTwelveHour": "12:00 AM",
|
||||
"timeHintTextInTwentyFourHour": "12:00"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
67
frontend/app_flowy/lib/core/frameless_window.dart
Normal file
67
frontend/app_flowy/lib/core/frameless_window.dart
Normal file
@ -0,0 +1,67 @@
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'dart:io' show Platform;
|
||||
|
||||
class CocoaWindowChannel {
|
||||
CocoaWindowChannel._();
|
||||
|
||||
final MethodChannel _channel = const MethodChannel("flutter/cocoaWindow");
|
||||
|
||||
static final CocoaWindowChannel instance = CocoaWindowChannel._();
|
||||
|
||||
Future<void> setWindowPosition(Offset offset) async {
|
||||
await _channel.invokeMethod("setWindowPosition", [offset.dx, offset.dy]);
|
||||
}
|
||||
|
||||
Future<List<double>> getWindowPosition() async {
|
||||
final raw = await _channel.invokeMethod("getWindowPosition");
|
||||
final arr = raw as List<dynamic>;
|
||||
final List<double> result = arr.map((s) => s as double).toList();
|
||||
return result;
|
||||
}
|
||||
|
||||
Future<void> zoom() async {
|
||||
await _channel.invokeMethod("zoom");
|
||||
}
|
||||
}
|
||||
|
||||
class MoveWindowDetector extends StatefulWidget {
|
||||
const MoveWindowDetector({Key? key, this.child}) : super(key: key);
|
||||
|
||||
final Widget? child;
|
||||
|
||||
@override
|
||||
_MoveWindowDetectorState createState() => _MoveWindowDetectorState();
|
||||
}
|
||||
|
||||
class _MoveWindowDetectorState extends State<MoveWindowDetector> {
|
||||
double winX = 0;
|
||||
double winY = 0;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!Platform.isMacOS) {
|
||||
return widget.child ?? Container();
|
||||
}
|
||||
return GestureDetector(
|
||||
// https://stackoverflow.com/questions/52965799/flutter-gesturedetector-not-working-with-containers-in-stack
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onDoubleTap: () async {
|
||||
await CocoaWindowChannel.instance.zoom();
|
||||
},
|
||||
onPanStart: (DragStartDetails details) {
|
||||
winX = details.globalPosition.dx;
|
||||
winY = details.globalPosition.dy;
|
||||
},
|
||||
onPanUpdate: (DragUpdateDetails details) async {
|
||||
final windowPos = await CocoaWindowChannel.instance.getWindowPosition();
|
||||
final double dx = windowPos[0];
|
||||
final double dy = windowPos[1];
|
||||
final deltaX = details.globalPosition.dx - winX;
|
||||
final deltaY = details.globalPosition.dy - winY;
|
||||
await CocoaWindowChannel.instance.setWindowPosition(Offset(dx + deltaX, dy - deltaY));
|
||||
},
|
||||
child: widget.child,
|
||||
);
|
||||
}
|
||||
}
|
@ -15,10 +15,8 @@ import 'package:app_flowy/workspace/presentation/home/home_stack.dart';
|
||||
import 'package:app_flowy/workspace/presentation/home/menu/menu.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-folder-data-model/app.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-folder-data-model/view.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid-data-model/grid.pb.dart' show EditFieldContext;
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/date_type_option.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/number_type_option.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-user-data-model/user_profile.pb.dart';
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
|
||||
class DependencyResolver {
|
||||
@ -49,6 +47,8 @@ void _resolveUserDeps(GetIt getIt) {
|
||||
}
|
||||
|
||||
void _resolveHomeDeps(GetIt getIt) {
|
||||
getIt.registerSingleton(FToast());
|
||||
|
||||
getIt.registerSingleton(MenuSharedState());
|
||||
|
||||
getIt.registerFactoryParam<UserListener, UserProfile, void>(
|
||||
@ -157,21 +157,14 @@ void _resolveGridDeps(GetIt getIt) {
|
||||
),
|
||||
);
|
||||
|
||||
getIt.registerFactoryParam<FieldEditorBloc, String, EditFieldContextLoader>(
|
||||
(gridId, fieldLoader) => FieldEditorBloc(
|
||||
gridId: gridId,
|
||||
fieldLoader: fieldLoader,
|
||||
),
|
||||
);
|
||||
|
||||
getIt.registerFactoryParam<TextCellBloc, GridCellContext, void>(
|
||||
(context, _) => TextCellBloc(
|
||||
cellContext: context,
|
||||
),
|
||||
);
|
||||
|
||||
getIt.registerFactoryParam<SelectionCellBloc, GridSelectOptionCellContext, void>(
|
||||
(context, _) => SelectionCellBloc(
|
||||
getIt.registerFactoryParam<SelectOptionCellBloc, GridSelectOptionCellContext, void>(
|
||||
(context, _) => SelectOptionCellBloc(
|
||||
cellContext: context,
|
||||
),
|
||||
);
|
||||
@ -195,18 +188,6 @@ void _resolveGridDeps(GetIt getIt) {
|
||||
),
|
||||
);
|
||||
|
||||
getIt.registerFactoryParam<FieldEditorPannelBloc, EditFieldContext, void>(
|
||||
(context, _) => FieldEditorPannelBloc(context),
|
||||
);
|
||||
|
||||
getIt.registerFactoryParam<DateTypeOptionBloc, DateTypeOption, void>(
|
||||
(typeOption, _) => DateTypeOptionBloc(typeOption: typeOption),
|
||||
);
|
||||
|
||||
getIt.registerFactoryParam<NumberTypeOptionBloc, NumberTypeOption, void>(
|
||||
(typeOption, _) => NumberTypeOptionBloc(typeOption: typeOption),
|
||||
);
|
||||
|
||||
getIt.registerFactoryParam<GridPropertyBloc, String, GridFieldCache>(
|
||||
(gridId, cache) => GridPropertyBloc(gridId: gridId, fieldCache: cache),
|
||||
);
|
||||
|
@ -67,40 +67,42 @@ class ApplicationWidget extends StatelessWidget {
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => ChangeNotifierProvider.value(
|
||||
value: settingModel,
|
||||
builder: (context, _) {
|
||||
const ratio = 1.73;
|
||||
const minWidth = 600.0;
|
||||
setWindowMinSize(const Size(minWidth, minWidth / ratio));
|
||||
settingModel.readLocaleWhenAppLaunch(context);
|
||||
AppTheme theme = context.select<AppearanceSettingModel, AppTheme>(
|
||||
(value) => value.theme,
|
||||
);
|
||||
Locale locale = context.select<AppearanceSettingModel, Locale>(
|
||||
(value) => value.locale,
|
||||
);
|
||||
Widget build(BuildContext context) {
|
||||
return ChangeNotifierProvider.value(
|
||||
value: settingModel,
|
||||
builder: (context, _) {
|
||||
const ratio = 1.73;
|
||||
const minWidth = 600.0;
|
||||
setWindowMinSize(const Size(minWidth, minWidth / ratio));
|
||||
settingModel.readLocaleWhenAppLaunch(context);
|
||||
AppTheme theme = context.select<AppearanceSettingModel, AppTheme>(
|
||||
(value) => value.theme,
|
||||
);
|
||||
Locale locale = context.select<AppearanceSettingModel, Locale>(
|
||||
(value) => value.locale,
|
||||
);
|
||||
|
||||
return MultiProvider(
|
||||
providers: [
|
||||
Provider.value(value: theme),
|
||||
Provider.value(value: locale),
|
||||
],
|
||||
builder: (context, _) {
|
||||
return MaterialApp(
|
||||
builder: overlayManagerBuilder(),
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: theme.themeData,
|
||||
localizationsDelegates: context.localizationDelegates,
|
||||
supportedLocales: context.supportedLocales,
|
||||
locale: locale,
|
||||
navigatorKey: AppGlobals.rootNavKey,
|
||||
home: child,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
return MultiProvider(
|
||||
providers: [
|
||||
Provider.value(value: theme),
|
||||
Provider.value(value: locale),
|
||||
],
|
||||
builder: (context, _) {
|
||||
return MaterialApp(
|
||||
builder: overlayManagerBuilder(),
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: theme.themeData,
|
||||
localizationsDelegates: context.localizationDelegates,
|
||||
supportedLocales: context.supportedLocales,
|
||||
locale: locale,
|
||||
navigatorKey: AppGlobals.rootNavKey,
|
||||
home: child,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AppGlobals {
|
||||
|
@ -88,8 +88,9 @@ class _SkipLogInScreenState extends State<SkipLogInScreen> {
|
||||
}
|
||||
|
||||
_launchURL(String url) async {
|
||||
if (await canLaunch(url)) {
|
||||
await launch(url);
|
||||
final uri = Uri.parse(url);
|
||||
if (await canLaunchUrl(uri)) {
|
||||
await launchUrl(uri);
|
||||
} else {
|
||||
throw 'Could not launch $url';
|
||||
}
|
||||
|
@ -1,3 +1,5 @@
|
||||
import 'dart:collection';
|
||||
|
||||
import 'package:app_flowy/plugin/plugin.dart';
|
||||
import 'package:app_flowy/startup/startup.dart';
|
||||
import 'package:app_flowy/workspace/application/app/app_listener.dart';
|
||||
|
@ -12,14 +12,14 @@ class DocumentService {
|
||||
await FolderEventSetLatestView(ViewId(value: docId)).send();
|
||||
|
||||
final payload = TextBlockId(value: docId);
|
||||
return BlockEventGetBlockData(payload).send();
|
||||
return TextBlockEventGetBlockData(payload).send();
|
||||
}
|
||||
|
||||
Future<Either<TextBlockDelta, FlowyError>> composeDelta({required String docId, required String data}) {
|
||||
final payload = TextBlockDelta.create()
|
||||
..blockId = docId
|
||||
..deltaStr = data;
|
||||
return BlockEventApplyDelta(payload).send();
|
||||
return TextBlockEventApplyDelta(payload).send();
|
||||
}
|
||||
|
||||
Future<Either<Unit, FlowyError>> closeDocument({required String docId}) {
|
||||
|
@ -1,3 +1,6 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'package:app_flowy/startup/tasks/rust_sdk.dart';
|
||||
import 'package:app_flowy/workspace/application/doc/share_service.dart';
|
||||
import 'package:app_flowy/workspace/application/markdown/delta_markdown.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-text-block/entities.pb.dart';
|
||||
@ -33,8 +36,30 @@ class DocShareBloc extends Bloc<DocShareEvent, DocShareState> {
|
||||
ExportData _convertDeltaToMarkdown(ExportData value) {
|
||||
final result = deltaToMarkdown(value.data);
|
||||
value.data = result;
|
||||
writeFile(result);
|
||||
return value;
|
||||
}
|
||||
|
||||
Future<Directory> get _exportDir async {
|
||||
Directory documentsDir = await appFlowyDocumentDirectory();
|
||||
|
||||
return documentsDir;
|
||||
}
|
||||
|
||||
Future<String> get _localPath async {
|
||||
final dir = await _exportDir;
|
||||
return dir.path;
|
||||
}
|
||||
|
||||
Future<File> get _localFile async {
|
||||
final path = await _localPath;
|
||||
return File('$path/${view.name}.md');
|
||||
}
|
||||
|
||||
Future<File> writeFile(String md) async {
|
||||
final file = await _localFile;
|
||||
return file.writeAsString(md);
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
|
@ -10,7 +10,7 @@ class ShareService {
|
||||
..viewId = docId
|
||||
..exportType = type;
|
||||
|
||||
return BlockEventExportDocument(request).send();
|
||||
return TextBlockEventExportDocument(request).send();
|
||||
}
|
||||
|
||||
Future<Either<ExportData, FlowyError>> exportText(String docId) {
|
||||
|
@ -10,13 +10,13 @@ import 'package:flowy_sdk/protobuf/flowy-grid-data-model/grid.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/cell_entities.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/date_type_option.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/selection_type_option.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/url_type_option.pb.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
import 'package:app_flowy/workspace/application/grid/cell/cell_listener.dart';
|
||||
import 'package:app_flowy/workspace/application/grid/cell/select_option_service.dart';
|
||||
import 'package:app_flowy/workspace/application/grid/field/field_service.dart';
|
||||
|
||||
import 'dart:convert' show utf8;
|
||||
part 'cell_service.freezed.dart';
|
||||
part 'data_loader.dart';
|
||||
part 'context_builder.dart';
|
||||
|
@ -1,8 +1,9 @@
|
||||
part of 'cell_service.dart';
|
||||
|
||||
typedef GridCellContext = _GridCellContext<Cell, String>;
|
||||
typedef GridCellContext = _GridCellContext<String, String>;
|
||||
typedef GridSelectOptionCellContext = _GridCellContext<SelectOptionCellData, String>;
|
||||
typedef GridDateCellContext = _GridCellContext<DateCellData, DateCalData>;
|
||||
typedef GridDateCellContext = _GridCellContext<DateCellData, CalendarData>;
|
||||
typedef GridURLCellContext = _GridCellContext<URLCellData, String>;
|
||||
|
||||
class GridCellContextBuilder {
|
||||
final GridCellCache _cellCache;
|
||||
@ -16,61 +17,100 @@ class GridCellContextBuilder {
|
||||
_GridCellContext build() {
|
||||
switch (_gridCell.field.fieldType) {
|
||||
case FieldType.Checkbox:
|
||||
final cellDataLoader = GridCellDataLoader(
|
||||
gridCell: _gridCell,
|
||||
parser: StringCellDataParser(),
|
||||
);
|
||||
return GridCellContext(
|
||||
gridCell: _gridCell,
|
||||
cellCache: _cellCache,
|
||||
cellDataLoader: CellDataLoader(gridCell: _gridCell),
|
||||
cellDataLoader: cellDataLoader,
|
||||
cellDataPersistence: CellDataPersistence(gridCell: _gridCell),
|
||||
);
|
||||
case FieldType.DateTime:
|
||||
final cellDataLoader = GridCellDataLoader(
|
||||
gridCell: _gridCell,
|
||||
parser: DateCellDataParser(),
|
||||
config: const GridCellDataConfig(reloadOnFieldChanged: true),
|
||||
);
|
||||
|
||||
return GridDateCellContext(
|
||||
gridCell: _gridCell,
|
||||
cellCache: _cellCache,
|
||||
cellDataLoader: DateCellDataLoader(gridCell: _gridCell),
|
||||
cellDataLoader: cellDataLoader,
|
||||
cellDataPersistence: DateCellDataPersistence(gridCell: _gridCell),
|
||||
);
|
||||
case FieldType.Number:
|
||||
final cellDataLoader = GridCellDataLoader(
|
||||
gridCell: _gridCell,
|
||||
parser: StringCellDataParser(),
|
||||
config: const GridCellDataConfig(reloadOnCellChanged: true, reloadOnFieldChanged: true),
|
||||
);
|
||||
return GridCellContext(
|
||||
gridCell: _gridCell,
|
||||
cellCache: _cellCache,
|
||||
cellDataLoader: CellDataLoader(gridCell: _gridCell, reloadOnCellChanged: true),
|
||||
cellDataLoader: cellDataLoader,
|
||||
cellDataPersistence: CellDataPersistence(gridCell: _gridCell),
|
||||
);
|
||||
case FieldType.RichText:
|
||||
final cellDataLoader = GridCellDataLoader(
|
||||
gridCell: _gridCell,
|
||||
parser: StringCellDataParser(),
|
||||
);
|
||||
return GridCellContext(
|
||||
gridCell: _gridCell,
|
||||
cellCache: _cellCache,
|
||||
cellDataLoader: CellDataLoader(gridCell: _gridCell),
|
||||
cellDataLoader: cellDataLoader,
|
||||
cellDataPersistence: CellDataPersistence(gridCell: _gridCell),
|
||||
);
|
||||
case FieldType.MultiSelect:
|
||||
case FieldType.SingleSelect:
|
||||
final cellDataLoader = GridCellDataLoader(
|
||||
gridCell: _gridCell,
|
||||
parser: SelectOptionCellDataParser(),
|
||||
config: const GridCellDataConfig(reloadOnFieldChanged: true),
|
||||
);
|
||||
|
||||
return GridSelectOptionCellContext(
|
||||
gridCell: _gridCell,
|
||||
cellCache: _cellCache,
|
||||
cellDataLoader: SelectOptionCellDataLoader(gridCell: _gridCell),
|
||||
cellDataLoader: cellDataLoader,
|
||||
cellDataPersistence: CellDataPersistence(gridCell: _gridCell),
|
||||
);
|
||||
|
||||
case FieldType.URL:
|
||||
final cellDataLoader = GridCellDataLoader(
|
||||
gridCell: _gridCell,
|
||||
parser: URLCellDataParser(),
|
||||
);
|
||||
return GridURLCellContext(
|
||||
gridCell: _gridCell,
|
||||
cellCache: _cellCache,
|
||||
cellDataLoader: cellDataLoader,
|
||||
cellDataPersistence: CellDataPersistence(gridCell: _gridCell),
|
||||
);
|
||||
default:
|
||||
throw UnimplementedError;
|
||||
}
|
||||
throw UnimplementedError;
|
||||
}
|
||||
}
|
||||
|
||||
// T: the type of the CellData
|
||||
// D: the type of the data that will be save to disk
|
||||
// ignore: must_be_immutable
|
||||
class _GridCellContext<T, D> extends Equatable {
|
||||
final GridCell gridCell;
|
||||
final GridCellCache cellCache;
|
||||
final GridCellCacheKey _cacheKey;
|
||||
final _GridCellDataLoader<T> cellDataLoader;
|
||||
final IGridCellDataLoader<T> cellDataLoader;
|
||||
final _GridCellDataPersistence<D> cellDataPersistence;
|
||||
final FieldService _fieldService;
|
||||
|
||||
late final CellListener _cellListener;
|
||||
late final ValueNotifier<T?> _cellDataNotifier;
|
||||
late final ValueNotifier<T?>? _cellDataNotifier;
|
||||
bool isListening = false;
|
||||
VoidCallback? _onFieldChangedFn;
|
||||
Timer? _delayOperation;
|
||||
Timer? _loadDataOperation;
|
||||
Timer? _saveDataOperation;
|
||||
|
||||
_GridCellContext({
|
||||
required this.gridCell,
|
||||
@ -100,7 +140,7 @@ class _GridCellContext<T, D> extends Equatable {
|
||||
|
||||
FieldType get fieldType => gridCell.field.fieldType;
|
||||
|
||||
VoidCallback? startListening({required void Function(T) onCellChanged}) {
|
||||
VoidCallback? startListening({required void Function(T?) onCellChanged}) {
|
||||
if (isListening) {
|
||||
Log.error("Already started. It seems like you should call clone first");
|
||||
return null;
|
||||
@ -124,52 +164,64 @@ class _GridCellContext<T, D> extends Equatable {
|
||||
}
|
||||
|
||||
onCellChangedFn() {
|
||||
final value = _cellDataNotifier.value;
|
||||
if (value is T) {
|
||||
onCellChanged(value);
|
||||
}
|
||||
onCellChanged(_cellDataNotifier?.value);
|
||||
|
||||
if (cellDataLoader.config.reloadOnCellChanged) {
|
||||
_loadData();
|
||||
}
|
||||
}
|
||||
|
||||
_cellDataNotifier.addListener(onCellChangedFn);
|
||||
_cellDataNotifier?.addListener(onCellChangedFn);
|
||||
return onCellChangedFn;
|
||||
}
|
||||
|
||||
void removeListener(VoidCallback fn) {
|
||||
_cellDataNotifier.removeListener(fn);
|
||||
_cellDataNotifier?.removeListener(fn);
|
||||
}
|
||||
|
||||
T? getCellData() {
|
||||
T? getCellData({bool loadIfNoCache = true}) {
|
||||
final data = cellCache.get(_cacheKey);
|
||||
if (data == null) {
|
||||
if (data == null && loadIfNoCache) {
|
||||
_loadData();
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
Future<Either<List<int>, FlowyError>> getTypeOptionData() {
|
||||
return _fieldService.getTypeOptionData(fieldType: fieldType);
|
||||
Future<Either<FieldTypeOptionData, FlowyError>> getTypeOptionData() {
|
||||
return _fieldService.getFieldTypeOptionData(fieldType: fieldType);
|
||||
}
|
||||
|
||||
Future<Option<FlowyError>> saveCellData(D data) {
|
||||
return cellDataPersistence.save(data);
|
||||
void saveCellData(D data, {bool deduplicate = false, void Function(Option<FlowyError>)? resultCallback}) async {
|
||||
if (deduplicate) {
|
||||
_loadDataOperation?.cancel();
|
||||
_loadDataOperation = Timer(const Duration(milliseconds: 300), () async {
|
||||
final result = await cellDataPersistence.save(data);
|
||||
if (resultCallback != null) {
|
||||
resultCallback(result);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
final result = await cellDataPersistence.save(data);
|
||||
if (resultCallback != null) {
|
||||
resultCallback(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _loadData() {
|
||||
_delayOperation?.cancel();
|
||||
_delayOperation = Timer(const Duration(milliseconds: 10), () {
|
||||
_loadDataOperation?.cancel();
|
||||
_loadDataOperation = Timer(const Duration(milliseconds: 10), () {
|
||||
cellDataLoader.loadData().then((data) {
|
||||
_cellDataNotifier.value = data;
|
||||
_cellDataNotifier?.value = data;
|
||||
cellCache.insert(GridCellCacheData(key: _cacheKey, object: data));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
_delayOperation?.cancel();
|
||||
_cellListener.stop();
|
||||
_loadDataOperation?.cancel();
|
||||
_saveDataOperation?.cancel();
|
||||
|
||||
if (_onFieldChangedFn != null) {
|
||||
cellCache.removeFieldListener(_cacheKey, _onFieldChangedFn!);
|
||||
|
@ -104,6 +104,8 @@ class GridCellCache {
|
||||
}
|
||||
|
||||
Future<void> dispose() async {
|
||||
_fieldListenerByFieldId.clear();
|
||||
_cellDataByFieldId.clear();
|
||||
fieldDelegate.dispose();
|
||||
}
|
||||
}
|
||||
|
@ -1,94 +1,78 @@
|
||||
part of 'cell_service.dart';
|
||||
|
||||
abstract class GridCellDataConfig {
|
||||
abstract class IGridCellDataConfig {
|
||||
// The cell data will reload if it receives the field's change notification.
|
||||
bool get reloadOnFieldChanged;
|
||||
|
||||
// The cell data will reload if it receives the cell's change notification.
|
||||
// For example, the number cell should be reloaded after user input the number.
|
||||
// When the reloadOnCellChanged is true, it will load the cell data after user input.
|
||||
// For example: The number cell reload the cell data that carries the format
|
||||
// user input: 12
|
||||
// cell display: $12
|
||||
bool get reloadOnCellChanged;
|
||||
}
|
||||
|
||||
class DefaultCellDataConfig implements GridCellDataConfig {
|
||||
class GridCellDataConfig implements IGridCellDataConfig {
|
||||
@override
|
||||
final bool reloadOnCellChanged;
|
||||
|
||||
@override
|
||||
final bool reloadOnFieldChanged;
|
||||
|
||||
DefaultCellDataConfig({
|
||||
const GridCellDataConfig({
|
||||
this.reloadOnCellChanged = false,
|
||||
this.reloadOnFieldChanged = false,
|
||||
});
|
||||
}
|
||||
|
||||
abstract class _GridCellDataLoader<T> {
|
||||
abstract class IGridCellDataLoader<T> {
|
||||
Future<T?> loadData();
|
||||
|
||||
GridCellDataConfig get config;
|
||||
IGridCellDataConfig get config;
|
||||
}
|
||||
|
||||
class CellDataLoader extends _GridCellDataLoader<Cell> {
|
||||
abstract class ICellDataParser<T> {
|
||||
T? parserData(List<int> data);
|
||||
}
|
||||
|
||||
class GridCellDataLoader<T> extends IGridCellDataLoader<T> {
|
||||
final CellService service = CellService();
|
||||
final GridCell gridCell;
|
||||
final GridCellDataConfig _config;
|
||||
|
||||
CellDataLoader({
|
||||
required this.gridCell,
|
||||
bool reloadOnCellChanged = false,
|
||||
}) : _config = DefaultCellDataConfig(reloadOnCellChanged: reloadOnCellChanged);
|
||||
final ICellDataParser<T> parser;
|
||||
|
||||
@override
|
||||
Future<Cell?> loadData() {
|
||||
final IGridCellDataConfig config;
|
||||
|
||||
GridCellDataLoader({
|
||||
required this.gridCell,
|
||||
required this.parser,
|
||||
this.config = const GridCellDataConfig(),
|
||||
});
|
||||
|
||||
@override
|
||||
Future<T?> loadData() {
|
||||
final fut = service.getCell(
|
||||
gridId: gridCell.gridId,
|
||||
fieldId: gridCell.field.id,
|
||||
rowId: gridCell.rowId,
|
||||
);
|
||||
return fut.then((result) {
|
||||
return result.fold((data) => data, (err) {
|
||||
return fut.then(
|
||||
(result) => result.fold((Cell cell) {
|
||||
try {
|
||||
return parser.parserData(cell.data);
|
||||
} catch (e, s) {
|
||||
Log.error('$parser parser cellData failed, $e');
|
||||
Log.error('Stack trace \n $s');
|
||||
return null;
|
||||
}
|
||||
}, (err) {
|
||||
Log.error(err);
|
||||
return null;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
GridCellDataConfig get config => _config;
|
||||
}
|
||||
|
||||
class DateCellDataLoader extends _GridCellDataLoader<DateCellData> {
|
||||
final GridCell gridCell;
|
||||
final GridCellDataConfig _config;
|
||||
DateCellDataLoader({
|
||||
required this.gridCell,
|
||||
}) : _config = DefaultCellDataConfig(reloadOnFieldChanged: true);
|
||||
|
||||
@override
|
||||
GridCellDataConfig get config => _config;
|
||||
|
||||
@override
|
||||
Future<DateCellData?> loadData() {
|
||||
final payload = CellIdentifierPayload.create()
|
||||
..gridId = gridCell.gridId
|
||||
..fieldId = gridCell.field.id
|
||||
..rowId = gridCell.rowId;
|
||||
|
||||
return GridEventGetDateCellData(payload).send().then((result) {
|
||||
return result.fold(
|
||||
(data) => data,
|
||||
(err) {
|
||||
Log.error(err);
|
||||
return null;
|
||||
},
|
||||
);
|
||||
});
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SelectOptionCellDataLoader extends _GridCellDataLoader<SelectOptionCellData> {
|
||||
class SelectOptionCellDataLoader extends IGridCellDataLoader<SelectOptionCellData> {
|
||||
final SelectOptionService service;
|
||||
final GridCell gridCell;
|
||||
SelectOptionCellDataLoader({
|
||||
@ -108,5 +92,43 @@ class SelectOptionCellDataLoader extends _GridCellDataLoader<SelectOptionCellDat
|
||||
}
|
||||
|
||||
@override
|
||||
GridCellDataConfig get config => DefaultCellDataConfig();
|
||||
IGridCellDataConfig get config => const GridCellDataConfig(reloadOnFieldChanged: true);
|
||||
}
|
||||
|
||||
class StringCellDataParser implements ICellDataParser<String> {
|
||||
@override
|
||||
String? parserData(List<int> data) {
|
||||
final s = utf8.decode(data);
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
class DateCellDataParser implements ICellDataParser<DateCellData> {
|
||||
@override
|
||||
DateCellData? parserData(List<int> data) {
|
||||
if (data.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
return DateCellData.fromBuffer(data);
|
||||
}
|
||||
}
|
||||
|
||||
class SelectOptionCellDataParser implements ICellDataParser<SelectOptionCellData> {
|
||||
@override
|
||||
SelectOptionCellData? parserData(List<int> data) {
|
||||
if (data.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
return SelectOptionCellData.fromBuffer(data);
|
||||
}
|
||||
}
|
||||
|
||||
class URLCellDataParser implements ICellDataParser<URLCellData> {
|
||||
@override
|
||||
URLCellData? parserData(List<int> data) {
|
||||
if (data.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
return URLCellData.fromBuffer(data);
|
||||
}
|
||||
}
|
||||
|
@ -31,18 +31,18 @@ class CellDataPersistence implements _GridCellDataPersistence<String> {
|
||||
}
|
||||
|
||||
@freezed
|
||||
class DateCalData with _$DateCalData {
|
||||
const factory DateCalData({required DateTime date, String? time}) = _DateCellPersistenceData;
|
||||
class CalendarData with _$CalendarData {
|
||||
const factory CalendarData({required DateTime date, String? time}) = _CalendarData;
|
||||
}
|
||||
|
||||
class DateCellDataPersistence implements _GridCellDataPersistence<DateCalData> {
|
||||
class DateCellDataPersistence implements _GridCellDataPersistence<CalendarData> {
|
||||
final GridCell gridCell;
|
||||
DateCellDataPersistence({
|
||||
required this.gridCell,
|
||||
});
|
||||
|
||||
@override
|
||||
Future<Option<FlowyError>> save(DateCalData data) {
|
||||
Future<Option<FlowyError>> save(CalendarData data) {
|
||||
var payload = DateChangesetPayload.create()..cellIdentifier = _cellIdentifier(gridCell);
|
||||
|
||||
final date = (data.date.millisecondsSinceEpoch ~/ 1000).toString();
|
||||
|
@ -1,4 +1,3 @@
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid-data-model/grid.pb.dart' show Cell;
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'dart:async';
|
||||
@ -16,15 +15,15 @@ class CheckboxCellBloc extends Bloc<CheckboxCellEvent, CheckboxCellState> {
|
||||
}) : super(CheckboxCellState.initial(cellContext)) {
|
||||
on<CheckboxCellEvent>(
|
||||
(event, emit) async {
|
||||
await event.map(
|
||||
initial: (_Initial value) {
|
||||
await event.when(
|
||||
initial: () {
|
||||
_startListening();
|
||||
},
|
||||
select: (_Selected value) async {
|
||||
select: () async {
|
||||
_updateCellData();
|
||||
},
|
||||
didReceiveCellUpdate: (_DidReceiveCellUpdate value) {
|
||||
emit(state.copyWith(isSelected: _isSelected(value.cell)));
|
||||
didReceiveCellUpdate: (cellData) {
|
||||
emit(state.copyWith(isSelected: _isSelected(cellData)));
|
||||
},
|
||||
);
|
||||
},
|
||||
@ -43,9 +42,9 @@ class CheckboxCellBloc extends Bloc<CheckboxCellEvent, CheckboxCellState> {
|
||||
}
|
||||
|
||||
void _startListening() {
|
||||
_onCellChangedFn = cellContext.startListening(onCellChanged: ((cell) {
|
||||
_onCellChangedFn = cellContext.startListening(onCellChanged: ((cellData) {
|
||||
if (!isClosed) {
|
||||
add(CheckboxCellEvent.didReceiveCellUpdate(cell));
|
||||
add(CheckboxCellEvent.didReceiveCellUpdate(cellData));
|
||||
}
|
||||
}));
|
||||
}
|
||||
@ -59,7 +58,7 @@ class CheckboxCellBloc extends Bloc<CheckboxCellEvent, CheckboxCellState> {
|
||||
class CheckboxCellEvent with _$CheckboxCellEvent {
|
||||
const factory CheckboxCellEvent.initial() = _Initial;
|
||||
const factory CheckboxCellEvent.select() = _Selected;
|
||||
const factory CheckboxCellEvent.didReceiveCellUpdate(Cell cell) = _DidReceiveCellUpdate;
|
||||
const factory CheckboxCellEvent.didReceiveCellUpdate(String? cellData) = _DidReceiveCellUpdate;
|
||||
}
|
||||
|
||||
@freezed
|
||||
@ -73,7 +72,6 @@ class CheckboxCellState with _$CheckboxCellState {
|
||||
}
|
||||
}
|
||||
|
||||
bool _isSelected(Cell? cell) {
|
||||
final content = cell?.content ?? "";
|
||||
return content == "Yes";
|
||||
bool _isSelected(String? cellData) {
|
||||
return cellData == "Yes";
|
||||
}
|
||||
|
@ -1,6 +1,9 @@
|
||||
import 'package:app_flowy/generated/locale_keys.g.dart';
|
||||
import 'package:app_flowy/workspace/application/grid/field/field_service.dart';
|
||||
import 'package:easy_localization/easy_localization.dart' show StringTranslateExtension;
|
||||
import 'package:flowy_sdk/log.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-error-code/code.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/date_type_option.pb.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
@ -34,10 +37,10 @@ class DateCalBloc extends Bloc<DateCalEvent, DateCalState> {
|
||||
setFocusedDay: (focusedDay) {
|
||||
emit(state.copyWith(focusedDay: focusedDay));
|
||||
},
|
||||
didReceiveCellUpdate: (DateCellData cellData) {
|
||||
final dateData = dateDataFromCellData(cellData);
|
||||
final time = dateData.foldRight("", (dateData, previous) => dateData.time);
|
||||
emit(state.copyWith(dateData: dateData, time: time));
|
||||
didReceiveCellUpdate: (DateCellData? cellData) {
|
||||
final calData = calDataFromCellData(cellData);
|
||||
final time = calData.foldRight("", (dateData, previous) => dateData.time);
|
||||
emit(state.copyWith(calData: calData, time: time));
|
||||
},
|
||||
setIncludeTime: (includeTime) async {
|
||||
await _updateTypeOption(emit, includeTime: includeTime);
|
||||
@ -49,7 +52,12 @@ class DateCalBloc extends Bloc<DateCalEvent, DateCalState> {
|
||||
await _updateTypeOption(emit, timeFormat: timeFormat);
|
||||
},
|
||||
setTime: (time) async {
|
||||
await _updateDateData(emit, time: time);
|
||||
if (state.calData.isSome()) {
|
||||
await _updateDateData(emit, time: time);
|
||||
}
|
||||
},
|
||||
didUpdateCalData: (Option<CalendarData> data, Option<String> timeFormatError) {
|
||||
emit(state.copyWith(calData: data, timeFormatError: timeFormatError));
|
||||
},
|
||||
);
|
||||
},
|
||||
@ -57,8 +65,8 @@ class DateCalBloc extends Bloc<DateCalEvent, DateCalState> {
|
||||
}
|
||||
|
||||
Future<void> _updateDateData(Emitter<DateCalState> emit, {DateTime? date, String? time}) {
|
||||
final DateCalData newDateData = state.dateData.fold(
|
||||
() => DateCalData(date: date ?? DateTime.now(), time: time),
|
||||
final CalendarData newDateData = state.calData.fold(
|
||||
() => CalendarData(date: date ?? DateTime.now(), time: time),
|
||||
(dateData) {
|
||||
var newDateData = dateData;
|
||||
if (date != null && !isSameDay(newDateData.date, date)) {
|
||||
@ -75,30 +83,44 @@ class DateCalBloc extends Bloc<DateCalEvent, DateCalState> {
|
||||
return _saveDateData(emit, newDateData);
|
||||
}
|
||||
|
||||
Future<void> _saveDateData(Emitter<DateCalState> emit, DateCalData newDateData) async {
|
||||
if (state.dateData == Some(newDateData)) {
|
||||
Future<void> _saveDateData(Emitter<DateCalState> emit, CalendarData newCalData) async {
|
||||
if (state.calData == Some(newCalData)) {
|
||||
return;
|
||||
}
|
||||
|
||||
final result = await cellContext.saveCellData(newDateData);
|
||||
result.fold(
|
||||
() => emit(state.copyWith(
|
||||
dateData: Some(newDateData),
|
||||
timeFormatError: none(),
|
||||
)),
|
||||
(err) {
|
||||
switch (ErrorCode.valueOf(err.code)!) {
|
||||
case ErrorCode.InvalidDateTimeFormat:
|
||||
emit(state.copyWith(
|
||||
dateData: Some(newDateData),
|
||||
timeFormatError: Some(err.toString()),
|
||||
));
|
||||
break;
|
||||
default:
|
||||
Log.error(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
updateCalData(Option<CalendarData> calData, Option<String> timeFormatError) {
|
||||
if (!isClosed) add(DateCalEvent.didUpdateCalData(calData, timeFormatError));
|
||||
}
|
||||
|
||||
cellContext.saveCellData(newCalData, resultCallback: (result) {
|
||||
result.fold(
|
||||
() => updateCalData(Some(newCalData), none()),
|
||||
(err) {
|
||||
switch (ErrorCode.valueOf(err.code)!) {
|
||||
case ErrorCode.InvalidDateTimeFormat:
|
||||
updateCalData(none(), Some(timeFormatPrompt(err)));
|
||||
break;
|
||||
default:
|
||||
Log.error(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
String timeFormatPrompt(FlowyError error) {
|
||||
String msg = LocaleKeys.grid_field_invalidTimeFormat.tr() + ". ";
|
||||
switch (state.dateTypeOption.timeFormat) {
|
||||
case TimeFormat.TwelveHour:
|
||||
msg = msg + "e.g. 01: 00 AM";
|
||||
break;
|
||||
case TimeFormat.TwentyFourHour:
|
||||
msg = msg + "e.g. 13: 00";
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return msg;
|
||||
}
|
||||
|
||||
@override
|
||||
@ -149,7 +171,7 @@ class DateCalBloc extends Bloc<DateCalEvent, DateCalState> {
|
||||
);
|
||||
|
||||
result.fold(
|
||||
(l) => emit(state.copyWith(dateTypeOption: newDateTypeOption)),
|
||||
(l) => emit(state.copyWith(dateTypeOption: newDateTypeOption, timeHintText: _timeHintText(newDateTypeOption))),
|
||||
(err) => Log.error(err),
|
||||
);
|
||||
}
|
||||
@ -165,7 +187,9 @@ class DateCalEvent with _$DateCalEvent {
|
||||
const factory DateCalEvent.setDateFormat(DateFormat dateFormat) = _DateFormat;
|
||||
const factory DateCalEvent.setIncludeTime(bool includeTime) = _IncludeTime;
|
||||
const factory DateCalEvent.setTime(String time) = _Time;
|
||||
const factory DateCalEvent.didReceiveCellUpdate(DateCellData data) = _DidReceiveCellUpdate;
|
||||
const factory DateCalEvent.didReceiveCellUpdate(DateCellData? data) = _DidReceiveCellUpdate;
|
||||
const factory DateCalEvent.didUpdateCalData(Option<CalendarData> data, Option<String> timeFormatError) =
|
||||
_DidUpdateCalData;
|
||||
}
|
||||
|
||||
@freezed
|
||||
@ -175,36 +199,48 @@ class DateCalState with _$DateCalState {
|
||||
required CalendarFormat format,
|
||||
required DateTime focusedDay,
|
||||
required Option<String> timeFormatError,
|
||||
required Option<DateCalData> dateData,
|
||||
required Option<CalendarData> calData,
|
||||
required String? time,
|
||||
required String timeHintText,
|
||||
}) = _DateCalState;
|
||||
|
||||
factory DateCalState.initial(
|
||||
DateTypeOption dateTypeOption,
|
||||
DateCellData? cellData,
|
||||
) {
|
||||
Option<DateCalData> dateData = dateDataFromCellData(cellData);
|
||||
final time = dateData.foldRight("", (dateData, previous) => dateData.time);
|
||||
Option<CalendarData> calData = calDataFromCellData(cellData);
|
||||
final time = calData.foldRight("", (dateData, previous) => dateData.time);
|
||||
return DateCalState(
|
||||
dateTypeOption: dateTypeOption,
|
||||
format: CalendarFormat.month,
|
||||
focusedDay: DateTime.now(),
|
||||
time: time,
|
||||
dateData: dateData,
|
||||
calData: calData,
|
||||
timeFormatError: none(),
|
||||
timeHintText: _timeHintText(dateTypeOption),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Option<DateCalData> dateDataFromCellData(DateCellData? cellData) {
|
||||
String _timeHintText(DateTypeOption typeOption) {
|
||||
switch (typeOption.timeFormat) {
|
||||
case TimeFormat.TwelveHour:
|
||||
return LocaleKeys.grid_date_timeHintTextInTwelveHour.tr();
|
||||
case TimeFormat.TwentyFourHour:
|
||||
return LocaleKeys.grid_date_timeHintTextInTwentyFourHour.tr();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
Option<CalendarData> calDataFromCellData(DateCellData? cellData) {
|
||||
String? time = timeFromCellData(cellData);
|
||||
Option<DateCalData> dateData = none();
|
||||
Option<CalendarData> calData = none();
|
||||
if (cellData != null) {
|
||||
final timestamp = cellData.timestamp * 1000;
|
||||
final date = DateTime.fromMillisecondsSinceEpoch(timestamp.toInt());
|
||||
dateData = Some(DateCalData(date: date, time: time));
|
||||
calData = Some(CalendarData(date: date, time: time));
|
||||
}
|
||||
return dateData;
|
||||
return calData;
|
||||
}
|
||||
|
||||
$fixnum.Int64 timestampFromDateTime(DateTime dateTime) {
|
||||
|
@ -4,7 +4,6 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'dart:async';
|
||||
import 'cell_service/cell_service.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
part 'date_cell_bloc.freezed.dart';
|
||||
|
||||
class DateCellBloc extends Bloc<DateCellEvent, DateCellState> {
|
||||
@ -16,7 +15,9 @@ class DateCellBloc extends Bloc<DateCellEvent, DateCellState> {
|
||||
(event, emit) async {
|
||||
event.when(
|
||||
initial: () => _startListening(),
|
||||
didReceiveCellUpdate: (DateCellData value) => emit(state.copyWith(data: Some(value))),
|
||||
didReceiveCellUpdate: (DateCellData? cellData) {
|
||||
emit(state.copyWith(data: cellData, dateStr: _dateStrFromCellData(cellData)));
|
||||
},
|
||||
didReceiveFieldUpdate: (Field value) => emit(state.copyWith(field: value)),
|
||||
);
|
||||
},
|
||||
@ -47,28 +48,33 @@ class DateCellBloc extends Bloc<DateCellEvent, DateCellState> {
|
||||
@freezed
|
||||
class DateCellEvent with _$DateCellEvent {
|
||||
const factory DateCellEvent.initial() = _InitialCell;
|
||||
const factory DateCellEvent.didReceiveCellUpdate(DateCellData data) = _DidReceiveCellUpdate;
|
||||
const factory DateCellEvent.didReceiveCellUpdate(DateCellData? data) = _DidReceiveCellUpdate;
|
||||
const factory DateCellEvent.didReceiveFieldUpdate(Field field) = _DidReceiveFieldUpdate;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class DateCellState with _$DateCellState {
|
||||
const factory DateCellState({
|
||||
required Option<DateCellData> data,
|
||||
required DateCellData? data,
|
||||
required String dateStr,
|
||||
required Field field,
|
||||
}) = _DateCellState;
|
||||
|
||||
factory DateCellState.initial(GridDateCellContext context) {
|
||||
final cellData = context.getCellData();
|
||||
Option<DateCellData> data = none();
|
||||
|
||||
if (cellData != null) {
|
||||
data = Some(cellData);
|
||||
}
|
||||
|
||||
return DateCellState(
|
||||
field: context.field,
|
||||
data: data,
|
||||
data: cellData,
|
||||
dateStr: _dateStrFromCellData(cellData),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _dateStrFromCellData(DateCellData? cellData) {
|
||||
String dateStr = "";
|
||||
if (cellData != null) {
|
||||
dateStr = cellData.date + " " + cellData.time;
|
||||
}
|
||||
return dateStr;
|
||||
}
|
||||
|
@ -1,7 +1,8 @@
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid-data-model/grid.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'dart:async';
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'cell_service/cell_service.dart';
|
||||
|
||||
part 'number_cell_bloc.freezed.dart';
|
||||
@ -15,25 +16,28 @@ class NumberCellBloc extends Bloc<NumberCellEvent, NumberCellState> {
|
||||
}) : super(NumberCellState.initial(cellContext)) {
|
||||
on<NumberCellEvent>(
|
||||
(event, emit) async {
|
||||
await event.map(
|
||||
initial: (_Initial value) async {
|
||||
event.when(
|
||||
initial: () {
|
||||
_startListening();
|
||||
},
|
||||
didReceiveCellUpdate: (_DidReceiveCellUpdate value) {
|
||||
emit(state.copyWith(content: value.cell.content));
|
||||
didReceiveCellUpdate: (content) {
|
||||
emit(state.copyWith(content: content));
|
||||
},
|
||||
updateCell: (_UpdateCell value) async {
|
||||
await _updateCellValue(value, emit);
|
||||
updateCell: (text) {
|
||||
cellContext.saveCellData(text, resultCallback: (result) {
|
||||
result.fold(
|
||||
() => null,
|
||||
(err) {
|
||||
if (!isClosed) add(NumberCellEvent.didReceiveCellUpdate(right(err)));
|
||||
},
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _updateCellValue(_UpdateCell value, Emitter<NumberCellState> emit) async {
|
||||
cellContext.saveCellData(value.text);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
if (_onCellChangedFn != null) {
|
||||
@ -46,9 +50,9 @@ class NumberCellBloc extends Bloc<NumberCellEvent, NumberCellState> {
|
||||
|
||||
void _startListening() {
|
||||
_onCellChangedFn = cellContext.startListening(
|
||||
onCellChanged: ((cell) {
|
||||
onCellChanged: ((cellContent) {
|
||||
if (!isClosed) {
|
||||
add(NumberCellEvent.didReceiveCellUpdate(cell));
|
||||
add(NumberCellEvent.didReceiveCellUpdate(left(cellContent ?? "")));
|
||||
}
|
||||
}),
|
||||
);
|
||||
@ -59,17 +63,19 @@ 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(Cell cell) = _DidReceiveCellUpdate;
|
||||
const factory NumberCellEvent.didReceiveCellUpdate(Either<String, FlowyError> cellContent) = _DidReceiveCellUpdate;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class NumberCellState with _$NumberCellState {
|
||||
const factory NumberCellState({
|
||||
required String content,
|
||||
required Either<String, FlowyError> content,
|
||||
}) = _NumberCellState;
|
||||
|
||||
factory NumberCellState.initial(GridCellContext context) {
|
||||
final cell = context.getCellData();
|
||||
return NumberCellState(content: cell?.content ?? "");
|
||||
final cellContent = context.getCellData() ?? "";
|
||||
return NumberCellState(
|
||||
content: left(cellContent),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -4,16 +4,16 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:app_flowy/workspace/application/grid/cell/cell_service/cell_service.dart';
|
||||
|
||||
part 'selection_cell_bloc.freezed.dart';
|
||||
part 'select_option_cell_bloc.freezed.dart';
|
||||
|
||||
class SelectionCellBloc extends Bloc<SelectionCellEvent, SelectionCellState> {
|
||||
class SelectOptionCellBloc extends Bloc<SelectOptionCellEvent, SelectOptionCellState> {
|
||||
final GridSelectOptionCellContext cellContext;
|
||||
void Function()? _onCellChangedFn;
|
||||
|
||||
SelectionCellBloc({
|
||||
SelectOptionCellBloc({
|
||||
required this.cellContext,
|
||||
}) : super(SelectionCellState.initial(cellContext)) {
|
||||
on<SelectionCellEvent>(
|
||||
}) : super(SelectOptionCellState.initial(cellContext)) {
|
||||
on<SelectOptionCellEvent>(
|
||||
(event, emit) async {
|
||||
await event.map(
|
||||
initial: (_InitialCell value) async {
|
||||
@ -21,7 +21,6 @@ class SelectionCellBloc extends Bloc<SelectionCellEvent, SelectionCellState> {
|
||||
},
|
||||
didReceiveOptions: (_DidReceiveOptions value) {
|
||||
emit(state.copyWith(
|
||||
options: value.options,
|
||||
selectedOptions: value.selectedOptions,
|
||||
));
|
||||
},
|
||||
@ -44,9 +43,8 @@ class SelectionCellBloc extends Bloc<SelectionCellEvent, SelectionCellState> {
|
||||
_onCellChangedFn = cellContext.startListening(
|
||||
onCellChanged: ((selectOptionContext) {
|
||||
if (!isClosed) {
|
||||
add(SelectionCellEvent.didReceiveOptions(
|
||||
selectOptionContext.options,
|
||||
selectOptionContext.selectOptions,
|
||||
add(SelectOptionCellEvent.didReceiveOptions(
|
||||
selectOptionContext?.selectOptions ?? [],
|
||||
));
|
||||
}
|
||||
}),
|
||||
@ -55,26 +53,23 @@ class SelectionCellBloc extends Bloc<SelectionCellEvent, SelectionCellState> {
|
||||
}
|
||||
|
||||
@freezed
|
||||
class SelectionCellEvent with _$SelectionCellEvent {
|
||||
const factory SelectionCellEvent.initial() = _InitialCell;
|
||||
const factory SelectionCellEvent.didReceiveOptions(
|
||||
List<SelectOption> options,
|
||||
class SelectOptionCellEvent with _$SelectOptionCellEvent {
|
||||
const factory SelectOptionCellEvent.initial() = _InitialCell;
|
||||
const factory SelectOptionCellEvent.didReceiveOptions(
|
||||
List<SelectOption> selectedOptions,
|
||||
) = _DidReceiveOptions;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class SelectionCellState with _$SelectionCellState {
|
||||
const factory SelectionCellState({
|
||||
required List<SelectOption> options,
|
||||
class SelectOptionCellState with _$SelectOptionCellState {
|
||||
const factory SelectOptionCellState({
|
||||
required List<SelectOption> selectedOptions,
|
||||
}) = _SelectionCellState;
|
||||
}) = _SelectOptionCellState;
|
||||
|
||||
factory SelectionCellState.initial(GridSelectOptionCellContext context) {
|
||||
factory SelectOptionCellState.initial(GridSelectOptionCellContext context) {
|
||||
final data = context.getCellData();
|
||||
|
||||
return SelectionCellState(
|
||||
options: data?.options ?? [],
|
||||
return SelectOptionCellState(
|
||||
selectedOptions: data?.selectOptions ?? [],
|
||||
);
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
import 'dart:async';
|
||||
import 'package:app_flowy/workspace/application/grid/field/grid_listenr.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:flowy_sdk/log.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/selection_type_option.pb.dart';
|
||||
@ -6,23 +7,28 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:app_flowy/workspace/application/grid/cell/cell_service/cell_service.dart';
|
||||
import 'select_option_service.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
|
||||
part 'selection_editor_bloc.freezed.dart';
|
||||
part 'select_option_editor_bloc.freezed.dart';
|
||||
|
||||
class SelectOptionEditorBloc extends Bloc<SelectOptionEditorEvent, SelectOptionEditorState> {
|
||||
class SelectOptionCellEditorBloc extends Bloc<SelectOptionEditorEvent, SelectOptionEditorState> {
|
||||
final SelectOptionService _selectOptionService;
|
||||
final GridSelectOptionCellContext cellContext;
|
||||
late final GridFieldsListener _fieldListener;
|
||||
void Function()? _onCellChangedFn;
|
||||
Timer? _delayOperation;
|
||||
|
||||
SelectOptionEditorBloc({
|
||||
SelectOptionCellEditorBloc({
|
||||
required this.cellContext,
|
||||
}) : _selectOptionService = SelectOptionService(gridCell: cellContext.gridCell),
|
||||
_fieldListener = GridFieldsListener(gridId: cellContext.gridId),
|
||||
super(SelectOptionEditorState.initial(cellContext)) {
|
||||
on<SelectOptionEditorEvent>(
|
||||
(event, emit) async {
|
||||
await event.map(
|
||||
initial: (_Initial value) async {
|
||||
_startListening();
|
||||
_loadOptions();
|
||||
},
|
||||
didReceiveOptions: (_DidReceiveOptions value) {
|
||||
final result = _makeOptions(state.filter, value.options);
|
||||
@ -62,6 +68,8 @@ class SelectOptionEditorBloc extends Bloc<SelectOptionEditorEvent, SelectOptionE
|
||||
cellContext.removeListener(_onCellChangedFn!);
|
||||
_onCellChangedFn = null;
|
||||
}
|
||||
_delayOperation?.cancel();
|
||||
await _fieldListener.stop();
|
||||
cellContext.dispose();
|
||||
return super.close();
|
||||
}
|
||||
@ -105,6 +113,24 @@ class SelectOptionEditorBloc extends Bloc<SelectOptionEditorEvent, SelectOptionE
|
||||
));
|
||||
}
|
||||
|
||||
void _loadOptions() {
|
||||
_delayOperation?.cancel();
|
||||
_delayOperation = Timer(const Duration(milliseconds: 10), () {
|
||||
_selectOptionService.getOpitonContext().then((result) {
|
||||
if (isClosed) {
|
||||
return;
|
||||
}
|
||||
return result.fold(
|
||||
(data) => add(SelectOptionEditorEvent.didReceiveOptions(data.options, data.selectOptions)),
|
||||
(err) {
|
||||
Log.error(err);
|
||||
return null;
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
_MakeOptionResult _makeOptions(Option<String> filter, List<SelectOption> allOptions) {
|
||||
final List<SelectOption> options = List.from(allOptions);
|
||||
Option<String> createOption = filter;
|
||||
@ -134,13 +160,21 @@ class SelectOptionEditorBloc extends Bloc<SelectOptionEditorEvent, SelectOptionE
|
||||
_onCellChangedFn = cellContext.startListening(
|
||||
onCellChanged: ((selectOptionContext) {
|
||||
if (!isClosed) {
|
||||
add(SelectOptionEditorEvent.didReceiveOptions(
|
||||
selectOptionContext.options,
|
||||
selectOptionContext.selectOptions,
|
||||
));
|
||||
_loadOptions();
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
_fieldListener.start(onFieldsChanged: (result) {
|
||||
result.fold(
|
||||
(changeset) {
|
||||
if (changeset.updatedFields.isNotEmpty) {
|
||||
_loadOptions();
|
||||
}
|
||||
},
|
||||
(err) => Log.error(err),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -167,7 +201,7 @@ class SelectOptionEditorState with _$SelectOptionEditorState {
|
||||
}) = _SelectOptionEditorState;
|
||||
|
||||
factory SelectOptionEditorState.initial(GridSelectOptionCellContext context) {
|
||||
final data = context.getCellData();
|
||||
final data = context.getCellData(loadIfNoCache: false);
|
||||
return SelectOptionEditorState(
|
||||
options: data?.options ?? [],
|
||||
allOptions: data?.options ?? [],
|
@ -1,4 +1,3 @@
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid-data-model/grid.pb.dart' show Cell;
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'dart:async';
|
||||
@ -14,21 +13,16 @@ class TextCellBloc extends Bloc<TextCellEvent, TextCellState> {
|
||||
}) : super(TextCellState.initial(cellContext)) {
|
||||
on<TextCellEvent>(
|
||||
(event, emit) async {
|
||||
await event.map(
|
||||
initial: (_InitialCell value) async {
|
||||
await event.when(
|
||||
initial: () async {
|
||||
_startListening();
|
||||
},
|
||||
updateText: (_UpdateText value) {
|
||||
cellContext.saveCellData(value.text);
|
||||
emit(state.copyWith(content: value.text));
|
||||
updateText: (text) {
|
||||
cellContext.saveCellData(text);
|
||||
emit(state.copyWith(content: text));
|
||||
},
|
||||
didReceiveCellData: (_DidReceiveCellData value) {
|
||||
emit(state.copyWith(content: value.cellData.cell?.content ?? ""));
|
||||
},
|
||||
didReceiveCellUpdate: (_DidReceiveCellUpdate value) {
|
||||
emit(state.copyWith(
|
||||
content: value.cell.content,
|
||||
));
|
||||
didReceiveCellUpdate: (content) {
|
||||
emit(state.copyWith(content: content));
|
||||
},
|
||||
);
|
||||
},
|
||||
@ -47,9 +41,9 @@ class TextCellBloc extends Bloc<TextCellEvent, TextCellState> {
|
||||
|
||||
void _startListening() {
|
||||
_onCellChangedFn = cellContext.startListening(
|
||||
onCellChanged: ((cell) {
|
||||
onCellChanged: ((cellContent) {
|
||||
if (!isClosed) {
|
||||
add(TextCellEvent.didReceiveCellUpdate(cell));
|
||||
add(TextCellEvent.didReceiveCellUpdate(cellContent ?? ""));
|
||||
}
|
||||
}),
|
||||
);
|
||||
@ -59,8 +53,7 @@ class TextCellBloc extends Bloc<TextCellEvent, TextCellState> {
|
||||
@freezed
|
||||
class TextCellEvent with _$TextCellEvent {
|
||||
const factory TextCellEvent.initial() = _InitialCell;
|
||||
const factory TextCellEvent.didReceiveCellData(GridCell cellData) = _DidReceiveCellData;
|
||||
const factory TextCellEvent.didReceiveCellUpdate(Cell cell) = _DidReceiveCellUpdate;
|
||||
const factory TextCellEvent.didReceiveCellUpdate(String cellContent) = _DidReceiveCellUpdate;
|
||||
const factory TextCellEvent.updateText(String text) = _UpdateText;
|
||||
}
|
||||
|
||||
@ -71,6 +64,6 @@ class TextCellState with _$TextCellState {
|
||||
}) = _TextCellState;
|
||||
|
||||
factory TextCellState.initial(GridCellContext context) => TextCellState(
|
||||
content: context.getCellData()?.content ?? "",
|
||||
content: context.getCellData() ?? "",
|
||||
);
|
||||
}
|
||||
|
@ -0,0 +1,77 @@
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/url_type_option.pb.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'dart:async';
|
||||
import 'cell_service/cell_service.dart';
|
||||
|
||||
part 'url_cell_bloc.freezed.dart';
|
||||
|
||||
class URLCellBloc extends Bloc<URLCellEvent, URLCellState> {
|
||||
final GridURLCellContext cellContext;
|
||||
void Function()? _onCellChangedFn;
|
||||
URLCellBloc({
|
||||
required this.cellContext,
|
||||
}) : super(URLCellState.initial(cellContext)) {
|
||||
on<URLCellEvent>(
|
||||
(event, emit) async {
|
||||
event.when(
|
||||
initial: () {
|
||||
_startListening();
|
||||
},
|
||||
didReceiveCellUpdate: (cellData) {
|
||||
emit(state.copyWith(
|
||||
content: cellData?.content ?? "",
|
||||
url: cellData?.url ?? "",
|
||||
));
|
||||
},
|
||||
updateURL: (String url) {
|
||||
cellContext.saveCellData(url, deduplicate: true);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
if (_onCellChangedFn != null) {
|
||||
cellContext.removeListener(_onCellChangedFn!);
|
||||
_onCellChangedFn = null;
|
||||
}
|
||||
cellContext.dispose();
|
||||
return super.close();
|
||||
}
|
||||
|
||||
void _startListening() {
|
||||
_onCellChangedFn = cellContext.startListening(
|
||||
onCellChanged: ((cellData) {
|
||||
if (!isClosed) {
|
||||
add(URLCellEvent.didReceiveCellUpdate(cellData));
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class URLCellEvent with _$URLCellEvent {
|
||||
const factory URLCellEvent.initial() = _InitialCell;
|
||||
const factory URLCellEvent.updateURL(String url) = _UpdateURL;
|
||||
const factory URLCellEvent.didReceiveCellUpdate(URLCellData? cell) = _DidReceiveCellUpdate;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class URLCellState with _$URLCellState {
|
||||
const factory URLCellState({
|
||||
required String content,
|
||||
required String url,
|
||||
}) = _URLCellState;
|
||||
|
||||
factory URLCellState.initial(GridURLCellContext context) {
|
||||
final cellData = context.getCellData();
|
||||
return URLCellState(
|
||||
content: cellData?.content ?? "",
|
||||
url: cellData?.url ?? "",
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,73 @@
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/url_type_option.pb.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'dart:async';
|
||||
import 'cell_service/cell_service.dart';
|
||||
|
||||
part 'url_cell_editor_bloc.freezed.dart';
|
||||
|
||||
class URLCellEditorBloc extends Bloc<URLCellEditorEvent, URLCellEditorState> {
|
||||
final GridURLCellContext cellContext;
|
||||
void Function()? _onCellChangedFn;
|
||||
URLCellEditorBloc({
|
||||
required this.cellContext,
|
||||
}) : super(URLCellEditorState.initial(cellContext)) {
|
||||
on<URLCellEditorEvent>(
|
||||
(event, emit) async {
|
||||
event.when(
|
||||
initial: () {
|
||||
_startListening();
|
||||
},
|
||||
updateText: (text) {
|
||||
cellContext.saveCellData(text, deduplicate: true);
|
||||
emit(state.copyWith(content: text));
|
||||
},
|
||||
didReceiveCellUpdate: (cellData) {
|
||||
emit(state.copyWith(content: cellData?.content ?? ""));
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
if (_onCellChangedFn != null) {
|
||||
cellContext.removeListener(_onCellChangedFn!);
|
||||
_onCellChangedFn = null;
|
||||
}
|
||||
cellContext.dispose();
|
||||
return super.close();
|
||||
}
|
||||
|
||||
void _startListening() {
|
||||
_onCellChangedFn = cellContext.startListening(
|
||||
onCellChanged: ((cellData) {
|
||||
if (!isClosed) {
|
||||
add(URLCellEditorEvent.didReceiveCellUpdate(cellData));
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class URLCellEditorEvent with _$URLCellEditorEvent {
|
||||
const factory URLCellEditorEvent.initial() = _InitialCell;
|
||||
const factory URLCellEditorEvent.didReceiveCellUpdate(URLCellData? cell) = _DidReceiveCellUpdate;
|
||||
const factory URLCellEditorEvent.updateText(String text) = _UpdateText;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class URLCellEditorState with _$URLCellEditorState {
|
||||
const factory URLCellEditorState({
|
||||
required String content,
|
||||
}) = _URLCellEditorState;
|
||||
|
||||
factory URLCellEditorState.initial(GridURLCellContext context) {
|
||||
final cellData = context.getCellData();
|
||||
return URLCellEditorState(
|
||||
content: cellData?.content ?? "",
|
||||
);
|
||||
}
|
||||
}
|
@ -11,7 +11,7 @@ class FieldActionSheetBloc extends Bloc<FieldActionSheetEvent, FieldActionSheetS
|
||||
final FieldService fieldService;
|
||||
|
||||
FieldActionSheetBloc({required Field field, required this.fieldService})
|
||||
: super(FieldActionSheetState.initial(EditFieldContext.create()..gridField = field)) {
|
||||
: super(FieldActionSheetState.initial(FieldTypeOptionData.create()..field_2 = field)) {
|
||||
on<FieldActionSheetEvent>(
|
||||
(event, emit) async {
|
||||
await event.map(
|
||||
@ -67,14 +67,14 @@ class FieldActionSheetEvent with _$FieldActionSheetEvent {
|
||||
@freezed
|
||||
class FieldActionSheetState with _$FieldActionSheetState {
|
||||
const factory FieldActionSheetState({
|
||||
required EditFieldContext editContext,
|
||||
required FieldTypeOptionData fieldTypeOptionData,
|
||||
required String errorText,
|
||||
required String fieldName,
|
||||
}) = _FieldActionSheetState;
|
||||
|
||||
factory FieldActionSheetState.initial(EditFieldContext editContext) => FieldActionSheetState(
|
||||
editContext: editContext,
|
||||
factory FieldActionSheetState.initial(FieldTypeOptionData data) => FieldActionSheetState(
|
||||
fieldTypeOptionData: data,
|
||||
errorText: '',
|
||||
fieldName: editContext.gridField.name,
|
||||
fieldName: data.field_2.name,
|
||||
);
|
||||
}
|
||||
|
@ -19,18 +19,20 @@ class FieldCellBloc extends Bloc<FieldCellEvent, FieldCellState> {
|
||||
super(FieldCellState.initial(cellContext)) {
|
||||
on<FieldCellEvent>(
|
||||
(event, emit) async {
|
||||
await event.map(
|
||||
initial: (_InitialCell value) async {
|
||||
event.when(
|
||||
initial: () {
|
||||
_startListening();
|
||||
},
|
||||
didReceiveFieldUpdate: (_DidReceiveFieldUpdate value) {
|
||||
emit(state.copyWith(field: value.field));
|
||||
didReceiveFieldUpdate: (field) {
|
||||
emit(state.copyWith(field: cellContext.field));
|
||||
},
|
||||
updateWidth: (_UpdateWidth value) {
|
||||
final defaultWidth = state.field.width.toDouble();
|
||||
final width = defaultWidth + value.offset;
|
||||
if (width > defaultWidth && width < 300) {
|
||||
_fieldService.updateField(width: width);
|
||||
startUpdateWidth: (offset) {
|
||||
final width = state.width + offset;
|
||||
emit(state.copyWith(width: width));
|
||||
},
|
||||
endUpdateWidth: () {
|
||||
if (state.width != state.field.width.toDouble()) {
|
||||
_fieldService.updateField(width: state.width);
|
||||
}
|
||||
},
|
||||
);
|
||||
@ -61,7 +63,8 @@ class FieldCellBloc extends Bloc<FieldCellEvent, FieldCellState> {
|
||||
class FieldCellEvent with _$FieldCellEvent {
|
||||
const factory FieldCellEvent.initial() = _InitialCell;
|
||||
const factory FieldCellEvent.didReceiveFieldUpdate(Field field) = _DidReceiveFieldUpdate;
|
||||
const factory FieldCellEvent.updateWidth(double offset) = _UpdateWidth;
|
||||
const factory FieldCellEvent.startUpdateWidth(double offset) = _StartUpdateWidth;
|
||||
const factory FieldCellEvent.endUpdateWidth() = _EndUpdateWidth;
|
||||
}
|
||||
|
||||
@freezed
|
||||
@ -69,10 +72,12 @@ class FieldCellState with _$FieldCellState {
|
||||
const factory FieldCellState({
|
||||
required String gridId,
|
||||
required Field field,
|
||||
required double width,
|
||||
}) = _FieldCellState;
|
||||
|
||||
factory FieldCellState.initial(GridFieldCellContext cellContext) => FieldCellState(
|
||||
gridId: cellContext.gridId,
|
||||
field: cellContext.field,
|
||||
width: cellContext.field.width.toDouble(),
|
||||
);
|
||||
}
|
||||
|
@ -1,41 +1,31 @@
|
||||
import 'dart:typed_data';
|
||||
import 'package:flowy_sdk/log.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid-data-model/grid.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 'package:protobuf/protobuf.dart';
|
||||
|
||||
part 'field_editor_bloc.freezed.dart';
|
||||
|
||||
class FieldEditorBloc extends Bloc<FieldEditorEvent, FieldEditorState> {
|
||||
final String gridId;
|
||||
final EditFieldContextLoader _loader;
|
||||
|
||||
FieldEditorBloc({
|
||||
required this.gridId,
|
||||
required EditFieldContextLoader fieldLoader,
|
||||
}) : _loader = fieldLoader,
|
||||
super(FieldEditorState.initial(gridId)) {
|
||||
required String gridId,
|
||||
required String fieldName,
|
||||
required IFieldContextLoader fieldContextLoader,
|
||||
}) : super(FieldEditorState.initial(gridId, fieldName, fieldContextLoader)) {
|
||||
on<FieldEditorEvent>(
|
||||
(event, emit) async {
|
||||
await event.map(
|
||||
initial: (_InitialField value) async {
|
||||
await _getEditFieldContext(emit);
|
||||
await event.when(
|
||||
initial: () async {
|
||||
final fieldContext = GridFieldContext(gridId: gridId, loader: fieldContextLoader);
|
||||
await fieldContext.loadData().then((result) {
|
||||
result.fold(
|
||||
(l) => emit(state.copyWith(fieldContext: Some(fieldContext), name: fieldContext.field.name)),
|
||||
(r) => null,
|
||||
);
|
||||
});
|
||||
},
|
||||
updateName: (_UpdateName value) {
|
||||
final newContext = _updateEditContext(name: value.name);
|
||||
emit(state.copyWith(editFieldContext: newContext));
|
||||
},
|
||||
updateField: (_UpdateField value) {
|
||||
final newContext = _updateEditContext(field: value.field, typeOptionData: value.typeOptionData);
|
||||
|
||||
emit(state.copyWith(editFieldContext: newContext));
|
||||
},
|
||||
done: (_Done value) async {
|
||||
await _saveField(emit);
|
||||
updateName: (name) {
|
||||
state.fieldContext.fold(() => null, (fieldContext) => fieldContext.fieldName = name);
|
||||
emit(state.copyWith(name: name));
|
||||
},
|
||||
);
|
||||
},
|
||||
@ -46,78 +36,12 @@ class FieldEditorBloc extends Bloc<FieldEditorEvent, FieldEditorState> {
|
||||
Future<void> close() async {
|
||||
return super.close();
|
||||
}
|
||||
|
||||
Option<EditFieldContext> _updateEditContext({
|
||||
String? name,
|
||||
Field? field,
|
||||
List<int>? typeOptionData,
|
||||
}) {
|
||||
return state.editFieldContext.fold(
|
||||
() => none(),
|
||||
(context) {
|
||||
context.freeze();
|
||||
final newContext = context.rebuild((newContext) {
|
||||
newContext.gridField.rebuild((newField) {
|
||||
if (name != null) {
|
||||
newField.name = name;
|
||||
}
|
||||
|
||||
newContext.gridField = newField;
|
||||
});
|
||||
|
||||
if (field != null) {
|
||||
newContext.gridField = field;
|
||||
}
|
||||
|
||||
if (typeOptionData != null) {
|
||||
newContext.typeOptionData = typeOptionData;
|
||||
}
|
||||
});
|
||||
|
||||
FieldService.insertField(
|
||||
gridId: gridId,
|
||||
field: newContext.gridField,
|
||||
typeOptionData: newContext.typeOptionData,
|
||||
);
|
||||
|
||||
return Some(newContext);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _saveField(Emitter<FieldEditorState> emit) async {
|
||||
await state.editFieldContext.fold(
|
||||
() async => null,
|
||||
(context) async {
|
||||
final result = await FieldService.insertField(
|
||||
gridId: gridId,
|
||||
field: context.gridField,
|
||||
typeOptionData: context.typeOptionData,
|
||||
);
|
||||
result.fold((l) => null, (r) => null);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _getEditFieldContext(Emitter<FieldEditorState> emit) async {
|
||||
final result = await _loader.load();
|
||||
result.fold(
|
||||
(context) {
|
||||
emit(state.copyWith(
|
||||
editFieldContext: Some(context),
|
||||
));
|
||||
},
|
||||
(err) => Log.error(err),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class FieldEditorEvent with _$FieldEditorEvent {
|
||||
const factory FieldEditorEvent.initial() = _InitialField;
|
||||
const factory FieldEditorEvent.updateName(String name) = _UpdateName;
|
||||
const factory FieldEditorEvent.updateField(Field field, Uint8List typeOptionData) = _UpdateField;
|
||||
const factory FieldEditorEvent.done() = _Done;
|
||||
}
|
||||
|
||||
@freezed
|
||||
@ -125,12 +49,14 @@ class FieldEditorState with _$FieldEditorState {
|
||||
const factory FieldEditorState({
|
||||
required String gridId,
|
||||
required String errorText,
|
||||
required Option<EditFieldContext> editFieldContext,
|
||||
required String name,
|
||||
required Option<GridFieldContext> fieldContext,
|
||||
}) = _FieldEditorState;
|
||||
|
||||
factory FieldEditorState.initial(String gridId) => FieldEditorState(
|
||||
factory FieldEditorState.initial(String gridId, String fieldName, IFieldContextLoader loader) => FieldEditorState(
|
||||
gridId: gridId,
|
||||
editFieldContext: none(),
|
||||
fieldContext: none(),
|
||||
errorText: '',
|
||||
name: fieldName,
|
||||
);
|
||||
}
|
||||
|
@ -1,24 +1,29 @@
|
||||
import 'dart:typed_data';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid-data-model/grid.pb.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'dart:async';
|
||||
|
||||
import 'field_service.dart';
|
||||
|
||||
part 'field_editor_pannel_bloc.freezed.dart';
|
||||
|
||||
class FieldEditorPannelBloc extends Bloc<FieldEditorPannelEvent, FieldEditorPannelState> {
|
||||
FieldEditorPannelBloc(EditFieldContext editContext) : super(FieldEditorPannelState.initial(editContext)) {
|
||||
final GridFieldContext _fieldContext;
|
||||
void Function()? _fieldListenFn;
|
||||
|
||||
FieldEditorPannelBloc(GridFieldContext fieldContext)
|
||||
: _fieldContext = fieldContext,
|
||||
super(FieldEditorPannelState.initial(fieldContext)) {
|
||||
on<FieldEditorPannelEvent>(
|
||||
(event, emit) async {
|
||||
await event.map(
|
||||
toFieldType: (_ToFieldType value) async {
|
||||
emit(state.copyWith(
|
||||
field: value.field,
|
||||
typeOptionData: Uint8List.fromList(value.typeOptionData),
|
||||
));
|
||||
event.when(
|
||||
initial: () {
|
||||
_fieldListenFn = fieldContext.addFieldListener((field) {
|
||||
add(FieldEditorPannelEvent.didReceiveFieldUpdated(field));
|
||||
});
|
||||
},
|
||||
didUpdateTypeOptionData: (_DidUpdateTypeOptionData value) {
|
||||
emit(state.copyWith(typeOptionData: value.typeOptionData));
|
||||
didReceiveFieldUpdated: (field) {
|
||||
emit(state.copyWith(field: field));
|
||||
},
|
||||
);
|
||||
},
|
||||
@ -27,27 +32,26 @@ class FieldEditorPannelBloc extends Bloc<FieldEditorPannelEvent, FieldEditorPann
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
if (_fieldListenFn != null) {
|
||||
_fieldContext.removeFieldListener(_fieldListenFn!);
|
||||
}
|
||||
return super.close();
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class FieldEditorPannelEvent with _$FieldEditorPannelEvent {
|
||||
const factory FieldEditorPannelEvent.toFieldType(Field field, List<int> typeOptionData) = _ToFieldType;
|
||||
const factory FieldEditorPannelEvent.didUpdateTypeOptionData(Uint8List typeOptionData) = _DidUpdateTypeOptionData;
|
||||
const factory FieldEditorPannelEvent.initial() = _Initial;
|
||||
const factory FieldEditorPannelEvent.didReceiveFieldUpdated(Field field) = _DidReceiveFieldUpdated;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class FieldEditorPannelState with _$FieldEditorPannelState {
|
||||
const factory FieldEditorPannelState({
|
||||
required String gridId,
|
||||
required Field field,
|
||||
required Uint8List typeOptionData,
|
||||
}) = _FieldEditorPannelState;
|
||||
|
||||
factory FieldEditorPannelState.initial(EditFieldContext context) => FieldEditorPannelState(
|
||||
gridId: context.gridId,
|
||||
field: context.gridField,
|
||||
typeOptionData: Uint8List.fromList(context.typeOptionData),
|
||||
factory FieldEditorPannelState.initial(GridFieldContext fieldContext) => FieldEditorPannelState(
|
||||
field: fieldContext.field,
|
||||
);
|
||||
}
|
||||
|
@ -1,9 +1,12 @@
|
||||
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-data-model/grid.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/field_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';
|
||||
|
||||
class FieldService {
|
||||
@ -12,24 +15,6 @@ class FieldService {
|
||||
|
||||
FieldService({required this.gridId, required this.fieldId});
|
||||
|
||||
Future<Either<EditFieldContext, FlowyError>> switchToField(FieldType fieldType) {
|
||||
final payload = EditFieldPayload.create()
|
||||
..gridId = gridId
|
||||
..fieldId = fieldId
|
||||
..fieldType = fieldType;
|
||||
|
||||
return GridEventSwitchToField(payload).send();
|
||||
}
|
||||
|
||||
Future<Either<EditFieldContext, FlowyError>> getEditFieldContext(FieldType fieldType) {
|
||||
final payload = EditFieldPayload.create()
|
||||
..gridId = gridId
|
||||
..fieldId = fieldId
|
||||
..fieldType = fieldType;
|
||||
|
||||
return GridEventGetEditFieldContext(payload).send();
|
||||
}
|
||||
|
||||
Future<Either<Unit, FlowyError>> moveField(int fromIndex, int toIndex) {
|
||||
final payload = MoveItemPayload.create()
|
||||
..gridId = gridId
|
||||
@ -128,7 +113,7 @@ class FieldService {
|
||||
return GridEventDuplicateField(payload).send();
|
||||
}
|
||||
|
||||
Future<Either<List<int>, FlowyError>> getTypeOptionData({
|
||||
Future<Either<FieldTypeOptionData, FlowyError>> getFieldTypeOptionData({
|
||||
required FieldType fieldType,
|
||||
}) {
|
||||
final payload = EditFieldPayload.create()
|
||||
@ -137,7 +122,7 @@ class FieldService {
|
||||
..fieldType = fieldType;
|
||||
return GridEventGetFieldTypeOption(payload).send().then((result) {
|
||||
return result.fold(
|
||||
(data) => left(data.typeOptionData),
|
||||
(data) => left(data),
|
||||
(err) => right(err),
|
||||
);
|
||||
});
|
||||
@ -152,59 +137,162 @@ class GridFieldCellContext with _$GridFieldCellContext {
|
||||
}) = _GridFieldCellContext;
|
||||
}
|
||||
|
||||
abstract class EditFieldContextLoader {
|
||||
Future<Either<EditFieldContext, FlowyError>> load();
|
||||
abstract class IFieldContextLoader {
|
||||
String get gridId;
|
||||
Future<Either<FieldTypeOptionData, FlowyError>> load();
|
||||
|
||||
Future<Either<EditFieldContext, FlowyError>> switchToField(String fieldId, FieldType fieldType);
|
||||
Future<Either<FieldTypeOptionData, FlowyError>> switchToField(String fieldId, FieldType fieldType) {
|
||||
final payload = EditFieldPayload.create()
|
||||
..gridId = gridId
|
||||
..fieldId = fieldId
|
||||
..fieldType = fieldType;
|
||||
|
||||
return GridEventSwitchToField(payload).send();
|
||||
}
|
||||
}
|
||||
|
||||
class NewFieldContextLoader extends EditFieldContextLoader {
|
||||
class NewFieldContextLoader extends IFieldContextLoader {
|
||||
@override
|
||||
final String gridId;
|
||||
NewFieldContextLoader({
|
||||
required this.gridId,
|
||||
});
|
||||
|
||||
@override
|
||||
Future<Either<EditFieldContext, FlowyError>> load() {
|
||||
Future<Either<FieldTypeOptionData, FlowyError>> load() {
|
||||
final payload = EditFieldPayload.create()
|
||||
..gridId = gridId
|
||||
..fieldType = FieldType.RichText;
|
||||
|
||||
return GridEventGetEditFieldContext(payload).send();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<EditFieldContext, FlowyError>> switchToField(String fieldId, FieldType fieldType) {
|
||||
final payload = EditFieldPayload.create()
|
||||
..gridId = gridId
|
||||
..fieldType = fieldType;
|
||||
|
||||
return GridEventGetEditFieldContext(payload).send();
|
||||
return GridEventCreateFieldTypeOption(payload).send();
|
||||
}
|
||||
}
|
||||
|
||||
class FieldContextLoaderAdaptor extends EditFieldContextLoader {
|
||||
class FieldContextLoader extends IFieldContextLoader {
|
||||
@override
|
||||
final String gridId;
|
||||
final Field field;
|
||||
|
||||
FieldContextLoaderAdaptor({
|
||||
FieldContextLoader({
|
||||
required this.gridId,
|
||||
required this.field,
|
||||
});
|
||||
|
||||
@override
|
||||
Future<Either<EditFieldContext, FlowyError>> load() {
|
||||
Future<Either<FieldTypeOptionData, FlowyError>> load() {
|
||||
final payload = EditFieldPayload.create()
|
||||
..gridId = gridId
|
||||
..fieldId = field.id
|
||||
..fieldType = field.fieldType;
|
||||
|
||||
return GridEventGetEditFieldContext(payload).send();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<EditFieldContext, FlowyError>> switchToField(String fieldId, FieldType fieldType) async {
|
||||
final fieldService = FieldService(gridId: gridId, fieldId: fieldId);
|
||||
return fieldService.switchToField(fieldType);
|
||||
return GridEventGetFieldTypeOption(payload).send();
|
||||
}
|
||||
}
|
||||
|
||||
class GridFieldContext {
|
||||
final String gridId;
|
||||
final IFieldContextLoader _loader;
|
||||
|
||||
late FieldTypeOptionData _data;
|
||||
ValueNotifier<Field>? _fieldNotifier;
|
||||
|
||||
GridFieldContext({
|
||||
required this.gridId,
|
||||
required IFieldContextLoader loader,
|
||||
}) : _loader = loader;
|
||||
|
||||
Future<Either<Unit, FlowyError>> loadData() async {
|
||||
final result = await _loader.load();
|
||||
return result.fold(
|
||||
(data) {
|
||||
data.freeze();
|
||||
_data = data;
|
||||
|
||||
if (_fieldNotifier == null) {
|
||||
_fieldNotifier = ValueNotifier(data.field_2);
|
||||
} else {
|
||||
_fieldNotifier?.value = data.field_2;
|
||||
}
|
||||
|
||||
return left(unit);
|
||||
},
|
||||
(err) {
|
||||
Log.error(err);
|
||||
return right(err);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Field get field => _data.field_2;
|
||||
|
||||
set field(Field 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, Field? 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;
|
||||
}
|
||||
});
|
||||
|
||||
if (_data.field_2 != _fieldNotifier?.value) {
|
||||
_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(Field) callback) {
|
||||
listener() {
|
||||
callback(field);
|
||||
}
|
||||
|
||||
_fieldNotifier?.addListener(listener);
|
||||
return listener;
|
||||
}
|
||||
|
||||
void removeFieldListener(void Function() listener) {
|
||||
_fieldNotifier?.removeListener(listener);
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
import 'package:app_flowy/workspace/application/grid/field/type_option/type_option_service.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/date_type_option.pb.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
@ -5,8 +6,18 @@ import 'dart:async';
|
||||
import 'package:protobuf/protobuf.dart';
|
||||
part 'date_bloc.freezed.dart';
|
||||
|
||||
typedef DateTypeOptionContext = TypeOptionContext<DateTypeOption>;
|
||||
|
||||
class DateTypeOptionDataBuilder extends TypeOptionDataBuilder<DateTypeOption> {
|
||||
@override
|
||||
DateTypeOption fromBuffer(List<int> buffer) {
|
||||
return DateTypeOption.fromBuffer(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
class DateTypeOptionBloc extends Bloc<DateTypeOptionEvent, DateTypeOptionState> {
|
||||
DateTypeOptionBloc({required DateTypeOption typeOption}) : super(DateTypeOptionState.initial(typeOption)) {
|
||||
DateTypeOptionBloc({required DateTypeOptionContext typeOptionContext})
|
||||
: super(DateTypeOptionState.initial(typeOptionContext.typeOption)) {
|
||||
on<DateTypeOptionEvent>(
|
||||
(event, emit) async {
|
||||
event.map(
|
||||
|
@ -1,65 +0,0 @@
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/selection_type_option.pb.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'dart:async';
|
||||
import 'package:dartz/dartz.dart';
|
||||
part 'field_option_pannel_bloc.freezed.dart';
|
||||
|
||||
class FieldOptionPannelBloc extends Bloc<FieldOptionPannelEvent, FieldOptionPannelState> {
|
||||
FieldOptionPannelBloc({required List<SelectOption> options}) : super(FieldOptionPannelState.initial(options)) {
|
||||
on<FieldOptionPannelEvent>(
|
||||
(event, emit) async {
|
||||
await event.map(
|
||||
createOption: (_CreateOption value) async {
|
||||
emit(state.copyWith(isEditingOption: false, newOptionName: Some(value.optionName)));
|
||||
},
|
||||
beginAddingOption: (_BeginAddingOption value) {
|
||||
emit(state.copyWith(isEditingOption: true, newOptionName: none()));
|
||||
},
|
||||
endAddingOption: (_EndAddingOption value) {
|
||||
emit(state.copyWith(isEditingOption: false, newOptionName: none()));
|
||||
},
|
||||
updateOption: (_UpdateOption value) {
|
||||
emit(state.copyWith(updateOption: Some(value.option)));
|
||||
},
|
||||
deleteOption: (_DeleteOption value) {
|
||||
emit(state.copyWith(deleteOption: Some(value.option)));
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
return super.close();
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class FieldOptionPannelEvent with _$FieldOptionPannelEvent {
|
||||
const factory FieldOptionPannelEvent.createOption(String optionName) = _CreateOption;
|
||||
const factory FieldOptionPannelEvent.beginAddingOption() = _BeginAddingOption;
|
||||
const factory FieldOptionPannelEvent.endAddingOption() = _EndAddingOption;
|
||||
const factory FieldOptionPannelEvent.updateOption(SelectOption option) = _UpdateOption;
|
||||
const factory FieldOptionPannelEvent.deleteOption(SelectOption option) = _DeleteOption;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class FieldOptionPannelState with _$FieldOptionPannelState {
|
||||
const factory FieldOptionPannelState({
|
||||
required List<SelectOption> options,
|
||||
required bool isEditingOption,
|
||||
required Option<String> newOptionName,
|
||||
required Option<SelectOption> updateOption,
|
||||
required Option<SelectOption> deleteOption,
|
||||
}) = _FieldOptionPannelState;
|
||||
|
||||
factory FieldOptionPannelState.initial(List<SelectOption> options) => FieldOptionPannelState(
|
||||
options: options,
|
||||
isEditingOption: false,
|
||||
newOptionName: none(),
|
||||
updateOption: none(),
|
||||
deleteOption: none(),
|
||||
);
|
||||
}
|
@ -1,89 +0,0 @@
|
||||
import 'package:flowy_sdk/log.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/selection_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_service.dart';
|
||||
|
||||
part 'multi_select_bloc.freezed.dart';
|
||||
|
||||
class MultiSelectTypeOptionBloc extends Bloc<MultiSelectTypeOptionEvent, MultiSelectTypeOptionState> {
|
||||
final TypeOptionService service;
|
||||
|
||||
MultiSelectTypeOptionBloc(TypeOptionContext typeOptionContext)
|
||||
: service = TypeOptionService(gridId: typeOptionContext.gridId, fieldId: typeOptionContext.field.id),
|
||||
super(MultiSelectTypeOptionState.initial(MultiSelectTypeOption.fromBuffer(typeOptionContext.data))) {
|
||||
on<MultiSelectTypeOptionEvent>(
|
||||
(event, emit) async {
|
||||
await event.map(
|
||||
createOption: (_CreateOption value) async {
|
||||
final result = await service.newOption(name: value.optionName);
|
||||
result.fold(
|
||||
(option) {
|
||||
emit(state.copyWith(typeOption: _insertOption(option)));
|
||||
},
|
||||
(err) => Log.error(err),
|
||||
);
|
||||
},
|
||||
updateOption: (_UpdateOption value) async {
|
||||
emit(state.copyWith(typeOption: _updateOption(value.option)));
|
||||
},
|
||||
deleteOption: (_DeleteOption value) {
|
||||
emit(state.copyWith(typeOption: _deleteOption(value.option)));
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
return super.close();
|
||||
}
|
||||
|
||||
MultiSelectTypeOption _insertOption(SelectOption option) {
|
||||
state.typeOption.freeze();
|
||||
return state.typeOption.rebuild((typeOption) {
|
||||
typeOption.options.insert(0, option);
|
||||
});
|
||||
}
|
||||
|
||||
MultiSelectTypeOption _updateOption(SelectOption option) {
|
||||
state.typeOption.freeze();
|
||||
return state.typeOption.rebuild((typeOption) {
|
||||
final index = typeOption.options.indexWhere((element) => element.id == option.id);
|
||||
if (index != -1) {
|
||||
typeOption.options[index] = option;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
MultiSelectTypeOption _deleteOption(SelectOption option) {
|
||||
state.typeOption.freeze();
|
||||
return state.typeOption.rebuild((typeOption) {
|
||||
final index = typeOption.options.indexWhere((element) => element.id == option.id);
|
||||
if (index != -1) {
|
||||
typeOption.options.removeAt(index);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class MultiSelectTypeOptionEvent with _$MultiSelectTypeOptionEvent {
|
||||
const factory MultiSelectTypeOptionEvent.createOption(String optionName) = _CreateOption;
|
||||
const factory MultiSelectTypeOptionEvent.updateOption(SelectOption option) = _UpdateOption;
|
||||
const factory MultiSelectTypeOptionEvent.deleteOption(SelectOption option) = _DeleteOption;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class MultiSelectTypeOptionState with _$MultiSelectTypeOptionState {
|
||||
const factory MultiSelectTypeOptionState({
|
||||
required MultiSelectTypeOption typeOption,
|
||||
}) = _MultiSelectTypeOptionState;
|
||||
|
||||
factory MultiSelectTypeOptionState.initial(MultiSelectTypeOption typeOption) => MultiSelectTypeOptionState(
|
||||
typeOption: typeOption,
|
||||
);
|
||||
}
|
@ -0,0 +1,77 @@
|
||||
import 'package:app_flowy/workspace/application/grid/field/field_service.dart';
|
||||
import 'package:flowy_sdk/log.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/selection_type_option.pb.dart';
|
||||
import 'dart:async';
|
||||
import 'package:protobuf/protobuf.dart';
|
||||
import 'select_option_type_option_bloc.dart';
|
||||
import 'type_option_service.dart';
|
||||
|
||||
class MultiSelectTypeOptionContext extends TypeOptionContext<MultiSelectTypeOption> with SelectOptionTypeOptionAction {
|
||||
final TypeOptionService service;
|
||||
|
||||
MultiSelectTypeOptionContext({
|
||||
required MultiSelectTypeOptionDataBuilder dataBuilder,
|
||||
required GridFieldContext fieldContext,
|
||||
}) : service = TypeOptionService(
|
||||
gridId: fieldContext.gridId,
|
||||
fieldId: fieldContext.field.id,
|
||||
),
|
||||
super(dataBuilder: dataBuilder, fieldContext: fieldContext);
|
||||
|
||||
@override
|
||||
List<SelectOption> Function(SelectOption) get deleteOption {
|
||||
return (SelectOption option) {
|
||||
typeOption.freeze();
|
||||
typeOption = typeOption.rebuild((typeOption) {
|
||||
final index = typeOption.options.indexWhere((element) => element.id == option.id);
|
||||
if (index != -1) {
|
||||
typeOption.options.removeAt(index);
|
||||
}
|
||||
});
|
||||
return typeOption.options;
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<SelectOption>> Function(String) get insertOption {
|
||||
return (String optionName) {
|
||||
return service.newOption(name: optionName).then((result) {
|
||||
return result.fold(
|
||||
(option) {
|
||||
typeOption.freeze();
|
||||
typeOption = typeOption.rebuild((typeOption) {
|
||||
typeOption.options.insert(0, option);
|
||||
});
|
||||
|
||||
return typeOption.options;
|
||||
},
|
||||
(err) {
|
||||
Log.error(err);
|
||||
return typeOption.options;
|
||||
},
|
||||
);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
List<SelectOption> Function(SelectOption) get udpateOption {
|
||||
return (SelectOption option) {
|
||||
typeOption.freeze();
|
||||
typeOption = typeOption.rebuild((typeOption) {
|
||||
final index = typeOption.options.indexWhere((element) => element.id == option.id);
|
||||
if (index != -1) {
|
||||
typeOption.options[index] = option;
|
||||
}
|
||||
});
|
||||
return typeOption.options;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class MultiSelectTypeOptionDataBuilder extends TypeOptionDataBuilder<MultiSelectTypeOption> {
|
||||
@override
|
||||
MultiSelectTypeOption fromBuffer(List<int> buffer) {
|
||||
return MultiSelectTypeOption.fromBuffer(buffer);
|
||||
}
|
||||
}
|
@ -1,3 +1,5 @@
|
||||
import 'package:app_flowy/workspace/application/grid/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';
|
||||
@ -6,8 +8,18 @@ import 'package:protobuf/protobuf.dart';
|
||||
|
||||
part 'number_bloc.freezed.dart';
|
||||
|
||||
typedef NumberTypeOptionContext = TypeOptionContext<NumberTypeOption>;
|
||||
|
||||
class NumberTypeOptionDataBuilder extends TypeOptionDataBuilder<NumberTypeOption> {
|
||||
@override
|
||||
NumberTypeOption fromBuffer(List<int> buffer) {
|
||||
return NumberTypeOption.fromBuffer(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
class NumberTypeOptionBloc extends Bloc<NumberTypeOptionEvent, NumberTypeOptionState> {
|
||||
NumberTypeOptionBloc({required NumberTypeOption typeOption}) : super(NumberTypeOptionState.initial(typeOption)) {
|
||||
NumberTypeOptionBloc({required NumberTypeOptionContext typeOptionContext})
|
||||
: super(NumberTypeOptionState.initial(typeOptionContext.typeOption)) {
|
||||
on<NumberTypeOptionEvent>(
|
||||
(event, emit) async {
|
||||
event.map(
|
||||
|
@ -1,4 +1,4 @@
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/number_type_option.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/format.pbenum.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'dart:async';
|
||||
|
@ -0,0 +1,77 @@
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/selection_type_option.pb.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'dart:async';
|
||||
import 'package:dartz/dartz.dart';
|
||||
part 'select_option_type_option_bloc.freezed.dart';
|
||||
|
||||
abstract class SelectOptionTypeOptionAction {
|
||||
Future<List<SelectOption>> Function(String) get insertOption;
|
||||
|
||||
List<SelectOption> Function(SelectOption) get deleteOption;
|
||||
|
||||
List<SelectOption> Function(SelectOption) get udpateOption;
|
||||
}
|
||||
|
||||
class SelectOptionTypeOptionBloc extends Bloc<SelectOptionTypeOptionEvent, SelectOptionTypeOptionState> {
|
||||
final SelectOptionTypeOptionAction typeOptionAction;
|
||||
|
||||
SelectOptionTypeOptionBloc({
|
||||
required List<SelectOption> options,
|
||||
required this.typeOptionAction,
|
||||
}) : super(SelectOptionTypeOptionState.initial(options)) {
|
||||
on<SelectOptionTypeOptionEvent>(
|
||||
(event, emit) async {
|
||||
await event.when(
|
||||
createOption: (optionName) async {
|
||||
final List<SelectOption> options = await typeOptionAction.insertOption(optionName);
|
||||
emit(state.copyWith(options: options));
|
||||
},
|
||||
addingOption: () {
|
||||
emit(state.copyWith(isEditingOption: true, newOptionName: none()));
|
||||
},
|
||||
endAddingOption: () {
|
||||
emit(state.copyWith(isEditingOption: false, newOptionName: none()));
|
||||
},
|
||||
updateOption: (option) {
|
||||
final List<SelectOption> options = typeOptionAction.udpateOption(option);
|
||||
emit(state.copyWith(options: options));
|
||||
},
|
||||
deleteOption: (option) {
|
||||
final List<SelectOption> options = typeOptionAction.deleteOption(option);
|
||||
emit(state.copyWith(options: options));
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
return super.close();
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class SelectOptionTypeOptionEvent with _$SelectOptionTypeOptionEvent {
|
||||
const factory SelectOptionTypeOptionEvent.createOption(String optionName) = _CreateOption;
|
||||
const factory SelectOptionTypeOptionEvent.addingOption() = _AddingOption;
|
||||
const factory SelectOptionTypeOptionEvent.endAddingOption() = _EndAddingOption;
|
||||
const factory SelectOptionTypeOptionEvent.updateOption(SelectOption option) = _UpdateOption;
|
||||
const factory SelectOptionTypeOptionEvent.deleteOption(SelectOption option) = _DeleteOption;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class SelectOptionTypeOptionState with _$SelectOptionTypeOptionState {
|
||||
const factory SelectOptionTypeOptionState({
|
||||
required List<SelectOption> options,
|
||||
required bool isEditingOption,
|
||||
required Option<String> newOptionName,
|
||||
}) = _SelectOptionTyepOptionState;
|
||||
|
||||
factory SelectOptionTypeOptionState.initial(List<SelectOption> options) => SelectOptionTypeOptionState(
|
||||
options: options,
|
||||
isEditingOption: false,
|
||||
newOptionName: none(),
|
||||
);
|
||||
}
|
@ -1,92 +0,0 @@
|
||||
import 'package:flowy_sdk/log.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/selection_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_service.dart';
|
||||
|
||||
part 'single_select_bloc.freezed.dart';
|
||||
|
||||
class SingleSelectTypeOptionBloc extends Bloc<SingleSelectTypeOptionEvent, SingleSelectTypeOptionState> {
|
||||
final TypeOptionService service;
|
||||
|
||||
SingleSelectTypeOptionBloc(
|
||||
TypeOptionContext typeOptionContext,
|
||||
) : service = TypeOptionService(gridId: typeOptionContext.gridId, fieldId: typeOptionContext.field.id),
|
||||
super(
|
||||
SingleSelectTypeOptionState.initial(SingleSelectTypeOption.fromBuffer(typeOptionContext.data)),
|
||||
) {
|
||||
on<SingleSelectTypeOptionEvent>(
|
||||
(event, emit) async {
|
||||
await event.map(
|
||||
createOption: (_CreateOption value) async {
|
||||
final result = await service.newOption(name: value.optionName);
|
||||
result.fold(
|
||||
(option) {
|
||||
emit(state.copyWith(typeOption: _insertOption(option)));
|
||||
},
|
||||
(err) => Log.error(err),
|
||||
);
|
||||
},
|
||||
updateOption: (_UpdateOption value) async {
|
||||
emit(state.copyWith(typeOption: _updateOption(value.option)));
|
||||
},
|
||||
deleteOption: (_DeleteOption value) {
|
||||
emit(state.copyWith(typeOption: _deleteOption(value.option)));
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
return super.close();
|
||||
}
|
||||
|
||||
SingleSelectTypeOption _insertOption(SelectOption option) {
|
||||
state.typeOption.freeze();
|
||||
return state.typeOption.rebuild((typeOption) {
|
||||
typeOption.options.insert(0, option);
|
||||
});
|
||||
}
|
||||
|
||||
SingleSelectTypeOption _updateOption(SelectOption option) {
|
||||
state.typeOption.freeze();
|
||||
return state.typeOption.rebuild((typeOption) {
|
||||
final index = typeOption.options.indexWhere((element) => element.id == option.id);
|
||||
if (index != -1) {
|
||||
typeOption.options[index] = option;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
SingleSelectTypeOption _deleteOption(SelectOption option) {
|
||||
state.typeOption.freeze();
|
||||
return state.typeOption.rebuild((typeOption) {
|
||||
final index = typeOption.options.indexWhere((element) => element.id == option.id);
|
||||
if (index != -1) {
|
||||
typeOption.options.removeAt(index);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class SingleSelectTypeOptionEvent with _$SingleSelectTypeOptionEvent {
|
||||
const factory SingleSelectTypeOptionEvent.createOption(String optionName) = _CreateOption;
|
||||
const factory SingleSelectTypeOptionEvent.updateOption(SelectOption option) = _UpdateOption;
|
||||
const factory SingleSelectTypeOptionEvent.deleteOption(SelectOption option) = _DeleteOption;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class SingleSelectTypeOptionState with _$SingleSelectTypeOptionState {
|
||||
const factory SingleSelectTypeOptionState({
|
||||
required SingleSelectTypeOption typeOption,
|
||||
}) = _SingleSelectTypeOptionState;
|
||||
|
||||
factory SingleSelectTypeOptionState.initial(SingleSelectTypeOption typeOption) => SingleSelectTypeOptionState(
|
||||
typeOption: typeOption,
|
||||
);
|
||||
}
|
@ -0,0 +1,78 @@
|
||||
import 'package:app_flowy/workspace/application/grid/field/field_service.dart';
|
||||
import 'package:flowy_sdk/log.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/selection_type_option.pb.dart';
|
||||
import 'dart:async';
|
||||
import 'package:protobuf/protobuf.dart';
|
||||
import 'select_option_type_option_bloc.dart';
|
||||
import 'type_option_service.dart';
|
||||
|
||||
class SingleSelectTypeOptionContext extends TypeOptionContext<SingleSelectTypeOption>
|
||||
with SelectOptionTypeOptionAction {
|
||||
final TypeOptionService service;
|
||||
|
||||
SingleSelectTypeOptionContext({
|
||||
required SingleSelectTypeOptionDataBuilder dataBuilder,
|
||||
required GridFieldContext fieldContext,
|
||||
}) : service = TypeOptionService(
|
||||
gridId: fieldContext.gridId,
|
||||
fieldId: fieldContext.field.id,
|
||||
),
|
||||
super(dataBuilder: dataBuilder, fieldContext: fieldContext);
|
||||
|
||||
@override
|
||||
List<SelectOption> Function(SelectOption) get deleteOption {
|
||||
return (SelectOption option) {
|
||||
typeOption.freeze();
|
||||
typeOption = typeOption.rebuild((typeOption) {
|
||||
final index = typeOption.options.indexWhere((element) => element.id == option.id);
|
||||
if (index != -1) {
|
||||
typeOption.options.removeAt(index);
|
||||
}
|
||||
});
|
||||
return typeOption.options;
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<SelectOption>> Function(String) get insertOption {
|
||||
return (String optionName) {
|
||||
return service.newOption(name: optionName).then((result) {
|
||||
return result.fold(
|
||||
(option) {
|
||||
typeOption.freeze();
|
||||
typeOption = typeOption.rebuild((typeOption) {
|
||||
typeOption.options.insert(0, option);
|
||||
});
|
||||
|
||||
return typeOption.options;
|
||||
},
|
||||
(err) {
|
||||
Log.error(err);
|
||||
return typeOption.options;
|
||||
},
|
||||
);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
List<SelectOption> Function(SelectOption) get udpateOption {
|
||||
return (SelectOption option) {
|
||||
typeOption.freeze();
|
||||
typeOption = typeOption.rebuild((typeOption) {
|
||||
final index = typeOption.options.indexWhere((element) => element.id == option.id);
|
||||
if (index != -1) {
|
||||
typeOption.options[index] = option;
|
||||
}
|
||||
});
|
||||
return typeOption.options;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class SingleSelectTypeOptionDataBuilder extends TypeOptionDataBuilder<SingleSelectTypeOption> {
|
||||
@override
|
||||
SingleSelectTypeOption fromBuffer(List<int> buffer) {
|
||||
return SingleSelectTypeOption.fromBuffer(buffer);
|
||||
}
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:app_flowy/workspace/application/grid/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';
|
||||
@ -7,6 +8,7 @@ import 'package:flowy_sdk/protobuf/flowy-grid-data-model/grid.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/selection_type_option.pb.dart';
|
||||
import 'package:protobuf/protobuf.dart';
|
||||
|
||||
class TypeOptionService {
|
||||
final String gridId;
|
||||
@ -32,13 +34,76 @@ class TypeOptionService {
|
||||
}
|
||||
}
|
||||
|
||||
class TypeOptionContext {
|
||||
abstract class TypeOptionDataBuilder<T> {
|
||||
T fromBuffer(List<int> buffer);
|
||||
}
|
||||
|
||||
class TypeOptionContext<T extends GeneratedMessage> {
|
||||
T? _typeOptionObject;
|
||||
final GridFieldContext _fieldContext;
|
||||
final TypeOptionDataBuilder<T> dataBuilder;
|
||||
|
||||
TypeOptionContext({
|
||||
required this.dataBuilder,
|
||||
required GridFieldContext fieldContext,
|
||||
}) : _fieldContext = fieldContext;
|
||||
|
||||
String get gridId => _fieldContext.gridId;
|
||||
|
||||
Field get field => _fieldContext.field;
|
||||
|
||||
T get typeOption {
|
||||
if (_typeOptionObject != null) {
|
||||
return _typeOptionObject!;
|
||||
}
|
||||
|
||||
final T object = dataBuilder.fromBuffer(_fieldContext.typeOptionData);
|
||||
_typeOptionObject = object;
|
||||
return object;
|
||||
}
|
||||
|
||||
set typeOption(T typeOption) {
|
||||
_fieldContext.typeOptionData = typeOption.writeToBuffer();
|
||||
_typeOptionObject = typeOption;
|
||||
}
|
||||
}
|
||||
|
||||
abstract class TypeOptionFieldDelegate {
|
||||
void onFieldChanged(void Function(String) callback);
|
||||
void dispose();
|
||||
}
|
||||
|
||||
class TypeOptionContext2<T> {
|
||||
final String gridId;
|
||||
final Field field;
|
||||
final Uint8List data;
|
||||
const TypeOptionContext({
|
||||
final FieldService _fieldService;
|
||||
T? _data;
|
||||
final TypeOptionDataBuilder dataBuilder;
|
||||
|
||||
TypeOptionContext2({
|
||||
required this.gridId,
|
||||
required this.field,
|
||||
required this.data,
|
||||
});
|
||||
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),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
import 'dart:async';
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-folder-data-model/view.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid-data-model/protobuf.dart';
|
||||
@ -8,6 +9,7 @@ import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'cell/cell_service/cell_service.dart';
|
||||
import 'grid_service.dart';
|
||||
import 'row/row_service.dart';
|
||||
import 'dart:collection';
|
||||
|
||||
part 'grid_bloc.freezed.dart';
|
||||
|
||||
@ -33,19 +35,19 @@ class GridBloc extends Bloc<GridEvent, GridState> {
|
||||
|
||||
on<GridEvent>(
|
||||
(event, emit) async {
|
||||
await event.map(
|
||||
initial: (InitialGrid value) async {
|
||||
await event.when(
|
||||
initial: () async {
|
||||
_startListening();
|
||||
await _loadGrid(emit);
|
||||
},
|
||||
createRow: (_CreateRow value) {
|
||||
createRow: () {
|
||||
_gridService.createRow();
|
||||
},
|
||||
didReceiveRowUpdate: (_DidReceiveRowUpdate value) {
|
||||
emit(state.copyWith(rows: value.rows, listState: value.listState));
|
||||
didReceiveRowUpdate: (rows, listState) {
|
||||
emit(state.copyWith(rows: rows, listState: listState));
|
||||
},
|
||||
didReceiveFieldUpdate: (_DidReceiveFieldUpdate value) {
|
||||
emit(state.copyWith(rows: rowCache.clonedRows, fields: value.fields));
|
||||
didReceiveFieldUpdate: (fields) {
|
||||
emit(state.copyWith(rows: rowCache.clonedRows, fields: GridFieldEquatable(fields)));
|
||||
},
|
||||
);
|
||||
},
|
||||
@ -93,7 +95,7 @@ class GridBloc extends Bloc<GridEvent, GridState> {
|
||||
|
||||
emit(state.copyWith(
|
||||
grid: Some(grid),
|
||||
fields: fieldCache.clonedFields,
|
||||
fields: GridFieldEquatable(fieldCache.fields),
|
||||
rows: rowCache.clonedRows,
|
||||
loadingState: GridLoadingState.finish(left(unit)),
|
||||
));
|
||||
@ -117,14 +119,14 @@ class GridState with _$GridState {
|
||||
const factory GridState({
|
||||
required String gridId,
|
||||
required Option<Grid> grid,
|
||||
required List<Field> fields,
|
||||
required GridFieldEquatable fields,
|
||||
required List<GridRow> rows,
|
||||
required GridLoadingState loadingState,
|
||||
required GridRowChangeReason listState,
|
||||
}) = _GridState;
|
||||
|
||||
factory GridState.initial(String gridId) => GridState(
|
||||
fields: [],
|
||||
fields: const GridFieldEquatable([]),
|
||||
rows: [],
|
||||
grid: none(),
|
||||
gridId: gridId,
|
||||
@ -138,3 +140,19 @@ class GridLoadingState with _$GridLoadingState {
|
||||
const factory GridLoadingState.loading() = _Loading;
|
||||
const factory GridLoadingState.finish(Either<Unit, FlowyError> successOrFail) = _Finish;
|
||||
}
|
||||
|
||||
class GridFieldEquatable extends Equatable {
|
||||
final List<Field> _fields;
|
||||
|
||||
const GridFieldEquatable(List<Field> fields) : _fields = fields;
|
||||
|
||||
@override
|
||||
List<Object?> get props {
|
||||
return [
|
||||
_fields.length,
|
||||
_fields.map((field) => field.width).reduce((value, element) => value + element),
|
||||
];
|
||||
}
|
||||
|
||||
UnmodifiableListView<Field> get value => UnmodifiableListView(_fields);
|
||||
}
|
||||
|
@ -15,7 +15,7 @@ class GridHeaderBloc extends Bloc<GridHeaderEvent, GridHeaderState> {
|
||||
GridHeaderBloc({
|
||||
required this.gridId,
|
||||
required this.fieldCache,
|
||||
}) : super(GridHeaderState.initial(fieldCache.clonedFields)) {
|
||||
}) : super(GridHeaderState.initial(fieldCache.fields)) {
|
||||
on<GridHeaderEvent>(
|
||||
(event, emit) async {
|
||||
await event.map(
|
||||
|
@ -1,3 +1,5 @@
|
||||
import 'dart:collection';
|
||||
|
||||
import 'package:app_flowy/workspace/application/grid/field/grid_listenr.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:flowy_sdk/dispatch/dispatch.dart';
|
||||
@ -6,8 +8,6 @@ import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-folder-data-model/view.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid-data-model/grid.pb.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
import 'cell/cell_service/cell_service.dart';
|
||||
import 'row/row_service.dart';
|
||||
|
||||
@ -59,7 +59,7 @@ typedef ChangesetListener = void Function(GridFieldChangeset);
|
||||
class GridFieldCache {
|
||||
final String gridId;
|
||||
late final GridFieldsListener _fieldListener;
|
||||
final FieldsNotifier _fieldNotifier = FieldsNotifier();
|
||||
FieldsNotifier? _fieldNotifier = FieldsNotifier();
|
||||
final List<ChangesetListener> _changesetListener = [];
|
||||
|
||||
GridFieldCache({required this.gridId}) {
|
||||
@ -81,15 +81,16 @@ class GridFieldCache {
|
||||
|
||||
Future<void> dispose() async {
|
||||
await _fieldListener.stop();
|
||||
_fieldNotifier.dispose();
|
||||
_fieldNotifier?.dispose();
|
||||
_fieldNotifier = null;
|
||||
}
|
||||
|
||||
UnmodifiableListView<Field> get unmodifiableFields => UnmodifiableListView(_fieldNotifier.fields);
|
||||
UnmodifiableListView<Field> get unmodifiableFields => UnmodifiableListView(_fieldNotifier?.fields ?? []);
|
||||
|
||||
List<Field> get clonedFields => [..._fieldNotifier.fields];
|
||||
List<Field> get fields => [..._fieldNotifier?.fields ?? []];
|
||||
|
||||
set fields(List<Field> fields) {
|
||||
_fieldNotifier.fields = [...fields];
|
||||
_fieldNotifier?.fields = [...fields];
|
||||
}
|
||||
|
||||
VoidCallback addListener(
|
||||
@ -100,7 +101,7 @@ class GridFieldCache {
|
||||
}
|
||||
|
||||
if (onChanged != null) {
|
||||
onChanged(clonedFields);
|
||||
onChanged(fields);
|
||||
}
|
||||
|
||||
if (listener != null) {
|
||||
@ -108,12 +109,12 @@ class GridFieldCache {
|
||||
}
|
||||
}
|
||||
|
||||
_fieldNotifier.addListener(f);
|
||||
_fieldNotifier?.addListener(f);
|
||||
return f;
|
||||
}
|
||||
|
||||
void removeListener(VoidCallback f) {
|
||||
_fieldNotifier.removeListener(f);
|
||||
_fieldNotifier?.removeListener(f);
|
||||
}
|
||||
|
||||
void addChangesetListener(ChangesetListener listener) {
|
||||
@ -131,43 +132,43 @@ class GridFieldCache {
|
||||
if (deletedFields.isEmpty) {
|
||||
return;
|
||||
}
|
||||
final List<Field> fields = _fieldNotifier.fields;
|
||||
final List<Field> newFields = fields;
|
||||
final Map<String, FieldOrder> deletedFieldMap = {
|
||||
for (var fieldOrder in deletedFields) fieldOrder.fieldId: fieldOrder
|
||||
};
|
||||
|
||||
fields.retainWhere((field) => (deletedFieldMap[field.id] == null));
|
||||
_fieldNotifier.fields = fields;
|
||||
newFields.retainWhere((field) => (deletedFieldMap[field.id] == null));
|
||||
_fieldNotifier?.fields = newFields;
|
||||
}
|
||||
|
||||
void _insertFields(List<IndexField> insertedFields) {
|
||||
if (insertedFields.isEmpty) {
|
||||
return;
|
||||
}
|
||||
final List<Field> fields = _fieldNotifier.fields;
|
||||
final List<Field> newFields = fields;
|
||||
for (final indexField in insertedFields) {
|
||||
if (fields.length > indexField.index) {
|
||||
fields.insert(indexField.index, indexField.field_1);
|
||||
if (newFields.length > indexField.index) {
|
||||
newFields.insert(indexField.index, indexField.field_1);
|
||||
} else {
|
||||
fields.add(indexField.field_1);
|
||||
newFields.add(indexField.field_1);
|
||||
}
|
||||
}
|
||||
_fieldNotifier.fields = fields;
|
||||
_fieldNotifier?.fields = newFields;
|
||||
}
|
||||
|
||||
void _updateFields(List<Field> updatedFields) {
|
||||
if (updatedFields.isEmpty) {
|
||||
return;
|
||||
}
|
||||
final List<Field> fields = _fieldNotifier.fields;
|
||||
final List<Field> newFields = fields;
|
||||
for (final updatedField in updatedFields) {
|
||||
final index = fields.indexWhere((field) => field.id == updatedField.id);
|
||||
final index = newFields.indexWhere((field) => field.id == updatedField.id);
|
||||
if (index != -1) {
|
||||
fields.removeAt(index);
|
||||
fields.insert(index, updatedField);
|
||||
newFields.removeAt(index);
|
||||
newFields.insert(index, updatedField);
|
||||
}
|
||||
}
|
||||
_fieldNotifier.fields = fields;
|
||||
_fieldNotifier?.fields = newFields;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -13,12 +13,12 @@ export 'field/field_editor_pannel_bloc.dart';
|
||||
// Field Type Option
|
||||
export 'field/type_option/date_bloc.dart';
|
||||
export 'field/type_option/number_bloc.dart';
|
||||
export 'field/type_option/single_select_bloc.dart';
|
||||
export 'field/type_option/single_select_type_option.dart';
|
||||
|
||||
// Cell
|
||||
export 'cell/text_cell_bloc.dart';
|
||||
export 'cell/number_cell_bloc.dart';
|
||||
export 'cell/selection_cell_bloc.dart';
|
||||
export 'cell/select_option_cell_bloc.dart';
|
||||
export 'cell/date_cell_bloc.dart';
|
||||
export 'cell/checkbox_cell_bloc.dart';
|
||||
export 'cell/cell_service/cell_service.dart';
|
||||
|
@ -30,7 +30,7 @@ class RowBloc extends Bloc<RowEvent, RowState> {
|
||||
_rowService.createRow();
|
||||
},
|
||||
didReceiveCellDatas: (_DidReceiveCellDatas value) async {
|
||||
final fields = value.gridCellMap.values.map((e) => CellSnapshot(e.field)).toList();
|
||||
final fields = value.gridCellMap.values.map((e) => GridCellEquatable(e.field)).toList();
|
||||
final snapshots = UnmodifiableListView(fields);
|
||||
emit(state.copyWith(
|
||||
gridCellMap: value.gridCellMap,
|
||||
@ -74,26 +74,27 @@ class RowState with _$RowState {
|
||||
const factory RowState({
|
||||
required GridRow rowData,
|
||||
required GridCellMap gridCellMap,
|
||||
required UnmodifiableListView<CellSnapshot> snapshots,
|
||||
required UnmodifiableListView<GridCellEquatable> snapshots,
|
||||
GridRowChangeReason? changeReason,
|
||||
}) = _RowState;
|
||||
|
||||
factory RowState.initial(GridRow rowData, GridCellMap cellDataMap) => RowState(
|
||||
rowData: rowData,
|
||||
gridCellMap: cellDataMap,
|
||||
snapshots: UnmodifiableListView(cellDataMap.values.map((e) => CellSnapshot(e.field)).toList()),
|
||||
snapshots: UnmodifiableListView(cellDataMap.values.map((e) => GridCellEquatable(e.field)).toList()),
|
||||
);
|
||||
}
|
||||
|
||||
class CellSnapshot extends Equatable {
|
||||
class GridCellEquatable extends Equatable {
|
||||
final Field _field;
|
||||
|
||||
const CellSnapshot(Field field) : _field = field;
|
||||
const GridCellEquatable(Field field) : _field = field;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
_field.id,
|
||||
_field.fieldType,
|
||||
_field.visibility,
|
||||
_field.width,
|
||||
];
|
||||
}
|
||||
|
@ -14,7 +14,7 @@ class GridPropertyBloc extends Bloc<GridPropertyEvent, GridPropertyState> {
|
||||
|
||||
GridPropertyBloc({required String gridId, required GridFieldCache fieldCache})
|
||||
: _fieldCache = fieldCache,
|
||||
super(GridPropertyState.initial(gridId, fieldCache.clonedFields)) {
|
||||
super(GridPropertyState.initial(gridId, fieldCache.fields)) {
|
||||
on<GridPropertyEvent>(
|
||||
(event, emit) async {
|
||||
await event.map(
|
||||
|
@ -49,6 +49,9 @@ class HomeBloc extends Bloc<HomeEvent, HomeState> {
|
||||
unauthorized: (_Unauthorized value) {
|
||||
emit(state.copyWith(unauthorized: true));
|
||||
},
|
||||
collapseMenu: (e) {
|
||||
emit(state.copyWith(isMenuCollapsed: !state.isMenuCollapsed));
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
@ -77,6 +80,7 @@ class HomeEvent with _$HomeEvent {
|
||||
const factory HomeEvent.dismissEditPannel() = _DismissEditPannel;
|
||||
const factory HomeEvent.didReceiveWorkspaceSetting(CurrentWorkspaceSetting setting) = _DidReceiveWorkspaceSetting;
|
||||
const factory HomeEvent.unauthorized(String msg) = _Unauthorized;
|
||||
const factory HomeEvent.collapseMenu() = _CollapseMenu;
|
||||
}
|
||||
|
||||
@freezed
|
||||
@ -87,6 +91,7 @@ class HomeState with _$HomeState {
|
||||
required Option<EditPannelContext> pannelContext,
|
||||
required CurrentWorkspaceSetting workspaceSetting,
|
||||
required bool unauthorized,
|
||||
required bool isMenuCollapsed,
|
||||
}) = _HomeState;
|
||||
|
||||
factory HomeState.initial(CurrentWorkspaceSetting workspaceSetting) => HomeState(
|
||||
@ -95,5 +100,6 @@ class HomeState with _$HomeState {
|
||||
pannelContext: none(),
|
||||
workspaceSetting: workspaceSetting,
|
||||
unauthorized: false,
|
||||
isMenuCollapsed: false,
|
||||
);
|
||||
}
|
||||
|
@ -25,10 +25,6 @@ class MenuBloc extends Bloc<MenuEvent, MenuState> {
|
||||
listener.start(addAppCallback: _handleAppsOrFail);
|
||||
await _fetchApps(emit);
|
||||
},
|
||||
collapse: (e) async {
|
||||
final isCollapse = state.isCollapse;
|
||||
emit(state.copyWith(isCollapse: !isCollapse));
|
||||
},
|
||||
openPage: (e) async {
|
||||
emit(state.copyWith(plugin: e.plugin));
|
||||
},
|
||||
@ -94,7 +90,6 @@ class MenuBloc extends Bloc<MenuEvent, MenuState> {
|
||||
@freezed
|
||||
class MenuEvent with _$MenuEvent {
|
||||
const factory MenuEvent.initial() = _Initial;
|
||||
const factory MenuEvent.collapse() = _Collapse;
|
||||
const factory MenuEvent.openPage(Plugin plugin) = _OpenPage;
|
||||
const factory MenuEvent.createApp(String name, {String? desc}) = _CreateApp;
|
||||
const factory MenuEvent.moveApp(int fromIndex, int toIndex) = _MoveApp;
|
||||
@ -104,14 +99,12 @@ class MenuEvent with _$MenuEvent {
|
||||
@freezed
|
||||
class MenuState with _$MenuState {
|
||||
const factory MenuState({
|
||||
required bool isCollapse,
|
||||
required List<App> apps,
|
||||
required Either<Unit, FlowyError> successOrFailure,
|
||||
required Plugin plugin,
|
||||
}) = _MenuState;
|
||||
|
||||
factory MenuState.initial() => MenuState(
|
||||
isCollapse: false,
|
||||
apps: [],
|
||||
successOrFailure: left(unit),
|
||||
plugin: makePlugin(pluginType: DefaultPlugin.blank.type()),
|
||||
|
@ -18,7 +18,6 @@ import 'home_stack.dart';
|
||||
import 'menu/menu.dart';
|
||||
|
||||
class HomeScreen extends StatefulWidget {
|
||||
static GlobalKey<ScaffoldState> scaffoldKey = GlobalKey();
|
||||
final UserProfile user;
|
||||
final CurrentWorkspaceSetting workspaceSetting;
|
||||
const HomeScreen(this.user, this.workspaceSetting, {Key? key}) : super(key: key);
|
||||
@ -52,7 +51,6 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
),
|
||||
],
|
||||
child: Scaffold(
|
||||
key: HomeScreen.scaffoldKey,
|
||||
body: BlocListener<HomeBloc, HomeState>(
|
||||
listenWhen: (p, c) => p.unauthorized != c.unauthorized,
|
||||
listener: (context, state) {
|
||||
|
@ -1,27 +1,26 @@
|
||||
import 'dart:io' show Platform;
|
||||
|
||||
import 'package:app_flowy/startup/startup.dart';
|
||||
import 'package:app_flowy/workspace/presentation/home/home_screen.dart';
|
||||
import 'package:app_flowy/workspace/application/home/home_bloc.dart';
|
||||
import 'package:app_flowy/workspace/presentation/home/toast.dart';
|
||||
import 'package:flowy_infra/theme.dart';
|
||||
import 'package:flowy_sdk/log.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:time/time.dart';
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
|
||||
import 'package:app_flowy/plugin/plugin.dart';
|
||||
import 'package:app_flowy/workspace/presentation/plugins/blank/blank.dart';
|
||||
import 'package:app_flowy/workspace/presentation/home/home_sizes.dart';
|
||||
import 'package:app_flowy/workspace/presentation/home/navigation.dart';
|
||||
import 'package:app_flowy/core/frameless_window.dart';
|
||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/extension.dart';
|
||||
import 'package:flowy_infra/notifier.dart';
|
||||
|
||||
typedef NavigationCallback = void Function(String id);
|
||||
|
||||
late FToast fToast;
|
||||
|
||||
class HomeStack extends StatelessWidget {
|
||||
static GlobalKey<ScaffoldState> scaffoldKey = GlobalKey();
|
||||
// final Size size;
|
||||
const HomeStack({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
@ -69,8 +68,7 @@ class _FadingIndexedStackState extends State<FadingIndexedStack> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
fToast = FToast();
|
||||
fToast.init(HomeScreen.scaffoldKey.currentState!.context);
|
||||
initToastWithContext(context);
|
||||
}
|
||||
|
||||
@override
|
||||
@ -152,7 +150,7 @@ class HomeStackManager {
|
||||
child: Selector<HomeStackNotifier, Widget>(
|
||||
selector: (context, notifier) => notifier.titleWidget,
|
||||
builder: (context, widget, child) {
|
||||
return const HomeTopBar();
|
||||
return const MoveWindowDetector(child: HomeTopBar());
|
||||
},
|
||||
),
|
||||
);
|
||||
@ -191,6 +189,14 @@ class HomeTopBar extends StatelessWidget {
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
BlocBuilder<HomeBloc, HomeState>(
|
||||
buildWhen: ((previous, current) => previous.isMenuCollapsed != current.isMenuCollapsed),
|
||||
builder: (context, state) {
|
||||
if (state.isMenuCollapsed && Platform.isMacOS) {
|
||||
return const HSpace(80);
|
||||
}
|
||||
return const HSpace(0);
|
||||
}),
|
||||
const FlowyNavigation(),
|
||||
const HSpace(16),
|
||||
ChangeNotifierProvider.value(
|
||||
|
@ -28,7 +28,7 @@ class AddButton extends StatelessWidget {
|
||||
onSelected: onSelected,
|
||||
).show(context);
|
||||
},
|
||||
icon: svgWidget("home/add").padding(horizontal: 3, vertical: 3),
|
||||
icon: svgWidget("home/add", color: theme.iconColor).padding(horizontal: 3, vertical: 3),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -46,8 +46,8 @@ class ActionList {
|
||||
return CreateItem(
|
||||
pluginBuilder: pluginBuilder,
|
||||
onSelected: (builder) {
|
||||
FlowyOverlay.of(buildContext).remove(_identifier);
|
||||
onSelected(builder);
|
||||
FlowyOverlay.of(buildContext).remove(_identifier);
|
||||
},
|
||||
);
|
||||
},
|
||||
|
@ -26,7 +26,7 @@ class ViewSection extends StatelessWidget {
|
||||
listenWhen: (p, c) => p.selectedView != c.selectedView,
|
||||
listener: (context, state) {
|
||||
if (state.selectedView != null) {
|
||||
WidgetsBinding.instance?.addPostFrameCallback((_) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
getIt<HomeStackManager>().setPlugin(state.selectedView!.plugin());
|
||||
});
|
||||
}
|
||||
|
@ -1,6 +1,8 @@
|
||||
export './app/header/header.dart';
|
||||
export './app/menu_app.dart';
|
||||
|
||||
import 'dart:io' show Platform;
|
||||
import 'package:app_flowy/workspace/presentation/home/home_sizes.dart';
|
||||
import 'package:app_flowy/workspace/presentation/home/home_stack.dart';
|
||||
import 'package:app_flowy/workspace/presentation/plugins/trash/menu.dart';
|
||||
import 'package:flowy_infra/notifier.dart';
|
||||
@ -18,7 +20,9 @@ import 'package:expandable/expandable.dart';
|
||||
import 'package:flowy_infra/time/duration.dart';
|
||||
import 'package:app_flowy/startup/startup.dart';
|
||||
import 'package:app_flowy/workspace/application/menu/menu_bloc.dart';
|
||||
import 'package:app_flowy/workspace/presentation/home/home_sizes.dart';
|
||||
import 'package:app_flowy/workspace/application/home/home_bloc.dart';
|
||||
import 'package:app_flowy/core/frameless_window.dart';
|
||||
// import 'package:app_flowy/workspace/presentation/home/home_sizes.dart';
|
||||
import 'package:flowy_infra/image.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
|
||||
|
||||
@ -59,10 +63,10 @@ class HomeMenu extends StatelessWidget {
|
||||
getIt<HomeStackManager>().setPlugin(state.plugin);
|
||||
},
|
||||
),
|
||||
BlocListener<MenuBloc, MenuState>(
|
||||
listenWhen: (p, c) => p.isCollapse != c.isCollapse,
|
||||
BlocListener<HomeBloc, HomeState>(
|
||||
listenWhen: (p, c) => p.isMenuCollapsed != c.isMenuCollapsed,
|
||||
listener: (context, state) {
|
||||
_collapsedNotifier.value = state.isCollapse;
|
||||
_collapsedNotifier.value = state.isMenuCollapsed;
|
||||
},
|
||||
)
|
||||
],
|
||||
@ -179,6 +183,17 @@ class MenuSharedState {
|
||||
|
||||
class MenuTopBar extends StatelessWidget {
|
||||
const MenuTopBar({Key? key}) : super(key: key);
|
||||
|
||||
Widget renderIcon(BuildContext context) {
|
||||
if (Platform.isMacOS) {
|
||||
return Container();
|
||||
}
|
||||
final theme = context.watch<AppTheme>();
|
||||
return (theme.isDark
|
||||
? svgWithSize("flowy_logo_dark_mode", const Size(92, 17))
|
||||
: svgWithSize("flowy_logo_with_text", const Size(92, 17)));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = context.watch<AppTheme>();
|
||||
@ -186,20 +201,19 @@ class MenuTopBar extends StatelessWidget {
|
||||
builder: (context, state) {
|
||||
return SizedBox(
|
||||
height: HomeSizes.topBarHeight,
|
||||
child: Row(
|
||||
child: MoveWindowDetector(
|
||||
child: Row(
|
||||
children: [
|
||||
(theme.isDark
|
||||
? svgWithSize("flowy_logo_dark_mode", const Size(92, 17))
|
||||
: svgWithSize("flowy_logo_with_text", const Size(92, 17))),
|
||||
renderIcon(context),
|
||||
const Spacer(),
|
||||
FlowyIconButton(
|
||||
width: 28,
|
||||
onPressed: () => context.read<MenuBloc>().add(const MenuEvent.collapse()),
|
||||
onPressed: () => context.read<HomeBloc>().add(const HomeEvent.collapseMenu()),
|
||||
iconPadding: const EdgeInsets.fromLTRB(4, 4, 4, 4),
|
||||
icon: svgWidget("home/hide_menu", color: theme.iconColor),
|
||||
)
|
||||
],
|
||||
),
|
||||
)),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
@ -1,3 +1,4 @@
|
||||
import 'package:app_flowy/workspace/application/home/home_bloc.dart';
|
||||
import 'package:app_flowy/workspace/presentation/home/home_stack.dart';
|
||||
import 'package:flowy_infra/image.dart';
|
||||
import 'package:flowy_infra/notifier.dart';
|
||||
@ -95,6 +96,7 @@ class FlowyNavigation extends StatelessWidget {
|
||||
width: 24,
|
||||
onPressed: () {
|
||||
notifier.value = false;
|
||||
ctx.read<HomeBloc>().add(const HomeEvent.collapseMenu());
|
||||
},
|
||||
iconPadding: const EdgeInsets.fromLTRB(2, 2, 2, 2),
|
||||
icon: svgWidget("home/hide_menu", color: theme.iconColor),
|
||||
|
@ -0,0 +1,37 @@
|
||||
import 'package:app_flowy/startup/startup.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
|
||||
class FlowyMessageToast extends StatelessWidget {
|
||||
final String message;
|
||||
const FlowyMessageToast({required this.message, Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
child: FlowyText.medium(message, color: Colors.white),
|
||||
),
|
||||
decoration: const BoxDecoration(
|
||||
borderRadius: BorderRadius.all(Radius.circular(4)),
|
||||
color: Colors.black,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void initToastWithContext(BuildContext context) {
|
||||
getIt<FToast>().init(context);
|
||||
}
|
||||
|
||||
void showMessageToast(String message) {
|
||||
final child = FlowyMessageToast(message: message);
|
||||
|
||||
getIt<FToast>().showToast(
|
||||
child: child,
|
||||
gravity: ToastGravity.BOTTOM,
|
||||
toastDuration: const Duration(seconds: 3),
|
||||
);
|
||||
}
|
@ -7,6 +7,7 @@ import 'package:app_flowy/workspace/application/appearance.dart';
|
||||
import 'package:app_flowy/workspace/application/doc/share_bloc.dart';
|
||||
import 'package:app_flowy/workspace/application/view/view_listener.dart';
|
||||
import 'package:app_flowy/workspace/presentation/home/home_stack.dart';
|
||||
import 'package:app_flowy/workspace/presentation/home/toast.dart';
|
||||
import 'package:app_flowy/workspace/presentation/plugins/widgets/left_bar_item.dart';
|
||||
import 'package:app_flowy/workspace/presentation/widgets/dialogs.dart';
|
||||
import 'package:app_flowy/workspace/presentation/widgets/pop_up_action.dart';
|
||||
@ -179,6 +180,7 @@ class DocumentShareButton extends StatelessWidget {
|
||||
switch (action) {
|
||||
case ShareAction.markdown:
|
||||
context.read<DocShareBloc>().add(const DocShareEvent.shareMarkdown());
|
||||
showMessageToast('Exported to: ${LocaleKeys.notifications_export_path.tr()}');
|
||||
break;
|
||||
case ShareAction.copyLink:
|
||||
FlowyAlertDialog(title: LocaleKeys.shareAction_workInProgress.tr()).show(context);
|
||||
|
@ -184,7 +184,7 @@ class _ToolbarButtonListState extends State<ToolbarButtonList> with WidgetsBindi
|
||||
// Listening to the WidgetsBinding instance is necessary so that we can
|
||||
// hide the arrows when the window gets a new size and thus the toolbar
|
||||
// becomes scrollable/unscrollable.
|
||||
WidgetsBinding.instance!.addObserver(this);
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
|
||||
// Workaround to allow the scroll controller attach to our ListView so that
|
||||
// we can detect if overflow arrows need to be shown on init.
|
||||
@ -226,7 +226,7 @@ class _ToolbarButtonListState extends State<ToolbarButtonList> with WidgetsBindi
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
WidgetsBinding.instance!.removeObserver(this);
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
@ -29,9 +29,9 @@ class ToolbarIconButton extends StatelessWidget {
|
||||
iconPadding: const EdgeInsets.symmetric(horizontal: 4, vertical: 4),
|
||||
onPressed: onPressed,
|
||||
width: width,
|
||||
icon: isToggled == true ? svgWidget(iconName, color: Colors.white) : svgWidget(iconName),
|
||||
icon: isToggled == true ? svgWidget(iconName, color: Colors.white) : svgWidget(iconName, color: theme.iconColor),
|
||||
fillColor: isToggled == true ? theme.main1 : theme.shader6,
|
||||
hoverColor: isToggled == true ? theme.main1 : theme.shader5,
|
||||
hoverColor: isToggled == true ? theme.main1 : theme.hover,
|
||||
tooltipText: tooltipText,
|
||||
);
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ import 'layout/sizes.dart';
|
||||
import 'widgets/row/grid_row.dart';
|
||||
import 'widgets/footer/grid_footer.dart';
|
||||
import 'widgets/header/grid_header.dart';
|
||||
import 'widgets/shortcuts.dart';
|
||||
import 'widgets/toolbar/grid_toolbar.dart';
|
||||
|
||||
class GridPage extends StatefulWidget {
|
||||
@ -40,7 +41,7 @@ class _GridPageState extends State<GridPage> {
|
||||
return state.loadingState.map(
|
||||
loading: (_) => const Center(child: CircularProgressIndicator.adaptive()),
|
||||
finish: (result) => result.successOrFail.fold(
|
||||
(_) => const FlowyGrid(),
|
||||
(_) => const GridShortcuts(child: FlowyGrid()),
|
||||
(err) => FlowyErrorPage(err.toString()),
|
||||
),
|
||||
);
|
||||
@ -91,9 +92,9 @@ class _FlowyGridState extends State<FlowyGrid> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<GridBloc, GridState>(
|
||||
buildWhen: (previous, current) => previous.fields.length != current.fields.length,
|
||||
buildWhen: (previous, current) => previous.fields != current.fields,
|
||||
builder: (context, state) {
|
||||
final contentWidth = GridLayout.headerWidth(state.fields);
|
||||
final contentWidth = GridLayout.headerWidth(state.fields.value);
|
||||
final child = _wrapScrollView(
|
||||
contentWidth,
|
||||
[
|
||||
|
@ -0,0 +1,198 @@
|
||||
import 'package:flowy_infra/image.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flowy_infra/theme.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:flowy_infra/size.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
|
||||
class GridCellAccessoryBuildContext {
|
||||
final BuildContext anchorContext;
|
||||
final bool isCellEditing;
|
||||
|
||||
GridCellAccessoryBuildContext({
|
||||
required this.anchorContext,
|
||||
required this.isCellEditing,
|
||||
});
|
||||
}
|
||||
|
||||
abstract class GridCellAccessory implements Widget {
|
||||
void onTap();
|
||||
|
||||
// The accessory will be hidden if enable() return false;
|
||||
bool enable() => true;
|
||||
}
|
||||
|
||||
class PrimaryCellAccessory extends StatelessWidget with GridCellAccessory {
|
||||
final VoidCallback onTapCallback;
|
||||
final bool isCellEditing;
|
||||
const PrimaryCellAccessory({
|
||||
required this.onTapCallback,
|
||||
required this.isCellEditing,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (isCellEditing) {
|
||||
return const SizedBox();
|
||||
} else {
|
||||
final theme = context.watch<AppTheme>();
|
||||
return svgWidget("grid/expander", color: theme.main1);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void onTap() => onTapCallback();
|
||||
|
||||
@override
|
||||
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;
|
||||
const AccessoryHover({
|
||||
required this.child,
|
||||
this.contentPadding = EdgeInsets.zero,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<AccessoryHover> createState() => _AccessoryHoverState();
|
||||
}
|
||||
|
||||
class _AccessoryHoverState extends State<AccessoryHover> {
|
||||
late AccessoryHoverState _hoverState;
|
||||
VoidCallback? _listenerFn;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_hoverState = AccessoryHoverState();
|
||||
_listenerFn = () => _hoverState.onHover = widget.child.onAccessoryHover?.value ?? false;
|
||||
widget.child.onAccessoryHover?.addListener(_listenerFn!);
|
||||
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_hoverState.dispose();
|
||||
|
||||
if (_listenerFn != null) {
|
||||
widget.child.onAccessoryHover?.removeListener(_listenerFn!);
|
||||
_listenerFn = null;
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
List<Widget> children = [
|
||||
const _Background(),
|
||||
Padding(padding: widget.contentPadding, child: widget.child),
|
||||
];
|
||||
|
||||
final accessoryBuilder = widget.child.accessoryBuilder;
|
||||
if (accessoryBuilder != null) {
|
||||
final accessories = accessoryBuilder((GridCellAccessoryBuildContext(
|
||||
anchorContext: context,
|
||||
isCellEditing: false,
|
||||
)));
|
||||
children.add(
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 6),
|
||||
child: CellAccessoryContainer(accessories: accessories),
|
||||
).positioned(right: 0),
|
||||
);
|
||||
}
|
||||
|
||||
return ChangeNotifierProvider.value(
|
||||
value: _hoverState,
|
||||
child: MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
opaque: false,
|
||||
onEnter: (p) => setState(() => _hoverState.onHover = true),
|
||||
onExit: (p) => setState(() => _hoverState.onHover = false),
|
||||
child: Stack(
|
||||
fit: StackFit.loose,
|
||||
alignment: AlignmentDirectional.center,
|
||||
children: children,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AccessoryHoverState extends ChangeNotifier {
|
||||
bool _onHover = false;
|
||||
|
||||
set onHover(bool value) {
|
||||
if (_onHover != value) {
|
||||
_onHover = value;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
bool get onHover => _onHover;
|
||||
}
|
||||
|
||||
class _Background extends StatelessWidget {
|
||||
const _Background({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = context.watch<AppTheme>();
|
||||
return Consumer<AccessoryHoverState>(
|
||||
builder: (context, state, child) {
|
||||
if (state.onHover) {
|
||||
return FlowyHoverContainer(
|
||||
style: HoverStyle(borderRadius: Corners.s6Border, hoverColor: theme.shader6),
|
||||
);
|
||||
} else {
|
||||
return const SizedBox();
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CellAccessoryContainer extends StatelessWidget {
|
||||
final List<GridCellAccessory> accessories;
|
||||
const CellAccessoryContainer({required this.accessories, Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = context.watch<AppTheme>();
|
||||
final children = accessories.where((accessory) => accessory.enable()).map((accessory) {
|
||||
final hover = FlowyHover(
|
||||
style: HoverStyle(hoverColor: theme.bg3, 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(),
|
||||
);
|
||||
}).toList();
|
||||
|
||||
return Wrap(children: children, spacing: 6);
|
||||
}
|
||||
}
|
@ -1,18 +1,16 @@
|
||||
import 'package:app_flowy/workspace/application/grid/cell/cell_service/cell_service.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid-data-model/grid.pb.dart' show FieldType;
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/row/grid_row.dart';
|
||||
import 'package:flowy_infra/theme.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:app_flowy/workspace/presentation/plugins/grid/src/layout/sizes.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'cell_accessory.dart';
|
||||
import 'cell_shortcuts.dart';
|
||||
import 'checkbox_cell.dart';
|
||||
import 'date_cell/date_cell.dart';
|
||||
import 'number_cell.dart';
|
||||
import 'selection_cell/selection_cell.dart';
|
||||
import 'select_option_cell/select_option_cell.dart';
|
||||
import 'text_cell.dart';
|
||||
import 'url_cell/url_cell.dart';
|
||||
|
||||
GridCellWidget buildGridCellWidget(GridCell gridCell, GridCellCache cellCache, {GridCellStyle? style}) {
|
||||
final key = ValueKey(gridCell.cellId());
|
||||
@ -32,10 +30,10 @@ GridCellWidget buildGridCellWidget(GridCell gridCell, GridCellCache cellCache, {
|
||||
return NumberCell(cellContextBuilder: cellContextBuilder, key: key);
|
||||
case FieldType.RichText:
|
||||
return GridTextCell(cellContextBuilder: cellContextBuilder, style: style, key: key);
|
||||
|
||||
default:
|
||||
throw UnimplementedError;
|
||||
case FieldType.URL:
|
||||
return GridURLCell(cellContextBuilder: cellContextBuilder, style: style, key: key);
|
||||
}
|
||||
throw UnimplementedError;
|
||||
}
|
||||
|
||||
class BlankCell extends StatelessWidget {
|
||||
@ -47,26 +45,132 @@ class BlankCell extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
abstract class GridCellWidget extends HoverWidget {
|
||||
@override
|
||||
final ValueNotifier<bool> onFocus = ValueNotifier<bool>(false);
|
||||
abstract class CellEditable {
|
||||
GridCellFocusListener get beginFocus;
|
||||
|
||||
final GridCellRequestFocusNotifier requestFocus = GridCellRequestFocusNotifier();
|
||||
ValueNotifier<bool> get onCellFocus;
|
||||
|
||||
GridCellWidget({Key? key}) : super(key: key);
|
||||
ValueNotifier<bool> get onCellEditing;
|
||||
}
|
||||
|
||||
class GridCellRequestFocusNotifier extends ChangeNotifier {
|
||||
VoidCallback? _listener;
|
||||
abstract class GridCellWidget extends StatefulWidget implements CellAccessory, CellEditable, CellShortcuts {
|
||||
GridCellWidget({Key? key}) : super(key: key) {
|
||||
onCellEditing.addListener(() {
|
||||
onCellFocus.value = onCellEditing.value;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void addListener(VoidCallback listener) {
|
||||
final ValueNotifier<bool> onCellFocus = ValueNotifier<bool>(false);
|
||||
|
||||
// When the cell is focused, we assume that the accessory alse be hovered.
|
||||
@override
|
||||
ValueNotifier<bool> get onAccessoryHover => onCellFocus;
|
||||
|
||||
@override
|
||||
final ValueNotifier<bool> onCellEditing = ValueNotifier<bool>(false);
|
||||
|
||||
@override
|
||||
List<GridCellAccessory> Function(GridCellAccessoryBuildContext buildContext)? get accessoryBuilder => null;
|
||||
|
||||
@override
|
||||
final GridCellFocusListener beginFocus = GridCellFocusListener();
|
||||
|
||||
@override
|
||||
final Map<CellKeyboardKey, CellKeyboardAction> shortcutHandlers = {};
|
||||
}
|
||||
|
||||
abstract class GridCellState<T extends GridCellWidget> extends State<T> {
|
||||
@override
|
||||
void initState() {
|
||||
widget.beginFocus.setListener(() => requestBeginFocus());
|
||||
widget.shortcutHandlers[CellKeyboardKey.onCopy] = () => onCopy();
|
||||
widget.shortcutHandlers[CellKeyboardKey.onInsert] = () {
|
||||
Clipboard.getData("text/plain").then((data) {
|
||||
final s = data?.text;
|
||||
if (s is String) {
|
||||
onInsert(s);
|
||||
}
|
||||
});
|
||||
};
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant T oldWidget) {
|
||||
if (oldWidget != this) {
|
||||
widget.beginFocus.setListener(() => requestBeginFocus());
|
||||
}
|
||||
super.didUpdateWidget(oldWidget);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
widget.beginFocus.removeAllListener();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void requestBeginFocus();
|
||||
|
||||
String? onCopy() => null;
|
||||
|
||||
void onInsert(String value) {}
|
||||
}
|
||||
|
||||
abstract class GridFocusNodeCellState<T extends GridCellWidget> extends GridCellState<T> {
|
||||
SingleListenrFocusNode focusNode = SingleListenrFocusNode();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
widget.shortcutHandlers[CellKeyboardKey.onEnter] = () => focusNode.unfocus();
|
||||
_listenOnFocusNodeChanged();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant T oldWidget) {
|
||||
if (oldWidget != this) {
|
||||
_listenOnFocusNodeChanged();
|
||||
}
|
||||
super.didUpdateWidget(oldWidget);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
widget.shortcutHandlers.clear();
|
||||
focusNode.removeAllListener();
|
||||
focusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void requestBeginFocus() {
|
||||
if (focusNode.hasFocus == false && focusNode.canRequestFocus) {
|
||||
FocusScope.of(context).requestFocus(focusNode);
|
||||
}
|
||||
}
|
||||
|
||||
void _listenOnFocusNodeChanged() {
|
||||
widget.onCellEditing.value = focusNode.hasFocus;
|
||||
focusNode.setListener(() {
|
||||
widget.onCellEditing.value = focusNode.hasFocus;
|
||||
focusChanged();
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> focusChanged() async {}
|
||||
}
|
||||
|
||||
class GridCellFocusListener extends ChangeNotifier {
|
||||
VoidCallback? _listener;
|
||||
|
||||
void setListener(VoidCallback listener) {
|
||||
if (_listener != null) {
|
||||
removeListener(_listener!);
|
||||
}
|
||||
|
||||
_listener = listener;
|
||||
super.addListener(listener);
|
||||
addListener(listener);
|
||||
}
|
||||
|
||||
void removeAllListener() {
|
||||
@ -82,10 +186,10 @@ class GridCellRequestFocusNotifier extends ChangeNotifier {
|
||||
|
||||
abstract class GridCellStyle {}
|
||||
|
||||
class CellSingleFocusNode extends FocusNode {
|
||||
class SingleListenrFocusNode extends FocusNode {
|
||||
VoidCallback? _listener;
|
||||
|
||||
void setSingleListener(VoidCallback listener) {
|
||||
void setListener(VoidCallback listener) {
|
||||
if (_listener != null) {
|
||||
removeListener(_listener!);
|
||||
}
|
||||
@ -94,120 +198,9 @@ class CellSingleFocusNode extends FocusNode {
|
||||
super.addListener(listener);
|
||||
}
|
||||
|
||||
void removeSingleListener() {
|
||||
void removeAllListener() {
|
||||
if (_listener != null) {
|
||||
removeListener(_listener!);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class CellStateNotifier extends ChangeNotifier {
|
||||
bool _isFocus = false;
|
||||
bool _onEnter = false;
|
||||
|
||||
set isFocus(bool value) {
|
||||
if (_isFocus != value) {
|
||||
_isFocus = value;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
set onEnter(bool value) {
|
||||
if (_onEnter != value) {
|
||||
_onEnter = value;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
bool get isFocus => _isFocus;
|
||||
|
||||
bool get onEnter => _onEnter;
|
||||
}
|
||||
|
||||
class CellContainer extends StatelessWidget {
|
||||
final GridCellWidget child;
|
||||
final Widget? expander;
|
||||
final double width;
|
||||
final RegionStateNotifier rowStateNotifier;
|
||||
const CellContainer({
|
||||
Key? key,
|
||||
required this.child,
|
||||
required this.width,
|
||||
required this.rowStateNotifier,
|
||||
this.expander,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ChangeNotifierProxyProvider<RegionStateNotifier, CellStateNotifier>(
|
||||
create: (_) => CellStateNotifier(),
|
||||
update: (_, row, cell) => cell!..onEnter = row.onEnter,
|
||||
child: Selector<CellStateNotifier, bool>(
|
||||
selector: (context, notifier) => notifier.isFocus,
|
||||
builder: (context, isFocus, _) {
|
||||
Widget container = Center(child: child);
|
||||
child.onFocus.addListener(() {
|
||||
Provider.of<CellStateNotifier>(context, listen: false).isFocus = child.onFocus.value;
|
||||
});
|
||||
|
||||
if (expander != null) {
|
||||
container = _CellEnterRegion(child: container, expander: expander!);
|
||||
}
|
||||
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: () => child.requestFocus.notify(),
|
||||
child: Container(
|
||||
constraints: BoxConstraints(maxWidth: width, minHeight: 46),
|
||||
decoration: _makeBoxDecoration(context, isFocus),
|
||||
padding: GridSize.cellContentInsets,
|
||||
child: container,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
BoxDecoration _makeBoxDecoration(BuildContext context, bool isFocus) {
|
||||
final theme = context.watch<AppTheme>();
|
||||
if (isFocus) {
|
||||
final borderSide = BorderSide(color: theme.main1, width: 1.0);
|
||||
return BoxDecoration(border: Border.fromBorderSide(borderSide));
|
||||
} else {
|
||||
final borderSide = BorderSide(color: theme.shader5, width: 1.0);
|
||||
return BoxDecoration(border: Border(right: borderSide, bottom: borderSide));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _CellEnterRegion extends StatelessWidget {
|
||||
final Widget child;
|
||||
final Widget expander;
|
||||
const _CellEnterRegion({required this.child, required this.expander, Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<CellStateNotifier, bool>(
|
||||
selector: (context, notifier) => notifier.onEnter,
|
||||
builder: (context, onEnter, _) {
|
||||
List<Widget> children = [child];
|
||||
if (onEnter) {
|
||||
children.add(expander.positioned(right: 0));
|
||||
}
|
||||
|
||||
return MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
onEnter: (p) => Provider.of<CellStateNotifier>(context, listen: false).onEnter = true,
|
||||
onExit: (p) => Provider.of<CellStateNotifier>(context, listen: false).onEnter = false,
|
||||
child: Stack(
|
||||
alignment: AlignmentDirectional.center,
|
||||
fit: StackFit.expand,
|
||||
// alignment: AlignmentDirectional.centerEnd,
|
||||
children: children,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,140 @@
|
||||
import 'package:app_flowy/workspace/presentation/plugins/grid/src/layout/sizes.dart';
|
||||
import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/row/grid_row.dart';
|
||||
import 'package:flowy_infra/theme.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
|
||||
import 'cell_accessory.dart';
|
||||
import 'cell_builder.dart';
|
||||
import 'cell_shortcuts.dart';
|
||||
|
||||
class CellContainer extends StatelessWidget {
|
||||
final GridCellWidget child;
|
||||
final AccessoryBuilder? accessoryBuilder;
|
||||
final double width;
|
||||
final RegionStateNotifier rowStateNotifier;
|
||||
const CellContainer({
|
||||
Key? key,
|
||||
required this.child,
|
||||
required this.width,
|
||||
required this.rowStateNotifier,
|
||||
this.accessoryBuilder,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ChangeNotifierProxyProvider<RegionStateNotifier, CellContainerNotifier>(
|
||||
create: (_) => CellContainerNotifier(child),
|
||||
update: (_, rowStateNotifier, cellStateNotifier) => cellStateNotifier!..onEnter = rowStateNotifier.onEnter,
|
||||
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,
|
||||
));
|
||||
|
||||
if (accessories.isNotEmpty) {
|
||||
container = CellEnterRegion(child: container, accessories: accessories);
|
||||
}
|
||||
}
|
||||
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: () => child.beginFocus.notify(),
|
||||
child: Container(
|
||||
constraints: BoxConstraints(maxWidth: width, minHeight: 46),
|
||||
decoration: _makeBoxDecoration(context, isFocus),
|
||||
padding: GridSize.cellContentInsets,
|
||||
child: container,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
BoxDecoration _makeBoxDecoration(BuildContext context, bool isFocus) {
|
||||
final theme = context.watch<AppTheme>();
|
||||
if (isFocus) {
|
||||
final borderSide = BorderSide(color: theme.main1, width: 1.0);
|
||||
return BoxDecoration(border: Border.fromBorderSide(borderSide));
|
||||
} else {
|
||||
final borderSide = BorderSide(color: theme.shader5, width: 1.0);
|
||||
return BoxDecoration(border: Border(right: borderSide, bottom: borderSide));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class CellEnterRegion extends StatelessWidget {
|
||||
final Widget child;
|
||||
final List<GridCellAccessory> accessories;
|
||||
const CellEnterRegion({required this.child, required this.accessories, Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<CellContainerNotifier, bool>(
|
||||
selector: (context, notifier) => notifier.onEnter,
|
||||
builder: (context, onEnter, _) {
|
||||
List<Widget> children = [child];
|
||||
if (onEnter) {
|
||||
children.add(CellAccessoryContainer(accessories: accessories).positioned(right: 0));
|
||||
}
|
||||
|
||||
return MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
onEnter: (p) => Provider.of<CellContainerNotifier>(context, listen: false).onEnter = true,
|
||||
onExit: (p) => Provider.of<CellContainerNotifier>(context, listen: false).onEnter = false,
|
||||
child: Stack(
|
||||
alignment: AlignmentDirectional.center,
|
||||
fit: StackFit.expand,
|
||||
children: children,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CellContainerNotifier extends ChangeNotifier {
|
||||
final CellEditable cellEditable;
|
||||
bool mouted = false;
|
||||
VoidCallback? _onCellFocusListener;
|
||||
bool _isFocus = false;
|
||||
bool _onEnter = false;
|
||||
|
||||
CellContainerNotifier(this.cellEditable) {
|
||||
_onCellFocusListener = () => isFocus = cellEditable.onCellFocus.value;
|
||||
cellEditable.onCellFocus.addListener(_onCellFocusListener!);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
if (_onCellFocusListener != null) {
|
||||
cellEditable.onCellFocus.removeListener(_onCellFocusListener!);
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
set isFocus(bool value) {
|
||||
if (_isFocus != value) {
|
||||
_isFocus = value;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
set onEnter(bool value) {
|
||||
if (_onEnter != value) {
|
||||
_onEnter = value;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
bool get isFocus => _isFocus;
|
||||
|
||||
bool get onEnter => _onEnter;
|
||||
}
|
@ -0,0 +1,96 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
typedef CellKeyboardAction = dynamic Function();
|
||||
|
||||
enum CellKeyboardKey {
|
||||
onEnter,
|
||||
onCopy,
|
||||
onInsert,
|
||||
}
|
||||
|
||||
abstract class CellShortcuts extends Widget {
|
||||
const CellShortcuts({Key? key}) : super(key: key);
|
||||
|
||||
Map<CellKeyboardKey, CellKeyboardAction> get shortcutHandlers;
|
||||
}
|
||||
|
||||
class GridCellShortcuts extends StatelessWidget {
|
||||
final CellShortcuts child;
|
||||
const GridCellShortcuts({required this.child, Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Shortcuts(
|
||||
shortcuts: {
|
||||
LogicalKeySet(LogicalKeyboardKey.enter): const GridCellEnterIdent(),
|
||||
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.keyC): const GridCellCopyIntent(),
|
||||
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.keyV): const GridCellInsertIntent(),
|
||||
},
|
||||
child: Actions(
|
||||
actions: {
|
||||
GridCellEnterIdent: GridCellEnterAction(child: child),
|
||||
GridCellCopyIntent: GridCellCopyAction(child: child),
|
||||
GridCellInsertIntent: GridCellInsertAction(child: child),
|
||||
},
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class GridCellEnterIdent extends Intent {
|
||||
const GridCellEnterIdent();
|
||||
}
|
||||
|
||||
class GridCellEnterAction extends Action<GridCellEnterIdent> {
|
||||
final CellShortcuts child;
|
||||
GridCellEnterAction({required this.child});
|
||||
|
||||
@override
|
||||
void invoke(covariant GridCellEnterIdent intent) {
|
||||
final callback = child.shortcutHandlers[CellKeyboardKey.onEnter];
|
||||
if (callback != null) {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class GridCellCopyIntent extends Intent {
|
||||
const GridCellCopyIntent();
|
||||
}
|
||||
|
||||
class GridCellCopyAction extends Action<GridCellCopyIntent> {
|
||||
final CellShortcuts child;
|
||||
GridCellCopyAction({required this.child});
|
||||
|
||||
@override
|
||||
void invoke(covariant GridCellCopyIntent intent) {
|
||||
final callback = child.shortcutHandlers[CellKeyboardKey.onCopy];
|
||||
if (callback == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final s = callback();
|
||||
if (s is String) {
|
||||
Clipboard.setData(ClipboardData(text: s));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class GridCellInsertIntent extends Intent {
|
||||
const GridCellInsertIntent();
|
||||
}
|
||||
|
||||
class GridCellInsertAction extends Action<GridCellInsertIntent> {
|
||||
final CellShortcuts child;
|
||||
GridCellInsertAction({required this.child});
|
||||
|
||||
@override
|
||||
void invoke(covariant GridCellInsertIntent intent) {
|
||||
final callback = child.shortcutHandlers[CellKeyboardKey.onInsert];
|
||||
if (callback != null) {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
}
|
@ -14,17 +14,16 @@ class CheckboxCell extends GridCellWidget {
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<CheckboxCell> createState() => _CheckboxCellState();
|
||||
GridCellState<CheckboxCell> createState() => _CheckboxCellState();
|
||||
}
|
||||
|
||||
class _CheckboxCellState extends State<CheckboxCell> {
|
||||
class _CheckboxCellState extends GridCellState<CheckboxCell> {
|
||||
late CheckboxCellBloc _cellBloc;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
final cellContext = widget.cellContextBuilder.build();
|
||||
_cellBloc = getIt<CheckboxCellBloc>(param1: cellContext)..add(const CheckboxCellEvent.initial());
|
||||
_listenCellRequestFocus();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@ -41,7 +40,7 @@ class _CheckboxCellState extends State<CheckboxCell> {
|
||||
onPressed: () => context.read<CheckboxCellBloc>().add(const CheckboxCellEvent.select()),
|
||||
iconPadding: EdgeInsets.zero,
|
||||
icon: icon,
|
||||
width: 23,
|
||||
width: 20,
|
||||
),
|
||||
);
|
||||
},
|
||||
@ -49,22 +48,23 @@ class _CheckboxCellState extends State<CheckboxCell> {
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant CheckboxCell oldWidget) {
|
||||
_listenCellRequestFocus();
|
||||
super.didUpdateWidget(oldWidget);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> dispose() async {
|
||||
widget.requestFocus.removeAllListener();
|
||||
_cellBloc.close();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _listenCellRequestFocus() {
|
||||
widget.requestFocus.addListener(() {
|
||||
_cellBloc.add(const CheckboxCellEvent.select());
|
||||
});
|
||||
@override
|
||||
void requestBeginFocus() {
|
||||
_cellBloc.add(const CheckboxCellEvent.select());
|
||||
}
|
||||
|
||||
@override
|
||||
String? onCopy() {
|
||||
if (_cellBloc.state.isSelected) {
|
||||
return "Yes";
|
||||
} else {
|
||||
return "No";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ import 'package:app_flowy/startup/startup.dart';
|
||||
import 'package:app_flowy/workspace/application/grid/prelude.dart';
|
||||
|
||||
import '../cell_builder.dart';
|
||||
import 'calendar.dart';
|
||||
import 'date_editor.dart';
|
||||
|
||||
class DateCellStyle extends GridCellStyle {
|
||||
Alignment alignment;
|
||||
@ -35,10 +35,10 @@ class DateCell extends GridCellWidget {
|
||||
}
|
||||
|
||||
@override
|
||||
State<DateCell> createState() => _DateCellState();
|
||||
GridCellState<DateCell> createState() => _DateCellState();
|
||||
}
|
||||
|
||||
class _DateCellState extends State<DateCell> {
|
||||
class _DateCellState extends GridCellState<DateCell> {
|
||||
late DateCellBloc _cellBloc;
|
||||
|
||||
@override
|
||||
@ -64,7 +64,7 @@ class _DateCellState extends State<DateCell> {
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: Align(
|
||||
alignment: alignment,
|
||||
child: FlowyText.medium(state.data.foldRight("", (data, _) => data.date), fontSize: 12),
|
||||
child: FlowyText.medium(state.dateStr, fontSize: 12),
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -76,8 +76,8 @@ class _DateCellState extends State<DateCell> {
|
||||
|
||||
void _showCalendar(BuildContext context) {
|
||||
final bloc = context.read<DateCellBloc>();
|
||||
widget.onFocus.value = true;
|
||||
final calendar = CellCalendar(onDismissed: () => widget.onFocus.value = false);
|
||||
widget.onCellEditing.value = true;
|
||||
final calendar = DateCellEditor(onDismissed: () => widget.onCellEditing.value = false);
|
||||
calendar.show(
|
||||
context,
|
||||
cellContext: bloc.cellContext.clone(),
|
||||
@ -89,4 +89,10 @@ class _DateCellState extends State<DateCell> {
|
||||
_cellBloc.close();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void requestBeginFocus() {}
|
||||
|
||||
@override
|
||||
String? onCopy() => _cellBloc.state.dateStr;
|
||||
}
|
||||
|
@ -22,10 +22,10 @@ final kFirstDay = DateTime(kToday.year, kToday.month - 3, kToday.day);
|
||||
final kLastDay = DateTime(kToday.year, kToday.month + 3, kToday.day);
|
||||
const kMargin = EdgeInsets.symmetric(horizontal: 6, vertical: 10);
|
||||
|
||||
class CellCalendar with FlowyOverlayDelegate {
|
||||
class DateCellEditor with FlowyOverlayDelegate {
|
||||
final VoidCallback onDismissed;
|
||||
|
||||
const CellCalendar({
|
||||
const DateCellEditor({
|
||||
required this.onDismissed,
|
||||
});
|
||||
|
||||
@ -33,23 +33,14 @@ class CellCalendar with FlowyOverlayDelegate {
|
||||
BuildContext context, {
|
||||
required GridDateCellContext cellContext,
|
||||
}) async {
|
||||
CellCalendar.remove(context);
|
||||
DateCellEditor.remove(context);
|
||||
|
||||
final result = await cellContext.getTypeOptionData();
|
||||
result.fold(
|
||||
(data) {
|
||||
final typeOptionData = DateTypeOption.fromBuffer(data);
|
||||
// DateTime? selectedDay;
|
||||
// final cellData = cellContext.getCellData();
|
||||
|
||||
// if (cellData != null) {
|
||||
// final timestamp = $fixnum.Int64.parseInt(cellData).toInt();
|
||||
// selectedDay = DateTime.fromMillisecondsSinceEpoch(timestamp * 1000);
|
||||
// }
|
||||
|
||||
final calendar = _CellCalendarWidget(
|
||||
cellContext: cellContext,
|
||||
dateTypeOption: typeOptionData,
|
||||
dateTypeOption: DateTypeOption.fromBuffer(data.typeOptionData),
|
||||
);
|
||||
|
||||
FlowyOverlay.of(context).insertWithAnchor(
|
||||
@ -57,7 +48,7 @@ class CellCalendar with FlowyOverlayDelegate {
|
||||
child: calendar,
|
||||
constraints: BoxConstraints.loose(const Size(320, 500)),
|
||||
),
|
||||
identifier: CellCalendar.identifier(),
|
||||
identifier: DateCellEditor.identifier(),
|
||||
anchorContext: context,
|
||||
anchorDirection: AnchorDirection.leftWithCenterAligned,
|
||||
style: FlowyOverlayStyle(blur: false),
|
||||
@ -73,7 +64,7 @@ class CellCalendar with FlowyOverlayDelegate {
|
||||
}
|
||||
|
||||
static String identifier() {
|
||||
return (CellCalendar).toString();
|
||||
return (DateCellEditor).toString();
|
||||
}
|
||||
|
||||
@override
|
||||
@ -169,18 +160,21 @@ class _CellCalendarWidget extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
selectedDayPredicate: (day) {
|
||||
return state.dateData.fold(
|
||||
return state.calData.fold(
|
||||
() => false,
|
||||
(dateData) => isSameDay(dateData.date, day),
|
||||
);
|
||||
},
|
||||
onDaySelected: (selectedDay, focusedDay) {
|
||||
_CalDateTimeSetting.hide(context);
|
||||
context.read<DateCalBloc>().add(DateCalEvent.selectDay(selectedDay));
|
||||
},
|
||||
onFormatChanged: (format) {
|
||||
_CalDateTimeSetting.hide(context);
|
||||
context.read<DateCalBloc>().add(DateCalEvent.setCalFormat(format));
|
||||
},
|
||||
onPageChanged: (focusedDay) {
|
||||
_CalDateTimeSetting.hide(context);
|
||||
context.read<DateCalBloc>().add(DateCalEvent.setFocusedDay(focusedDay));
|
||||
},
|
||||
);
|
||||
@ -243,6 +237,7 @@ class _TimeTextFieldState extends State<_TimeTextField> {
|
||||
if (widget.bloc.state.dateTypeOption.includeTime) {
|
||||
_focusNode.addListener(() {
|
||||
if (mounted) {
|
||||
_CalDateTimeSetting.hide(context);
|
||||
widget.bloc.add(DateCalEvent.setTime(_controller.text));
|
||||
}
|
||||
});
|
||||
@ -266,6 +261,7 @@ class _TimeTextFieldState extends State<_TimeTextField> {
|
||||
child: RoundedInputField(
|
||||
height: 40,
|
||||
focusNode: _focusNode,
|
||||
hintText: state.timeHintText,
|
||||
controller: _controller,
|
||||
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
|
||||
normalBorderColor: theme.shader4,
|
||||
@ -335,6 +331,7 @@ class _CalDateTimeSetting extends StatefulWidget {
|
||||
}
|
||||
|
||||
void show(BuildContext context) {
|
||||
hide(context);
|
||||
FlowyOverlay.of(context).insertWithAnchor(
|
||||
widget: OverlayContainer(
|
||||
child: this,
|
||||
@ -346,6 +343,10 @@ class _CalDateTimeSetting extends StatefulWidget {
|
||||
anchorOffset: const Offset(20, 0),
|
||||
);
|
||||
}
|
||||
|
||||
static void hide(BuildContext context) {
|
||||
FlowyOverlay.of(context).remove(identifier());
|
||||
}
|
||||
}
|
||||
|
||||
class _CalDateTimeSettingState extends State<_CalDateTimeSetting> {
|
@ -1,5 +1,4 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:app_flowy/startup/startup.dart';
|
||||
import 'package:app_flowy/workspace/application/grid/prelude.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@ -16,101 +15,79 @@ class NumberCell extends GridCellWidget {
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<NumberCell> createState() => _NumberCellState();
|
||||
GridFocusNodeCellState<NumberCell> createState() => _NumberCellState();
|
||||
}
|
||||
|
||||
class _NumberCellState extends State<NumberCell> {
|
||||
class _NumberCellState extends GridFocusNodeCellState<NumberCell> {
|
||||
late NumberCellBloc _cellBloc;
|
||||
late TextEditingController _controller;
|
||||
late CellSingleFocusNode _focusNode;
|
||||
Timer? _delayOperation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
final cellContext = widget.cellContextBuilder.build();
|
||||
_cellBloc = getIt<NumberCellBloc>(param1: cellContext)..add(const NumberCellEvent.initial());
|
||||
_controller = TextEditingController(text: _cellBloc.state.content);
|
||||
_focusNode = CellSingleFocusNode();
|
||||
_listenFocusNode();
|
||||
_controller = TextEditingController(text: contentFromState(_cellBloc.state));
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
_listenCellRequestFocus(context);
|
||||
return BlocProvider.value(
|
||||
value: _cellBloc,
|
||||
child: BlocConsumer<NumberCellBloc, NumberCellState>(
|
||||
listener: (context, state) {
|
||||
if (_controller.text != state.content) {
|
||||
_controller.text = state.content;
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
return TextField(
|
||||
controller: _controller,
|
||||
focusNode: _focusNode,
|
||||
onEditingComplete: () => _focusNode.unfocus(),
|
||||
maxLines: null,
|
||||
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
|
||||
decoration: const InputDecoration(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
border: InputBorder.none,
|
||||
isDense: true,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: MultiBlocListener(
|
||||
listeners: [
|
||||
BlocListener<NumberCellBloc, NumberCellState>(
|
||||
listenWhen: (p, c) => p.content != c.content,
|
||||
listener: (context, state) => _controller.text = contentFromState(state),
|
||||
),
|
||||
],
|
||||
child: TextField(
|
||||
controller: _controller,
|
||||
focusNode: focusNode,
|
||||
onEditingComplete: () => focusNode.unfocus(),
|
||||
maxLines: null,
|
||||
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
|
||||
decoration: const InputDecoration(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
border: InputBorder.none,
|
||||
isDense: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> dispose() async {
|
||||
widget.requestFocus.removeAllListener();
|
||||
_delayOperation?.cancel();
|
||||
_cellBloc.close();
|
||||
_focusNode.removeSingleListener();
|
||||
_focusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant NumberCell oldWidget) {
|
||||
if (oldWidget != widget) {
|
||||
_listenFocusNode();
|
||||
}
|
||||
super.didUpdateWidget(oldWidget);
|
||||
}
|
||||
|
||||
Future<void> focusChanged() async {
|
||||
if (mounted) {
|
||||
_delayOperation?.cancel();
|
||||
_delayOperation = Timer(const Duration(milliseconds: 300), () {
|
||||
if (_cellBloc.isClosed == false && _controller.text != _cellBloc.state.content) {
|
||||
final number = num.tryParse(_controller.text);
|
||||
if (number != null) {
|
||||
_cellBloc.add(NumberCellEvent.updateCell(_controller.text));
|
||||
} else {
|
||||
_controller.text = "";
|
||||
}
|
||||
if (_cellBloc.isClosed == false && _controller.text != contentFromState(_cellBloc.state)) {
|
||||
_cellBloc.add(NumberCellEvent.updateCell(_controller.text));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _listenFocusNode() {
|
||||
widget.onFocus.value = _focusNode.hasFocus;
|
||||
_focusNode.setSingleListener(() {
|
||||
widget.onFocus.value = _focusNode.hasFocus;
|
||||
focusChanged();
|
||||
});
|
||||
String contentFromState(NumberCellState state) {
|
||||
return state.content.fold((l) => l, (r) => "");
|
||||
}
|
||||
|
||||
void _listenCellRequestFocus(BuildContext context) {
|
||||
widget.requestFocus.addListener(() {
|
||||
if (_focusNode.hasFocus == false && _focusNode.canRequestFocus) {
|
||||
FocusScope.of(context).requestFocus(_focusNode);
|
||||
}
|
||||
});
|
||||
@override
|
||||
String? onCopy() {
|
||||
return _cellBloc.state.content.fold((content) => content, (r) => null);
|
||||
}
|
||||
|
||||
@override
|
||||
void onInsert(String value) {
|
||||
_cellBloc.add(NumberCellEvent.updateCell(value));
|
||||
}
|
||||
}
|
||||
|
@ -3,4 +3,4 @@ export 'text_cell.dart';
|
||||
export 'number_cell.dart';
|
||||
export 'date_cell/date_cell.dart';
|
||||
export 'checkbox_cell.dart';
|
||||
export 'selection_cell/selection_cell.dart';
|
||||
export 'select_option_cell/select_option_cell.dart';
|
||||
|
@ -1,4 +1,5 @@
|
||||
import 'package:flowy_infra/theme.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/selection_type_option.pb.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@ -63,9 +64,11 @@ class SelectOptionTag extends StatelessWidget {
|
||||
final String name;
|
||||
final Color color;
|
||||
final bool isSelected;
|
||||
final VoidCallback? onSelected;
|
||||
const SelectOptionTag({
|
||||
required this.name,
|
||||
required this.color,
|
||||
this.onSelected,
|
||||
this.isSelected = false,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
@ -73,12 +76,14 @@ class SelectOptionTag extends StatelessWidget {
|
||||
factory SelectOptionTag.fromSelectOption({
|
||||
required BuildContext context,
|
||||
required SelectOption option,
|
||||
VoidCallback? onSelected,
|
||||
bool isSelected = false,
|
||||
}) {
|
||||
return SelectOptionTag(
|
||||
name: option.name,
|
||||
color: option.color.make(context),
|
||||
isSelected: isSelected,
|
||||
onSelected: onSelected,
|
||||
);
|
||||
}
|
||||
|
||||
@ -86,23 +91,63 @@ class SelectOptionTag extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return ChoiceChip(
|
||||
pressElevation: 1,
|
||||
label: FlowyText.medium(name, fontSize: 12),
|
||||
label: FlowyText.medium(name, fontSize: 12, overflow: TextOverflow.ellipsis),
|
||||
selectedColor: color,
|
||||
backgroundColor: color,
|
||||
labelPadding: const EdgeInsets.symmetric(horizontal: 6),
|
||||
selected: true,
|
||||
onSelected: (_) {},
|
||||
onSelected: (_) {
|
||||
if (onSelected != null) {
|
||||
onSelected!();
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SelectOptionTagCell extends StatelessWidget {
|
||||
final List<Widget> children;
|
||||
final void Function(SelectOption) onSelected;
|
||||
final SelectOption option;
|
||||
const SelectOptionTagCell({
|
||||
required this.option,
|
||||
required this.onSelected,
|
||||
this.children = const [],
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = context.watch<AppTheme>();
|
||||
return Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
FlowyHover(
|
||||
style: HoverStyle(hoverColor: theme.hover),
|
||||
child: InkWell(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 3),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Flexible(
|
||||
fit: FlexFit.loose,
|
||||
flex: 2,
|
||||
child: SelectOptionTag.fromSelectOption(
|
||||
context: context,
|
||||
option: option,
|
||||
onSelected: () => onSelected(option),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
...children,
|
||||
],
|
||||
),
|
||||
),
|
||||
onTap: () => onSelected(option),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
// return Container(
|
||||
// decoration: BoxDecoration(
|
||||
// color: option.color.make(context),
|
||||
// shape: BoxShape.rectangle,
|
||||
// borderRadius: BorderRadius.circular(8.0),
|
||||
// ),
|
||||
// child: Center(child: FlowyText.medium(option.name, fontSize: 12)),
|
||||
// margin: const EdgeInsets.symmetric(horizontal: 3.0),
|
||||
// padding: const EdgeInsets.symmetric(horizontal: 6.0),
|
||||
// );
|
||||
}
|
||||
}
|
@ -10,7 +10,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import 'extension.dart';
|
||||
import 'selection_editor.dart';
|
||||
import 'select_option_editor.dart';
|
||||
|
||||
class SelectOptionCellStyle extends GridCellStyle {
|
||||
String placeholder;
|
||||
@ -41,12 +41,12 @@ class SingleSelectCell extends GridCellWidget {
|
||||
}
|
||||
|
||||
class _SingleSelectCellState extends State<SingleSelectCell> {
|
||||
late SelectionCellBloc _cellBloc;
|
||||
late SelectOptionCellBloc _cellBloc;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
final cellContext = widget.cellContextBuilder.build() as GridSelectOptionCellContext;
|
||||
_cellBloc = getIt<SelectionCellBloc>(param1: cellContext)..add(const SelectionCellEvent.initial());
|
||||
_cellBloc = getIt<SelectOptionCellBloc>(param1: cellContext)..add(const SelectOptionCellEvent.initial());
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@ -54,12 +54,12 @@ class _SingleSelectCellState extends State<SingleSelectCell> {
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider.value(
|
||||
value: _cellBloc,
|
||||
child: BlocBuilder<SelectionCellBloc, SelectionCellState>(
|
||||
child: BlocBuilder<SelectOptionCellBloc, SelectOptionCellState>(
|
||||
builder: (context, state) {
|
||||
return _SelectOptionCell(
|
||||
selectOptions: state.selectedOptions,
|
||||
cellStyle: widget.cellStyle,
|
||||
onFocus: (value) => widget.onFocus.value = value,
|
||||
onFocus: (value) => widget.onCellEditing.value = value,
|
||||
cellContextBuilder: widget.cellContextBuilder);
|
||||
},
|
||||
),
|
||||
@ -95,12 +95,12 @@ class MultiSelectCell extends GridCellWidget {
|
||||
}
|
||||
|
||||
class _MultiSelectCellState extends State<MultiSelectCell> {
|
||||
late SelectionCellBloc _cellBloc;
|
||||
late SelectOptionCellBloc _cellBloc;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
final cellContext = widget.cellContextBuilder.build() as GridSelectOptionCellContext;
|
||||
_cellBloc = getIt<SelectionCellBloc>(param1: cellContext)..add(const SelectionCellEvent.initial());
|
||||
_cellBloc = getIt<SelectOptionCellBloc>(param1: cellContext)..add(const SelectOptionCellEvent.initial());
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@ -108,12 +108,12 @@ class _MultiSelectCellState extends State<MultiSelectCell> {
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider.value(
|
||||
value: _cellBloc,
|
||||
child: BlocBuilder<SelectionCellBloc, SelectionCellState>(
|
||||
child: BlocBuilder<SelectOptionCellBloc, SelectOptionCellState>(
|
||||
builder: (context, state) {
|
||||
return _SelectOptionCell(
|
||||
selectOptions: state.selectedOptions,
|
||||
cellStyle: widget.cellStyle,
|
||||
onFocus: (value) => widget.onFocus.value = value,
|
||||
onFocus: (value) => widget.onCellEditing.value = value,
|
||||
cellContextBuilder: widget.cellContextBuilder);
|
||||
},
|
||||
),
|
||||
@ -160,7 +160,7 @@ class _SelectOptionCell extends StatelessWidget {
|
||||
.toList();
|
||||
child = Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Wrap(children: tags, spacing: 4, runSpacing: 4),
|
||||
child: Wrap(children: tags, spacing: 4, runSpacing: 2),
|
||||
);
|
||||
}
|
||||
|
@ -1,13 +1,12 @@
|
||||
import 'dart:collection';
|
||||
import 'package:app_flowy/workspace/application/grid/cell/cell_service/cell_service.dart';
|
||||
import 'package:app_flowy/workspace/application/grid/cell/selection_editor_bloc.dart';
|
||||
import 'package:app_flowy/workspace/application/grid/cell/select_option_editor_bloc.dart';
|
||||
import 'package:app_flowy/workspace/presentation/plugins/grid/src/layout/sizes.dart';
|
||||
import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/header/type_option/edit_option_pannel.dart';
|
||||
import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/header/type_option/select_option_editor.dart';
|
||||
import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/common/text_field.dart';
|
||||
import 'package:flowy_infra/image.dart';
|
||||
import 'package:flowy_infra/theme.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/scrolling/styled_list.dart';
|
||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||
@ -37,10 +36,10 @@ class SelectOptionCellEditor extends StatelessWidget with FlowyOverlayDelegate {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) => SelectOptionEditorBloc(
|
||||
create: (context) => SelectOptionCellEditorBloc(
|
||||
cellContext: cellContext,
|
||||
)..add(const SelectOptionEditorEvent.initial()),
|
||||
child: BlocBuilder<SelectOptionEditorBloc, SelectOptionEditorState>(
|
||||
child: BlocBuilder<SelectOptionCellEditorBloc, SelectOptionEditorState>(
|
||||
builder: (context, state) {
|
||||
return CustomScrollView(
|
||||
shrinkWrap: true,
|
||||
@ -102,7 +101,7 @@ class _OptionList extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<SelectOptionEditorBloc, SelectOptionEditorState>(
|
||||
return BlocBuilder<SelectOptionCellEditorBloc, SelectOptionEditorState>(
|
||||
builder: (context, state) {
|
||||
List<Widget> cells = [];
|
||||
cells.addAll(state.options.map((option) {
|
||||
@ -145,7 +144,7 @@ class _TextField extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<SelectOptionEditorBloc, SelectOptionEditorState>(
|
||||
return BlocBuilder<SelectOptionCellEditorBloc, SelectOptionEditorState>(
|
||||
builder: (context, state) {
|
||||
final optionMap = LinkedHashMap<String, SelectOption>.fromIterable(state.selectedOptions,
|
||||
key: (option) => option.name, value: (option) => option);
|
||||
@ -157,11 +156,12 @@ class _TextField extends StatelessWidget {
|
||||
selectedOptionMap: optionMap,
|
||||
distanceToText: _editorPannelWidth * 0.7,
|
||||
tagController: _tagController,
|
||||
onClick: () => FlowyOverlay.of(context).remove(SelectOptionTypeOptionEditor.identifier),
|
||||
newText: (text) {
|
||||
context.read<SelectOptionEditorBloc>().add(SelectOptionEditorEvent.filterOption(text));
|
||||
context.read<SelectOptionCellEditorBloc>().add(SelectOptionEditorEvent.filterOption(text));
|
||||
},
|
||||
onNewTag: (tagName) {
|
||||
context.read<SelectOptionEditorBloc>().add(SelectOptionEditorEvent.newOption(tagName));
|
||||
context.read<SelectOptionCellEditorBloc>().add(SelectOptionEditorEvent.newOption(tagName));
|
||||
},
|
||||
),
|
||||
);
|
||||
@ -208,6 +208,7 @@ class _CreateOptionCell extends StatelessWidget {
|
||||
SelectOptionTag(
|
||||
name: name,
|
||||
color: theme.shader6,
|
||||
onSelected: () => context.read<SelectOptionCellEditorBloc>().add(SelectOptionEditorEvent.newOption(name)),
|
||||
),
|
||||
],
|
||||
);
|
||||
@ -224,63 +225,47 @@ class _SelectOptionCell extends StatelessWidget {
|
||||
final theme = context.watch<AppTheme>();
|
||||
return SizedBox(
|
||||
height: GridSize.typeOptionItemHeight,
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
child: Row(
|
||||
children: [
|
||||
_body(theme, context),
|
||||
InkWell(
|
||||
onTap: () {
|
||||
context.read<SelectOptionEditorBloc>().add(SelectOptionEditorEvent.selectOption(option.id));
|
||||
},
|
||||
Flexible(
|
||||
fit: FlexFit.loose,
|
||||
child: SelectOptionTagCell(
|
||||
option: option,
|
||||
onSelected: (option) {
|
||||
context.read<SelectOptionCellEditorBloc>().add(SelectOptionEditorEvent.selectOption(option.id));
|
||||
},
|
||||
children: [
|
||||
if (isSelected)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 6),
|
||||
child: svgWidget("grid/checkmark"),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
FlowyIconButton(
|
||||
width: 30,
|
||||
onPressed: () => _showEditPannel(context),
|
||||
iconPadding: const EdgeInsets.fromLTRB(4, 4, 4, 4),
|
||||
icon: svgWidget("editor/details", color: theme.iconColor),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
FlowyHover _body(AppTheme theme, BuildContext context) {
|
||||
return FlowyHover(
|
||||
style: HoverStyle(hoverColor: theme.hover),
|
||||
builder: (_, onHover) {
|
||||
List<Widget> children = [
|
||||
SelectOptionTag(
|
||||
name: option.name,
|
||||
color: option.color.make(context),
|
||||
isSelected: isSelected,
|
||||
),
|
||||
const Spacer(),
|
||||
];
|
||||
|
||||
if (isSelected) {
|
||||
children.add(svgWidget("grid/checkmark"));
|
||||
}
|
||||
|
||||
if (onHover) {
|
||||
children.add(FlowyIconButton(
|
||||
width: 30,
|
||||
onPressed: () => _showEditPannel(context),
|
||||
iconPadding: const EdgeInsets.fromLTRB(4, 4, 4, 4),
|
||||
icon: svgWidget("editor/details", color: theme.iconColor),
|
||||
));
|
||||
}
|
||||
|
||||
return Row(children: children);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _showEditPannel(BuildContext context) {
|
||||
final pannel = EditSelectOptionPannel(
|
||||
final pannel = SelectOptionTypeOptionEditor(
|
||||
option: option,
|
||||
onDeleted: () {
|
||||
context.read<SelectOptionEditorBloc>().add(SelectOptionEditorEvent.deleteOption(option));
|
||||
context.read<SelectOptionCellEditorBloc>().add(SelectOptionEditorEvent.deleteOption(option));
|
||||
},
|
||||
onUpdated: (updatedOption) {
|
||||
context.read<SelectOptionEditorBloc>().add(SelectOptionEditorEvent.updateOption(updatedOption));
|
||||
context.read<SelectOptionCellEditorBloc>().add(SelectOptionEditorEvent.updateOption(updatedOption));
|
||||
},
|
||||
key: ValueKey(option.id), // Use ValueKey to refresh the UI, otherwise, it will remain the old value.
|
||||
);
|
||||
final overlayIdentifier = (EditSelectOptionPannel).toString();
|
||||
final overlayIdentifier = (SelectOptionTypeOptionEditor).toString();
|
||||
|
||||
FlowyOverlay.of(context).remove(overlayIdentifier);
|
||||
FlowyOverlay.of(context).insertWithAnchor(
|
@ -22,6 +22,7 @@ class SelectOptionTextField extends StatelessWidget {
|
||||
|
||||
final Function(String) onNewTag;
|
||||
final Function(String) newText;
|
||||
final VoidCallback? onClick;
|
||||
|
||||
SelectOptionTextField({
|
||||
required this.options,
|
||||
@ -30,6 +31,7 @@ class SelectOptionTextField extends StatelessWidget {
|
||||
required this.tagController,
|
||||
required this.onNewTag,
|
||||
required this.newText,
|
||||
this.onClick,
|
||||
TextEditingController? controller,
|
||||
FocusNode? focusNode,
|
||||
Key? key,
|
||||
@ -53,6 +55,7 @@ class SelectOptionTextField extends StatelessWidget {
|
||||
autofocus: true,
|
||||
controller: editController,
|
||||
focusNode: focusNode,
|
||||
onTap: onClick,
|
||||
onChanged: (text) {
|
||||
if (onChanged != null) {
|
||||
onChanged(text);
|
@ -29,13 +29,12 @@ class GridTextCell extends GridCellWidget {
|
||||
}
|
||||
|
||||
@override
|
||||
State<GridTextCell> createState() => _GridTextCellState();
|
||||
GridFocusNodeCellState<GridTextCell> createState() => _GridTextCellState();
|
||||
}
|
||||
|
||||
class _GridTextCellState extends State<GridTextCell> {
|
||||
class _GridTextCellState extends GridFocusNodeCellState<GridTextCell> {
|
||||
late TextCellBloc _cellBloc;
|
||||
late TextEditingController _controller;
|
||||
late CellSingleFocusNode _focusNode;
|
||||
Timer? _delayOperation;
|
||||
|
||||
@override
|
||||
@ -44,10 +43,6 @@ class _GridTextCellState extends State<GridTextCell> {
|
||||
_cellBloc = getIt<TextCellBloc>(param1: cellContext);
|
||||
_cellBloc.add(const TextCellEvent.initial());
|
||||
_controller = TextEditingController(text: _cellBloc.state.content);
|
||||
_focusNode = CellSingleFocusNode();
|
||||
|
||||
_listenFocusNode();
|
||||
_listenRequestFocus(context);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@ -63,9 +58,9 @@ class _GridTextCellState extends State<GridTextCell> {
|
||||
},
|
||||
child: TextField(
|
||||
controller: _controller,
|
||||
focusNode: _focusNode,
|
||||
focusNode: focusNode,
|
||||
onChanged: (value) => focusChanged(),
|
||||
onEditingComplete: () => _focusNode.unfocus(),
|
||||
onEditingComplete: () => focusNode.unfocus(),
|
||||
maxLines: null,
|
||||
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
|
||||
decoration: InputDecoration(
|
||||
@ -81,39 +76,12 @@ class _GridTextCellState extends State<GridTextCell> {
|
||||
|
||||
@override
|
||||
Future<void> dispose() async {
|
||||
widget.requestFocus.removeAllListener();
|
||||
_delayOperation?.cancel();
|
||||
_cellBloc.close();
|
||||
_focusNode.removeSingleListener();
|
||||
_focusNode.dispose();
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant GridTextCell oldWidget) {
|
||||
if (oldWidget != widget) {
|
||||
_listenFocusNode();
|
||||
}
|
||||
super.didUpdateWidget(oldWidget);
|
||||
}
|
||||
|
||||
void _listenFocusNode() {
|
||||
widget.onFocus.value = _focusNode.hasFocus;
|
||||
_focusNode.setSingleListener(() {
|
||||
widget.onFocus.value = _focusNode.hasFocus;
|
||||
focusChanged();
|
||||
});
|
||||
}
|
||||
|
||||
void _listenRequestFocus(BuildContext context) {
|
||||
widget.requestFocus.addListener(() {
|
||||
if (_focusNode.hasFocus == false && _focusNode.canRequestFocus) {
|
||||
FocusScope.of(context).requestFocus(_focusNode);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> focusChanged() async {
|
||||
if (mounted) {
|
||||
_delayOperation?.cancel();
|
||||
@ -124,4 +92,12 @@ class _GridTextCellState extends State<GridTextCell> {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
String? onCopy() => _cellBloc.state.content;
|
||||
|
||||
@override
|
||||
void onInsert(String value) {
|
||||
_cellBloc.add(TextCellEvent.updateText(value));
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,113 @@
|
||||
import 'package:app_flowy/workspace/application/grid/cell/cell_service/cell_service.dart';
|
||||
import 'package:app_flowy/workspace/application/grid/cell/url_cell_editor_bloc.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class URLCellEditor extends StatefulWidget with FlowyOverlayDelegate {
|
||||
final GridURLCellContext cellContext;
|
||||
final VoidCallback completed;
|
||||
const URLCellEditor({required this.cellContext, required this.completed, Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<URLCellEditor> createState() => _URLCellEditorState();
|
||||
|
||||
static void show(
|
||||
BuildContext context,
|
||||
GridURLCellContext cellContext,
|
||||
VoidCallback completed,
|
||||
) {
|
||||
FlowyOverlay.of(context).remove(identifier());
|
||||
final editor = URLCellEditor(
|
||||
cellContext: cellContext,
|
||||
completed: completed,
|
||||
);
|
||||
|
||||
//
|
||||
FlowyOverlay.of(context).insertWithAnchor(
|
||||
widget: OverlayContainer(
|
||||
child: SizedBox(
|
||||
width: 200,
|
||||
child: Padding(padding: const EdgeInsets.all(6), child: editor),
|
||||
),
|
||||
constraints: BoxConstraints.loose(const Size(300, 160)),
|
||||
),
|
||||
identifier: URLCellEditor.identifier(),
|
||||
anchorContext: context,
|
||||
anchorDirection: AnchorDirection.bottomWithCenterAligned,
|
||||
delegate: editor,
|
||||
);
|
||||
}
|
||||
|
||||
static String identifier() {
|
||||
return (URLCellEditor).toString();
|
||||
}
|
||||
|
||||
@override
|
||||
bool asBarrier() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
void didRemove() {
|
||||
completed();
|
||||
}
|
||||
}
|
||||
|
||||
class _URLCellEditorState extends State<URLCellEditor> {
|
||||
late URLCellEditorBloc _cellBloc;
|
||||
late TextEditingController _controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_cellBloc = URLCellEditorBloc(cellContext: widget.cellContext);
|
||||
_cellBloc.add(const URLCellEditorEvent.initial());
|
||||
_controller = TextEditingController(text: _cellBloc.state.content);
|
||||
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider.value(
|
||||
value: _cellBloc,
|
||||
child: BlocListener<URLCellEditorBloc, URLCellEditorState>(
|
||||
listener: (context, state) {
|
||||
if (_controller.text != state.content) {
|
||||
_controller.text = state.content;
|
||||
}
|
||||
},
|
||||
child: TextField(
|
||||
autofocus: true,
|
||||
controller: _controller,
|
||||
onChanged: (value) => focusChanged(),
|
||||
maxLines: null,
|
||||
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
|
||||
decoration: const InputDecoration(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
border: InputBorder.none,
|
||||
hintText: "",
|
||||
isDense: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> dispose() async {
|
||||
_cellBloc.close();
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> focusChanged() async {
|
||||
if (mounted) {
|
||||
if (_cellBloc.isClosed == false && _controller.text != _cellBloc.state.content) {
|
||||
_cellBloc.add(URLCellEditorEvent.updateText(_controller.text));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,194 @@
|
||||
import 'dart:async';
|
||||
import 'package:app_flowy/generated/locale_keys.g.dart';
|
||||
import 'package:app_flowy/workspace/application/grid/cell/url_cell_bloc.dart';
|
||||
import 'package:app_flowy/workspace/presentation/home/toast.dart';
|
||||
import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/cell/cell_accessory.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/image.dart';
|
||||
import 'package:flowy_infra/theme.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:app_flowy/workspace/application/grid/prelude.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import '../cell_builder.dart';
|
||||
import 'cell_editor.dart';
|
||||
|
||||
class GridURLCellStyle extends GridCellStyle {
|
||||
String? placeholder;
|
||||
|
||||
List<GridURLCellAccessoryType> accessoryTypes;
|
||||
|
||||
GridURLCellStyle({
|
||||
this.placeholder,
|
||||
this.accessoryTypes = const [],
|
||||
});
|
||||
}
|
||||
|
||||
enum GridURLCellAccessoryType {
|
||||
edit,
|
||||
copyURL,
|
||||
}
|
||||
|
||||
class GridURLCell extends GridCellWidget {
|
||||
final GridCellContextBuilder cellContextBuilder;
|
||||
late final GridURLCellStyle? cellStyle;
|
||||
GridURLCell({
|
||||
required this.cellContextBuilder,
|
||||
GridCellStyle? style,
|
||||
Key? key,
|
||||
}) : super(key: key) {
|
||||
if (style != null) {
|
||||
cellStyle = (style as GridURLCellStyle);
|
||||
} else {
|
||||
cellStyle = null;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
GridCellState<GridURLCell> createState() => _GridURLCellState();
|
||||
|
||||
GridCellAccessory accessoryFromType(GridURLCellAccessoryType ty, GridCellAccessoryBuildContext buildContext) {
|
||||
switch (ty) {
|
||||
case GridURLCellAccessoryType.edit:
|
||||
final cellContext = cellContextBuilder.build() as GridURLCellContext;
|
||||
return _EditURLAccessory(cellContext: cellContext, anchorContext: buildContext.anchorContext);
|
||||
|
||||
case GridURLCellAccessoryType.copyURL:
|
||||
final cellContext = cellContextBuilder.build() as GridURLCellContext;
|
||||
return _CopyURLAccessory(cellContext: cellContext);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
List<GridCellAccessory> Function(GridCellAccessoryBuildContext buildContext) get accessoryBuilder => (buildContext) {
|
||||
final List<GridCellAccessory> accessories = [];
|
||||
if (cellStyle != null) {
|
||||
accessories.addAll(cellStyle!.accessoryTypes.map((ty) {
|
||||
return accessoryFromType(ty, buildContext);
|
||||
}));
|
||||
}
|
||||
|
||||
// If the accessories is empty then the default accessory will be GridURLCellAccessoryType.edit
|
||||
if (accessories.isEmpty) {
|
||||
accessories.add(accessoryFromType(GridURLCellAccessoryType.edit, buildContext));
|
||||
}
|
||||
|
||||
return accessories;
|
||||
};
|
||||
}
|
||||
|
||||
class _GridURLCellState extends GridCellState<GridURLCell> {
|
||||
late URLCellBloc _cellBloc;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
final cellContext = widget.cellContextBuilder.build() as GridURLCellContext;
|
||||
_cellBloc = URLCellBloc(cellContext: cellContext);
|
||||
_cellBloc.add(const URLCellEvent.initial());
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = context.watch<AppTheme>();
|
||||
return BlocProvider.value(
|
||||
value: _cellBloc,
|
||||
child: BlocBuilder<URLCellBloc, URLCellState>(
|
||||
builder: (context, state) {
|
||||
final richText = RichText(
|
||||
textAlign: TextAlign.left,
|
||||
text: TextSpan(
|
||||
text: state.content,
|
||||
style: TextStyle(
|
||||
color: theme.main2,
|
||||
fontSize: 14,
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
return SizedBox.expand(
|
||||
child: GestureDetector(
|
||||
child: Align(alignment: Alignment.centerLeft, child: richText),
|
||||
onTap: () async {
|
||||
final url = context.read<URLCellBloc>().state.url;
|
||||
await _openUrlOrEdit(url);
|
||||
},
|
||||
));
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> dispose() async {
|
||||
_cellBloc.close();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _openUrlOrEdit(String url) async {
|
||||
final uri = Uri.parse(url);
|
||||
if (url.isNotEmpty && await canLaunchUrl(uri)) {
|
||||
await launchUrl(uri);
|
||||
} else {
|
||||
final cellContext = widget.cellContextBuilder.build() as GridURLCellContext;
|
||||
widget.onCellEditing.value = true;
|
||||
URLCellEditor.show(context, cellContext, () {
|
||||
widget.onCellEditing.value = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void requestBeginFocus() {
|
||||
_openUrlOrEdit(_cellBloc.state.url);
|
||||
}
|
||||
|
||||
@override
|
||||
String? onCopy() => _cellBloc.state.content;
|
||||
|
||||
@override
|
||||
void onInsert(String value) {
|
||||
_cellBloc.add(URLCellEvent.updateURL(value));
|
||||
}
|
||||
}
|
||||
|
||||
class _EditURLAccessory extends StatelessWidget with GridCellAccessory {
|
||||
final GridURLCellContext cellContext;
|
||||
final BuildContext anchorContext;
|
||||
const _EditURLAccessory({
|
||||
required this.cellContext,
|
||||
required this.anchorContext,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = context.watch<AppTheme>();
|
||||
return svgWidget("editor/edit", color: theme.iconColor);
|
||||
}
|
||||
|
||||
@override
|
||||
void onTap() {
|
||||
URLCellEditor.show(anchorContext, cellContext, () {});
|
||||
}
|
||||
}
|
||||
|
||||
class _CopyURLAccessory extends StatelessWidget with GridCellAccessory {
|
||||
final GridURLCellContext cellContext;
|
||||
const _CopyURLAccessory({required this.cellContext, Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = context.watch<AppTheme>();
|
||||
return svgWidget("editor/copy", color: theme.iconColor);
|
||||
}
|
||||
|
||||
@override
|
||||
void onTap() {
|
||||
final content = cellContext.getCellData(loadIfNoCache: false)?.content ?? "";
|
||||
Clipboard.setData(ClipboardData(text: content));
|
||||
showMessageToast(LocaleKeys.grid_row_copyProperty.tr());
|
||||
}
|
||||
}
|
@ -7,6 +7,7 @@ class InputTextField extends StatefulWidget {
|
||||
final void Function(String)? onDone;
|
||||
final void Function(String)? onChanged;
|
||||
final void Function() onCanceled;
|
||||
final bool autoClearWhenDone;
|
||||
final String text;
|
||||
|
||||
const InputTextField({
|
||||
@ -14,6 +15,7 @@ class InputTextField extends StatefulWidget {
|
||||
this.onDone,
|
||||
required this.onCanceled,
|
||||
this.onChanged,
|
||||
this.autoClearWhenDone = false,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@ -57,6 +59,10 @@ class _InputTextFieldState extends State<InputTextField> {
|
||||
if (widget.onDone != null) {
|
||||
widget.onDone!(_controller.text);
|
||||
}
|
||||
|
||||
if (widget.autoClearWhenDone) {
|
||||
_controller.text = "";
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
@ -6,7 +6,6 @@ import 'package:flowy_infra/theme.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/button.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flowy_sdk/log.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid-data-model/grid.pb.dart' show Field;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
@ -24,6 +23,7 @@ class GridFieldCell extends StatelessWidget {
|
||||
return BlocProvider(
|
||||
create: (context) => FieldCellBloc(cellContext: cellContext)..add(const FieldCellEvent.initial()),
|
||||
child: BlocBuilder<FieldCellBloc, FieldCellState>(
|
||||
// buildWhen: (p, c) => p.field != c.field,
|
||||
builder: (context, state) {
|
||||
final button = FieldCellButton(
|
||||
field: state.field,
|
||||
@ -37,8 +37,8 @@ class GridFieldCell extends StatelessWidget {
|
||||
child: _DragToExpandLine(),
|
||||
);
|
||||
|
||||
return _CellContainer(
|
||||
width: state.field.width.toDouble(),
|
||||
return _GridHeaderCellContainer(
|
||||
width: state.width,
|
||||
child: Stack(
|
||||
alignment: Alignment.centerRight,
|
||||
fit: StackFit.expand,
|
||||
@ -60,21 +60,23 @@ class GridFieldCell extends StatelessWidget {
|
||||
|
||||
void _showFieldEditor(BuildContext context) {
|
||||
final state = context.read<FieldCellBloc>().state;
|
||||
final field = state.field;
|
||||
|
||||
FieldEditor(
|
||||
gridId: state.gridId,
|
||||
fieldContextLoader: FieldContextLoaderAdaptor(
|
||||
fieldName: field.name,
|
||||
contextLoader: FieldContextLoader(
|
||||
gridId: state.gridId,
|
||||
field: state.field,
|
||||
field: field,
|
||||
),
|
||||
).show(context);
|
||||
}
|
||||
}
|
||||
|
||||
class _CellContainer extends StatelessWidget {
|
||||
class _GridHeaderCellContainer extends StatelessWidget {
|
||||
final Widget child;
|
||||
final double width;
|
||||
const _CellContainer({
|
||||
const _GridHeaderCellContainer({
|
||||
required this.child,
|
||||
required this.width,
|
||||
Key? key,
|
||||
@ -83,7 +85,7 @@ class _CellContainer extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = context.watch<AppTheme>();
|
||||
final borderSide = BorderSide(color: theme.shader4, width: 0.4);
|
||||
final borderSide = BorderSide(color: theme.shader5, width: 1.0);
|
||||
final decoration = BoxDecoration(
|
||||
border: Border(
|
||||
top: borderSide,
|
||||
@ -112,21 +114,19 @@ class _DragToExpandLine extends StatelessWidget {
|
||||
onTap: () {},
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onHorizontalDragCancel: () {},
|
||||
onHorizontalDragUpdate: (value) {
|
||||
// context.read<FieldCellBloc>().add(FieldCellEvent.updateWidth(value.delta.dx));
|
||||
Log.info(value);
|
||||
context.read<FieldCellBloc>().add(FieldCellEvent.startUpdateWidth(value.delta.dx));
|
||||
},
|
||||
onHorizontalDragEnd: (end) {
|
||||
Log.info(end);
|
||||
context.read<FieldCellBloc>().add(const FieldCellEvent.endUpdateWidth());
|
||||
},
|
||||
child: FlowyHover(
|
||||
style: HoverStyle(
|
||||
hoverColor: theme.main1,
|
||||
borderRadius: BorderRadius.zero,
|
||||
contentMargin: const EdgeInsets.only(left: 5),
|
||||
contentMargin: const EdgeInsets.only(left: 6),
|
||||
),
|
||||
child: const SizedBox(width: 2),
|
||||
child: const SizedBox(width: 4),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
@ -1,4 +1,3 @@
|
||||
import 'package:app_flowy/startup/startup.dart';
|
||||
import 'package:app_flowy/workspace/application/grid/field/field_editor_bloc.dart';
|
||||
import 'package:app_flowy/workspace/application/grid/field/field_service.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
@ -11,16 +10,42 @@ import 'package:app_flowy/generated/locale_keys.g.dart';
|
||||
import 'field_name_input.dart';
|
||||
import 'field_editor_pannel.dart';
|
||||
|
||||
class FieldEditor extends FlowyOverlayDelegate {
|
||||
class FieldEditor extends StatelessWidget with FlowyOverlayDelegate {
|
||||
final String gridId;
|
||||
final FieldEditorBloc _fieldEditorBloc;
|
||||
final EditFieldContextLoader fieldContextLoader;
|
||||
FieldEditor({
|
||||
final String fieldName;
|
||||
|
||||
final IFieldContextLoader contextLoader;
|
||||
const FieldEditor({
|
||||
required this.gridId,
|
||||
required this.fieldContextLoader,
|
||||
required this.fieldName,
|
||||
required this.contextLoader,
|
||||
Key? key,
|
||||
}) : _fieldEditorBloc = getIt<FieldEditorBloc>(param1: gridId, param2: fieldContextLoader) {
|
||||
_fieldEditorBloc.add(const FieldEditorEvent.initial());
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) => FieldEditorBloc(
|
||||
gridId: gridId,
|
||||
fieldName: fieldName,
|
||||
fieldContextLoader: contextLoader,
|
||||
)..add(const FieldEditorEvent.initial()),
|
||||
child: BlocBuilder<FieldEditorBloc, FieldEditorState>(
|
||||
buildWhen: (p, c) => false,
|
||||
builder: (context, state) {
|
||||
return ListView(
|
||||
shrinkWrap: true,
|
||||
children: [
|
||||
FlowyText.medium(LocaleKeys.grid_field_editProperty.tr(), fontSize: 12),
|
||||
const VSpace(10),
|
||||
const _FieldNameTextField(),
|
||||
const VSpace(10),
|
||||
const _FieldPannel(),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void show(
|
||||
@ -30,7 +55,7 @@ class FieldEditor extends FlowyOverlayDelegate {
|
||||
FlowyOverlay.of(context).remove(identifier());
|
||||
FlowyOverlay.of(context).insertWithAnchor(
|
||||
widget: OverlayContainer(
|
||||
child: _FieldEditorWidget(_fieldEditorBloc, fieldContextLoader),
|
||||
child: this,
|
||||
constraints: BoxConstraints.loose(const Size(280, 400)),
|
||||
),
|
||||
identifier: identifier(),
|
||||
@ -45,49 +70,23 @@ class FieldEditor extends FlowyOverlayDelegate {
|
||||
return (FieldEditor).toString();
|
||||
}
|
||||
|
||||
@override
|
||||
void didRemove() {
|
||||
_fieldEditorBloc.add(const FieldEditorEvent.done());
|
||||
}
|
||||
|
||||
@override
|
||||
bool asBarrier() => true;
|
||||
}
|
||||
|
||||
class _FieldEditorWidget extends StatelessWidget {
|
||||
final FieldEditorBloc editorBloc;
|
||||
final EditFieldContextLoader fieldContextLoader;
|
||||
const _FieldEditorWidget(this.editorBloc, this.fieldContextLoader, {Key? key}) : super(key: key);
|
||||
class _FieldPannel extends StatelessWidget {
|
||||
const _FieldPannel({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider.value(
|
||||
value: editorBloc,
|
||||
child: BlocBuilder<FieldEditorBloc, FieldEditorState>(
|
||||
builder: (context, state) {
|
||||
return state.editFieldContext.fold(
|
||||
() => const SizedBox(),
|
||||
(editFieldContext) => ListView(
|
||||
shrinkWrap: true,
|
||||
children: [
|
||||
FlowyText.medium(LocaleKeys.grid_field_editProperty.tr(), fontSize: 12),
|
||||
const VSpace(10),
|
||||
const _FieldNameTextField(),
|
||||
const VSpace(10),
|
||||
FieldEditorPannel(
|
||||
editFieldContext: editFieldContext,
|
||||
onSwitchToField: (fieldId, fieldType) {
|
||||
return fieldContextLoader.switchToField(fieldId, fieldType);
|
||||
},
|
||||
onUpdated: (field, typeOptionData) {
|
||||
context.read<FieldEditorBloc>().add(FieldEditorEvent.updateField(field, typeOptionData));
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
return BlocBuilder<FieldEditorBloc, FieldEditorState>(
|
||||
buildWhen: (p, c) => p.fieldContext != c.fieldContext,
|
||||
builder: (context, state) {
|
||||
return state.fieldContext.fold(
|
||||
() => const SizedBox(),
|
||||
(fieldContext) => FieldEditorPannel(fieldContext: fieldContext),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -97,16 +96,10 @@ class _FieldNameTextField extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocSelector<FieldEditorBloc, FieldEditorState, String>(
|
||||
selector: (state) {
|
||||
return state.editFieldContext.fold(
|
||||
() => "",
|
||||
(editFieldContext) => editFieldContext.gridField.name,
|
||||
);
|
||||
},
|
||||
builder: (context, name) {
|
||||
return BlocBuilder<FieldEditorBloc, FieldEditorState>(
|
||||
builder: (context, state) {
|
||||
return FieldNameTextField(
|
||||
name: name,
|
||||
name: state.name,
|
||||
errorText: context.read<FieldEditorBloc>().state.errorText,
|
||||
onNameChanged: (newName) {
|
||||
context.read<FieldEditorBloc>().add(FieldEditorEvent.updateName(newName));
|
||||
|
@ -1,20 +1,18 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:app_flowy/workspace/application/grid/field/type_option/multi_select_type_option.dart';
|
||||
import 'package:app_flowy/workspace/application/grid/field/type_option/type_option_service.dart';
|
||||
import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/header/type_option/checkbox.dart';
|
||||
import 'package:dartz/dartz.dart' show Either;
|
||||
import 'package:flowy_infra/image.dart';
|
||||
import 'package:flowy_infra/theme.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/button.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flowy_sdk/log.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid-data-model/grid.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/checkbox_type_option.pbserver.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/text_type_option.pb.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:app_flowy/startup/startup.dart';
|
||||
import 'package:app_flowy/workspace/application/grid/prelude.dart';
|
||||
import 'package:app_flowy/workspace/presentation/plugins/grid/src/layout/sizes.dart';
|
||||
import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/header/field_type_list.dart';
|
||||
@ -22,23 +20,21 @@ import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/header
|
||||
import 'field_type_extension.dart';
|
||||
import 'type_option/multi_select.dart';
|
||||
import 'type_option/number.dart';
|
||||
import 'type_option/rich_text.dart';
|
||||
import 'type_option/single_select.dart';
|
||||
import 'type_option/url.dart';
|
||||
|
||||
typedef UpdateFieldCallback = void Function(Field, Uint8List);
|
||||
typedef SwitchToFieldCallback = Future<Either<EditFieldContext, FlowyError>> Function(
|
||||
typedef SwitchToFieldCallback = Future<Either<FieldTypeOptionData, FlowyError>> Function(
|
||||
String fieldId,
|
||||
FieldType fieldType,
|
||||
);
|
||||
|
||||
class FieldEditorPannel extends StatefulWidget {
|
||||
final EditFieldContext editFieldContext;
|
||||
final UpdateFieldCallback onUpdated;
|
||||
final SwitchToFieldCallback onSwitchToField;
|
||||
final GridFieldContext fieldContext;
|
||||
|
||||
const FieldEditorPannel({
|
||||
required this.editFieldContext,
|
||||
required this.onUpdated,
|
||||
required this.onSwitchToField,
|
||||
required this.fieldContext,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@ -52,13 +48,10 @@ class _FieldEditorPannelState extends State<FieldEditorPannel> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) => getIt<FieldEditorPannelBloc>(param1: widget.editFieldContext),
|
||||
child: BlocConsumer<FieldEditorPannelBloc, FieldEditorPannelState>(
|
||||
listener: (context, state) {
|
||||
widget.onUpdated(state.field, state.typeOptionData);
|
||||
},
|
||||
create: (context) => FieldEditorPannelBloc(widget.fieldContext)..add(const FieldEditorPannelEvent.initial()),
|
||||
child: BlocBuilder<FieldEditorPannelBloc, FieldEditorPannelState>(
|
||||
builder: (context, state) {
|
||||
List<Widget> children = [_switchFieldTypeButton(context, state.field)];
|
||||
List<Widget> children = [_switchFieldTypeButton(context, widget.fieldContext.field)];
|
||||
final typeOptionWidget = _typeOptionWidget(context: context, state: state);
|
||||
|
||||
if (typeOptionWidget != null) {
|
||||
@ -84,19 +77,7 @@ class _FieldEditorPannelState extends State<FieldEditorPannel> {
|
||||
hoverColor: theme.hover,
|
||||
onTap: () {
|
||||
final list = FieldTypeList(onSelectField: (newFieldType) {
|
||||
widget.onSwitchToField(field.id, newFieldType).then((result) {
|
||||
result.fold(
|
||||
(editFieldContext) {
|
||||
context.read<FieldEditorPannelBloc>().add(
|
||||
FieldEditorPannelEvent.toFieldType(
|
||||
editFieldContext.gridField,
|
||||
editFieldContext.typeOptionData,
|
||||
),
|
||||
);
|
||||
},
|
||||
(err) => Log.error(err),
|
||||
);
|
||||
});
|
||||
widget.fieldContext.switchToField(newFieldType);
|
||||
});
|
||||
_showOverlay(context, list);
|
||||
},
|
||||
@ -115,18 +96,9 @@ class _FieldEditorPannelState extends State<FieldEditorPannel> {
|
||||
hideOverlay: _hideOverlay,
|
||||
);
|
||||
|
||||
final dataDelegate = TypeOptionDataDelegate(didUpdateTypeOptionData: (data) {
|
||||
context.read<FieldEditorPannelBloc>().add(FieldEditorPannelEvent.didUpdateTypeOptionData(data));
|
||||
});
|
||||
|
||||
final builder = _makeTypeOptionBuild(
|
||||
typeOptionContext: TypeOptionContext(
|
||||
gridId: state.gridId,
|
||||
field: state.field,
|
||||
data: state.typeOptionData,
|
||||
),
|
||||
typeOptionContext: _makeTypeOptionContext(widget.fieldContext),
|
||||
overlayDelegate: overlayDelegate,
|
||||
dataDelegate: dataDelegate,
|
||||
);
|
||||
|
||||
return builder.customWidget;
|
||||
@ -166,25 +138,86 @@ abstract class TypeOptionBuilder {
|
||||
TypeOptionBuilder _makeTypeOptionBuild({
|
||||
required TypeOptionContext typeOptionContext,
|
||||
required TypeOptionOverlayDelegate overlayDelegate,
|
||||
required TypeOptionDataDelegate dataDelegate,
|
||||
}) {
|
||||
switch (typeOptionContext.field.fieldType) {
|
||||
case FieldType.Checkbox:
|
||||
return CheckboxTypeOptionBuilder(typeOptionContext.data);
|
||||
return CheckboxTypeOptionBuilder(
|
||||
typeOptionContext as CheckboxTypeOptionContext,
|
||||
);
|
||||
case FieldType.DateTime:
|
||||
return DateTypeOptionBuilder(typeOptionContext.data, overlayDelegate, dataDelegate);
|
||||
return DateTypeOptionBuilder(
|
||||
typeOptionContext as DateTypeOptionContext,
|
||||
overlayDelegate,
|
||||
);
|
||||
case FieldType.SingleSelect:
|
||||
return SingleSelectTypeOptionBuilder(typeOptionContext, overlayDelegate, dataDelegate);
|
||||
return SingleSelectTypeOptionBuilder(
|
||||
typeOptionContext as SingleSelectTypeOptionContext,
|
||||
overlayDelegate,
|
||||
);
|
||||
case FieldType.MultiSelect:
|
||||
return MultiSelectTypeOptionBuilder(typeOptionContext, overlayDelegate, dataDelegate);
|
||||
return MultiSelectTypeOptionBuilder(
|
||||
typeOptionContext as MultiSelectTypeOptionContext,
|
||||
overlayDelegate,
|
||||
);
|
||||
case FieldType.Number:
|
||||
return NumberTypeOptionBuilder(typeOptionContext.data, overlayDelegate, dataDelegate);
|
||||
return NumberTypeOptionBuilder(
|
||||
typeOptionContext as NumberTypeOptionContext,
|
||||
overlayDelegate,
|
||||
);
|
||||
case FieldType.RichText:
|
||||
return RichTextTypeOptionBuilder(typeOptionContext.data);
|
||||
return RichTextTypeOptionBuilder(
|
||||
typeOptionContext as RichTextTypeOptionContext,
|
||||
);
|
||||
|
||||
default:
|
||||
throw UnimplementedError;
|
||||
case FieldType.URL:
|
||||
return URLTypeOptionBuilder(
|
||||
typeOptionContext as URLTypeOptionContext,
|
||||
);
|
||||
}
|
||||
throw UnimplementedError;
|
||||
}
|
||||
|
||||
TypeOptionContext _makeTypeOptionContext(GridFieldContext fieldContext) {
|
||||
switch (fieldContext.field.fieldType) {
|
||||
case FieldType.Checkbox:
|
||||
return CheckboxTypeOptionContext(
|
||||
fieldContext: fieldContext,
|
||||
dataBuilder: CheckboxTypeOptionDataBuilder(),
|
||||
);
|
||||
case FieldType.DateTime:
|
||||
return DateTypeOptionContext(
|
||||
fieldContext: fieldContext,
|
||||
dataBuilder: DateTypeOptionDataBuilder(),
|
||||
);
|
||||
case FieldType.MultiSelect:
|
||||
return MultiSelectTypeOptionContext(
|
||||
fieldContext: fieldContext,
|
||||
dataBuilder: MultiSelectTypeOptionDataBuilder(),
|
||||
);
|
||||
case FieldType.Number:
|
||||
return NumberTypeOptionContext(
|
||||
fieldContext: fieldContext,
|
||||
dataBuilder: NumberTypeOptionDataBuilder(),
|
||||
);
|
||||
case FieldType.RichText:
|
||||
return RichTextTypeOptionContext(
|
||||
fieldContext: fieldContext,
|
||||
dataBuilder: RichTextTypeOptionDataBuilder(),
|
||||
);
|
||||
case FieldType.SingleSelect:
|
||||
return SingleSelectTypeOptionContext(
|
||||
fieldContext: fieldContext,
|
||||
dataBuilder: SingleSelectTypeOptionDataBuilder(),
|
||||
);
|
||||
|
||||
case FieldType.URL:
|
||||
return URLTypeOptionContext(
|
||||
fieldContext: fieldContext,
|
||||
dataBuilder: URLTypeOptionDataBuilder(),
|
||||
);
|
||||
}
|
||||
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
abstract class TypeOptionWidget extends StatelessWidget {
|
||||
@ -208,29 +241,3 @@ class TypeOptionOverlayDelegate {
|
||||
required this.hideOverlay,
|
||||
});
|
||||
}
|
||||
|
||||
class TypeOptionDataDelegate {
|
||||
TypeOptionDataCallback didUpdateTypeOptionData;
|
||||
|
||||
TypeOptionDataDelegate({
|
||||
required this.didUpdateTypeOptionData,
|
||||
});
|
||||
}
|
||||
|
||||
class RichTextTypeOptionBuilder extends TypeOptionBuilder {
|
||||
RichTextTypeOption typeOption;
|
||||
|
||||
RichTextTypeOptionBuilder(TypeOptionData typeOptionData) : typeOption = RichTextTypeOption.fromBuffer(typeOptionData);
|
||||
|
||||
@override
|
||||
Widget? get customWidget => null;
|
||||
}
|
||||
|
||||
class CheckboxTypeOptionBuilder extends TypeOptionBuilder {
|
||||
CheckboxTypeOption typeOption;
|
||||
|
||||
CheckboxTypeOptionBuilder(TypeOptionData typeOptionData) : typeOption = CheckboxTypeOption.fromBuffer(typeOptionData);
|
||||
|
||||
@override
|
||||
Widget? get customWidget => null;
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ import 'package:flowy_infra_ui/widget/rounded_input_field.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class FieldNameTextField extends StatelessWidget {
|
||||
class FieldNameTextField extends StatefulWidget {
|
||||
final void Function(String) onNameChanged;
|
||||
final String name;
|
||||
final String errorText;
|
||||
@ -14,19 +14,41 @@ class FieldNameTextField extends StatelessWidget {
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<FieldNameTextField> createState() => _FieldNameTextFieldState();
|
||||
}
|
||||
|
||||
class _FieldNameTextFieldState extends State<FieldNameTextField> {
|
||||
late String name;
|
||||
TextEditingController controller = TextEditingController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
controller.text = widget.name;
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = context.watch<AppTheme>();
|
||||
return RoundedInputField(
|
||||
height: 36,
|
||||
style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500),
|
||||
initialValue: name,
|
||||
controller: controller,
|
||||
normalBorderColor: theme.shader4,
|
||||
errorBorderColor: theme.red,
|
||||
focusBorderColor: theme.main1,
|
||||
cursorColor: theme.main1,
|
||||
errorText: errorText,
|
||||
onChanged: onNameChanged,
|
||||
errorText: widget.errorText,
|
||||
onChanged: widget.onNameChanged,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant FieldNameTextField oldWidget) {
|
||||
controller.text = widget.name;
|
||||
controller.selection = TextSelection.fromPosition(TextPosition(offset: controller.text.length));
|
||||
|
||||
super.didUpdateWidget(oldWidget);
|
||||
}
|
||||
}
|
||||
|
@ -17,9 +17,10 @@ extension FieldTypeListExtension on FieldType {
|
||||
return "grid/field/text";
|
||||
case FieldType.SingleSelect:
|
||||
return "grid/field/single_select";
|
||||
default:
|
||||
throw UnimplementedError;
|
||||
case FieldType.URL:
|
||||
return "grid/field/url";
|
||||
}
|
||||
throw UnimplementedError;
|
||||
}
|
||||
|
||||
String title() {
|
||||
@ -36,8 +37,9 @@ extension FieldTypeListExtension on FieldType {
|
||||
return LocaleKeys.grid_field_textFieldName.tr();
|
||||
case FieldType.SingleSelect:
|
||||
return LocaleKeys.grid_field_singleSelectFieldName.tr();
|
||||
default:
|
||||
throw UnimplementedError;
|
||||
case FieldType.URL:
|
||||
return LocaleKeys.grid_field_urlFieldName.tr();
|
||||
}
|
||||
throw UnimplementedError;
|
||||
}
|
||||
}
|
||||
|
@ -150,7 +150,8 @@ class CreateFieldButton extends StatelessWidget {
|
||||
hoverColor: theme.hover,
|
||||
onTap: () => FieldEditor(
|
||||
gridId: gridId,
|
||||
fieldContextLoader: NewFieldContextLoader(gridId: gridId),
|
||||
fieldName: "",
|
||||
contextLoader: NewFieldContextLoader(gridId: gridId),
|
||||
).show(context),
|
||||
leftIcon: svgWidget("home/add"),
|
||||
);
|
||||
|
@ -0,0 +1,20 @@
|
||||
import 'package:app_flowy/workspace/application/grid/field/type_option/type_option_service.dart';
|
||||
import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/header/field_editor_pannel.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/checkbox_type_option.pb.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
typedef CheckboxTypeOptionContext = TypeOptionContext<CheckboxTypeOption>;
|
||||
|
||||
class CheckboxTypeOptionDataBuilder extends TypeOptionDataBuilder<CheckboxTypeOption> {
|
||||
@override
|
||||
CheckboxTypeOption fromBuffer(List<int> buffer) {
|
||||
return CheckboxTypeOption.fromBuffer(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
class CheckboxTypeOptionBuilder extends TypeOptionBuilder {
|
||||
CheckboxTypeOptionBuilder(CheckboxTypeOptionContext typeOptionContext);
|
||||
|
||||
@override
|
||||
Widget? get customWidget => null;
|
||||
}
|
@ -1,4 +1,3 @@
|
||||
import 'package:app_flowy/startup/startup.dart';
|
||||
import 'package:app_flowy/workspace/application/grid/field/type_option/date_bloc.dart';
|
||||
import 'package:app_flowy/workspace/presentation/plugins/grid/src/layout/sizes.dart';
|
||||
import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/header/field_editor_pannel.dart';
|
||||
@ -18,12 +17,10 @@ class DateTypeOptionBuilder extends TypeOptionBuilder {
|
||||
final DateTypeOptionWidget _widget;
|
||||
|
||||
DateTypeOptionBuilder(
|
||||
TypeOptionData typeOptionData,
|
||||
DateTypeOptionContext typeOptionContext,
|
||||
TypeOptionOverlayDelegate overlayDelegate,
|
||||
TypeOptionDataDelegate dataDelegate,
|
||||
) : _widget = DateTypeOptionWidget(
|
||||
typeOption: DateTypeOption.fromBuffer(typeOptionData),
|
||||
dataDelegate: dataDelegate,
|
||||
typeOptionContext: typeOptionContext,
|
||||
overlayDelegate: overlayDelegate,
|
||||
);
|
||||
|
||||
@ -32,12 +29,11 @@ class DateTypeOptionBuilder extends TypeOptionBuilder {
|
||||
}
|
||||
|
||||
class DateTypeOptionWidget extends TypeOptionWidget {
|
||||
final DateTypeOption typeOption;
|
||||
final DateTypeOptionContext typeOptionContext;
|
||||
final TypeOptionOverlayDelegate overlayDelegate;
|
||||
final TypeOptionDataDelegate dataDelegate;
|
||||
|
||||
const DateTypeOptionWidget({
|
||||
required this.typeOption,
|
||||
required this.dataDelegate,
|
||||
required this.typeOptionContext,
|
||||
required this.overlayDelegate,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
@ -45,9 +41,9 @@ class DateTypeOptionWidget extends TypeOptionWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) => getIt<DateTypeOptionBloc>(param1: typeOption),
|
||||
create: (context) => DateTypeOptionBloc(typeOptionContext: typeOptionContext),
|
||||
child: BlocConsumer<DateTypeOptionBloc, DateTypeOptionState>(
|
||||
listener: (context, state) => dataDelegate.didUpdateTypeOptionData(state.typeOption.writeToBuffer()),
|
||||
listener: (context, state) => typeOptionContext.typeOption = state.typeOption,
|
||||
builder: (context, state) {
|
||||
return Column(children: [
|
||||
_renderDateFormatButton(context, state.typeOption.dateFormat),
|
||||
|
@ -1,22 +1,18 @@
|
||||
import 'package:app_flowy/workspace/application/grid/field/type_option/multi_select_bloc.dart';
|
||||
import 'package:app_flowy/workspace/application/grid/field/type_option/type_option_service.dart';
|
||||
import 'package:app_flowy/workspace/application/grid/field/type_option/multi_select_type_option.dart';
|
||||
import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/header/field_editor_pannel.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import 'field_option_pannel.dart';
|
||||
import 'select_option.dart';
|
||||
|
||||
class MultiSelectTypeOptionBuilder extends TypeOptionBuilder {
|
||||
final MultiSelectTypeOptionWidget _widget;
|
||||
|
||||
MultiSelectTypeOptionBuilder(
|
||||
TypeOptionContext typeOptionContext,
|
||||
MultiSelectTypeOptionContext typeOptionContext,
|
||||
TypeOptionOverlayDelegate overlayDelegate,
|
||||
TypeOptionDataDelegate dataDelegate,
|
||||
) : _widget = MultiSelectTypeOptionWidget(
|
||||
typeOptionContext: typeOptionContext,
|
||||
overlayDelegate: overlayDelegate,
|
||||
dataDelegate: dataDelegate,
|
||||
);
|
||||
|
||||
@override
|
||||
@ -24,44 +20,23 @@ class MultiSelectTypeOptionBuilder extends TypeOptionBuilder {
|
||||
}
|
||||
|
||||
class MultiSelectTypeOptionWidget extends TypeOptionWidget {
|
||||
final TypeOptionContext typeOptionContext;
|
||||
final MultiSelectTypeOptionContext typeOptionContext;
|
||||
final TypeOptionOverlayDelegate overlayDelegate;
|
||||
final TypeOptionDataDelegate dataDelegate;
|
||||
|
||||
const MultiSelectTypeOptionWidget({
|
||||
required this.typeOptionContext,
|
||||
required this.overlayDelegate,
|
||||
required this.dataDelegate,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) => MultiSelectTypeOptionBloc(typeOptionContext),
|
||||
child: BlocConsumer<MultiSelectTypeOptionBloc, MultiSelectTypeOptionState>(
|
||||
listener: (context, state) {
|
||||
dataDelegate.didUpdateTypeOptionData(state.typeOption.writeToBuffer());
|
||||
},
|
||||
builder: (context, state) {
|
||||
return FieldSelectOptionPannel(
|
||||
options: state.typeOption.options,
|
||||
beginEdit: () {
|
||||
overlayDelegate.hideOverlay(context);
|
||||
},
|
||||
createOptionCallback: (name) {
|
||||
context.read<MultiSelectTypeOptionBloc>().add(MultiSelectTypeOptionEvent.createOption(name));
|
||||
},
|
||||
updateOptionCallback: (updateOption) {
|
||||
context.read<MultiSelectTypeOptionBloc>().add(MultiSelectTypeOptionEvent.updateOption(updateOption));
|
||||
},
|
||||
deleteOptionCallback: (deleteOption) {
|
||||
context.read<MultiSelectTypeOptionBloc>().add(MultiSelectTypeOptionEvent.deleteOption(deleteOption));
|
||||
},
|
||||
overlayDelegate: overlayDelegate,
|
||||
key: ValueKey(state.typeOption.hashCode),
|
||||
);
|
||||
},
|
||||
),
|
||||
return SelectOptionTypeOptionWidget(
|
||||
options: typeOptionContext.typeOption.options,
|
||||
beginEdit: () => overlayDelegate.hideOverlay(context),
|
||||
overlayDelegate: overlayDelegate,
|
||||
typeOptionAction: typeOptionContext,
|
||||
// key: ValueKey(state.typeOption.hashCode),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,3 @@
|
||||
import 'package:app_flowy/startup/startup.dart';
|
||||
import 'package:app_flowy/workspace/application/grid/field/type_option/number_bloc.dart';
|
||||
import 'package:app_flowy/workspace/application/grid/field/type_option/number_format_bloc.dart';
|
||||
import 'package:app_flowy/workspace/presentation/plugins/grid/src/layout/sizes.dart';
|
||||
@ -10,7 +9,7 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/button.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/number_type_option.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/format.pbenum.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:easy_localization/easy_localization.dart' hide NumberFormat;
|
||||
@ -20,12 +19,10 @@ class NumberTypeOptionBuilder extends TypeOptionBuilder {
|
||||
final NumberTypeOptionWidget _widget;
|
||||
|
||||
NumberTypeOptionBuilder(
|
||||
TypeOptionData typeOptionData,
|
||||
NumberTypeOptionContext typeOptionContext,
|
||||
TypeOptionOverlayDelegate overlayDelegate,
|
||||
TypeOptionDataDelegate dataDelegate,
|
||||
) : _widget = NumberTypeOptionWidget(
|
||||
typeOption: NumberTypeOption.fromBuffer(typeOptionData),
|
||||
dataDelegate: dataDelegate,
|
||||
typeOptionContext: typeOptionContext,
|
||||
overlayDelegate: overlayDelegate,
|
||||
);
|
||||
|
||||
@ -34,22 +31,23 @@ class NumberTypeOptionBuilder extends TypeOptionBuilder {
|
||||
}
|
||||
|
||||
class NumberTypeOptionWidget extends TypeOptionWidget {
|
||||
final TypeOptionDataDelegate dataDelegate;
|
||||
final TypeOptionOverlayDelegate overlayDelegate;
|
||||
final NumberTypeOption typeOption;
|
||||
const NumberTypeOptionWidget(
|
||||
{required this.typeOption, required this.dataDelegate, required this.overlayDelegate, Key? key})
|
||||
: super(key: key);
|
||||
final NumberTypeOptionContext typeOptionContext;
|
||||
const NumberTypeOptionWidget({
|
||||
required this.typeOptionContext,
|
||||
required this.overlayDelegate,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = context.watch<AppTheme>();
|
||||
return BlocProvider(
|
||||
create: (context) => getIt<NumberTypeOptionBloc>(param1: typeOption),
|
||||
create: (context) => NumberTypeOptionBloc(typeOptionContext: typeOptionContext),
|
||||
child: SizedBox(
|
||||
height: GridSize.typeOptionItemHeight,
|
||||
child: BlocConsumer<NumberTypeOptionBloc, NumberTypeOptionState>(
|
||||
listener: (context, state) => dataDelegate.didUpdateTypeOptionData(state.typeOption.writeToBuffer()),
|
||||
listener: (context, state) => typeOptionContext.typeOption = state.typeOption,
|
||||
builder: (context, state) {
|
||||
return FlowyButton(
|
||||
text: Row(
|
||||
|
@ -0,0 +1,21 @@
|
||||
import 'package:app_flowy/workspace/application/grid/field/type_option/type_option_service.dart';
|
||||
import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/header/field_editor_pannel.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/text_type_option.pb.dart';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
typedef RichTextTypeOptionContext = TypeOptionContext<RichTextTypeOption>;
|
||||
|
||||
class RichTextTypeOptionDataBuilder extends TypeOptionDataBuilder<RichTextTypeOption> {
|
||||
@override
|
||||
RichTextTypeOption fromBuffer(List<int> buffer) {
|
||||
return RichTextTypeOption.fromBuffer(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
class RichTextTypeOptionBuilder extends TypeOptionBuilder {
|
||||
RichTextTypeOptionBuilder(RichTextTypeOptionContext typeOptionContext);
|
||||
|
||||
@override
|
||||
Widget? get customWidget => null;
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user