Merge branch 'main' into add_translated_titles_304

This commit is contained in:
Nathan.fooo 2022-06-11 06:58:06 +08:00 committed by GitHub
commit 827e542c6e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
219 changed files with 6733 additions and 4095 deletions

View File

@ -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

View File

@ -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

View File

@ -25,6 +25,7 @@ jobs:
- uses: subosito/flutter-action@v2
with:
channel: 'stable'
flutter-version: '3.0.0'
cache: true
- name: Cache Cargo

View File

@ -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 }}

View File

@ -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

View File

@ -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/"
},
]
}

View File

@ -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"
}
]
}

View File

@ -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"

View File

@ -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"
},
]
}

View File

@ -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,
}

View File

@ -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}/../"
},
}
]
}

View 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

View File

@ -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"
}
}
}

View File

@ -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",
@ -144,3 +144,4 @@
}
}

View 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"
}
}
}

View File

@ -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"
}
}
}

View 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,
);
}
}

View File

@ -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),
);

View File

@ -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 {

View File

@ -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';
}

View File

@ -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';

View File

@ -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}) {

View File

@ -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

View File

@ -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) {

View File

@ -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';

View File

@ -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!);

View File

@ -104,6 +104,8 @@ class GridCellCache {
}
Future<void> dispose() async {
_fieldListenerByFieldId.clear();
_cellDataByFieldId.clear();
fieldDelegate.dispose();
}
}

View File

@ -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);
}
}

View File

@ -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();

View File

@ -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";
}

View File

@ -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) {

View File

@ -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;
}

View File

@ -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),
);
}
}

View File

@ -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 ?? [],
);
}

View File

@ -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 ?? [],

View File

@ -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() ?? "",
);
}

View File

@ -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 ?? "",
);
}
}

View File

@ -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 ?? "",
);
}
}

View File

@ -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,
);
}

View File

@ -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(),
);
}

View File

@ -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,
);
}

View File

@ -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,
);
}

View File

@ -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);
}
}

View File

@ -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(

View File

@ -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(),
);
}

View File

@ -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,
);
}

View File

@ -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);
}
}

View File

@ -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(

View File

@ -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';

View File

@ -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(),
);
}

View File

@ -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,
);
}

View File

@ -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);
}
}

View File

@ -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),
);
});
}
}

View File

@ -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);
}

View File

@ -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(

View File

@ -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;
}
}

View File

@ -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';

View File

@ -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,
];
}

View File

@ -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(

View File

@ -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,
);
}

View File

@ -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()),

View File

@ -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) {

View File

@ -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(

View File

@ -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);
},
);
},

View File

@ -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());
});
}

View File

@ -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),
)
],
),
)),
);
},
);

View File

@ -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),

View File

@ -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),
);
}

View File

@ -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);

View File

@ -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();
}

View File

@ -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,
);
}

View File

@ -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,
[

View File

@ -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);
}
}

View File

@ -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,
),
);
},
);
}
}

View File

@ -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;
}

View File

@ -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();
}
}
}

View File

@ -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";
}
}
}

View File

@ -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;
}

View File

@ -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> {

View File

@ -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));
}
}

View File

@ -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';

View File

@ -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),
// );
}
}

View File

@ -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),
);
}

View File

@ -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(

View File

@ -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);

View File

@ -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));
}
}

View File

@ -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));
}
}
}
}

View File

@ -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());
}
}

View File

@ -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 = "";
}
},
);
}

View File

@ -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),
),
),
);

View File

@ -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));

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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"),
);

View File

@ -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;
}

View File

@ -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),

View File

@ -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),
);
}
}

View File

@ -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(

View File

@ -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