Merge branch 'main' into feat/flowy-overlay

This commit is contained in:
Vincent Chan 2022-09-01 14:37:16 +08:00
commit 9b5184cd72
294 changed files with 7620 additions and 2230 deletions

View File

@ -59,6 +59,7 @@ jobs:
sudo wget -qO /etc/apt/sources.list.d/dart_stable.list https://storage.googleapis.com/download.dartlang.org/linux/debian/dart_stable.list sudo wget -qO /etc/apt/sources.list.d/dart_stable.list https://storage.googleapis.com/download.dartlang.org/linux/debian/dart_stable.list
sudo apt-get update sudo apt-get update
sudo apt-get install -y dart curl build-essential libsqlite3-dev libssl-dev clang cmake ninja-build pkg-config libgtk-3-dev sudo apt-get install -y dart curl build-essential libsqlite3-dev libssl-dev clang cmake ninja-build pkg-config libgtk-3-dev
sudo apt-get install keybinder-3.0
elif [ "$RUNNER_OS" == "macOS" ]; then elif [ "$RUNNER_OS" == "macOS" ]; then
echo 'do nothing' echo 'do nothing'
fi fi

View File

@ -4,10 +4,14 @@ on:
push: push:
branches: branches:
- "main" - "main"
paths:
- "frontend/app_flowy/packages/appflowy_editor"
pull_request: pull_request:
branches: branches:
- "main" - "main"
paths:
- "frontend/app_flowy/packages/appflowy_editor"
env: env:
CARGO_TERM_COLOR: always CARGO_TERM_COLOR: always

View File

@ -58,6 +58,7 @@ jobs:
sudo wget -qO /etc/apt/trusted.gpg.d/dart_linux_signing_key.asc https://dl-ssl.google.com/linux/linux_signing_key.pub sudo wget -qO /etc/apt/trusted.gpg.d/dart_linux_signing_key.asc https://dl-ssl.google.com/linux/linux_signing_key.pub
sudo apt-get update sudo apt-get update
sudo apt-get install -y build-essential libsqlite3-dev libssl-dev clang cmake ninja-build pkg-config libgtk-3-dev sudo apt-get install -y build-essential libsqlite3-dev libssl-dev clang cmake ninja-build pkg-config libgtk-3-dev
sudo apt-get install keybinder-3.0
source $HOME/.cargo/env source $HOME/.cargo/env
cargo install --force cargo-make cargo install --force cargo-make
cargo install --force duckscript_cli cargo install --force duckscript_cli

View File

@ -1,6 +1,29 @@
# Release Notes # Release Notes
## Version 0.0.4 - 2022-06-06 ## Version 0.0.5 - beta.2 - beta.1 - 09/01/2022
New features
- Board-view database
- Support start editing after creating a new card
- Support editing the card directly by clicking the edit button
- Add the `No Status` column to display the cards while their status is empty
### Bug Fixes
- Optimize insert card animation
- Fix some UI bugs
## Version 0.0.5 - beta.1 - 08/25/2022
New features
- Board-view database
- Group by single select
- drag and drop cards
- insert / delete cards
![Aug-25-2022 16-22-38](https://user-images.githubusercontent.com/86001920/186614248-23186dfe-410e-427a-8cc6-865b1f79e074.gif)
## Version 0.0.4 - 06/06/2022
- Drag to adjust the width of a column - Drag to adjust the width of a column
- Upgrade to Flutter 3.0 - Upgrade to Flutter 3.0
- Native support for M1 chip - Native support for M1 chip
@ -12,12 +35,12 @@
- Fixed some bugs - Fixed some bugs
## Version 0.0.4 - beta.3 - 2022-05-02 ## Version 0.0.4 - beta.3 - 05/02/2022
- Drag to reorder app/ view/ field - Drag to reorder app/ view/ field
- Row record open as a page - Row record open as a page
- Auto resize the height of the row in the grid - Auto resize the height of the row in the grid
- Support more number formats - Support more number formats
- Search column options, supporting Single select, Multi-select, and number format - 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) ![May-03-2022 10-03-00](https://user-images.githubusercontent.com/86001920/166394640-a8f1f3bc-5f20-4033-93e9-16bc308d7005.gif)
@ -27,7 +50,7 @@
- Fixed some bugs - Fixed some bugs
## Version 0.0.4 - beta.2 - 2022-04-11 ## Version 0.0.4 - beta.2 - 04/11/2022
- Support properties: Text, Number, Date, Checkbox, Select, Multi-select - Support properties: Text, Number, Date, Checkbox, Select, Multi-select
- Insert / delete rows - Insert / delete rows
@ -35,16 +58,16 @@
- Edit property - Edit property
![](https://user-images.githubusercontent.com/12026239/162753644-bf2f4e7a-2367-4d48-87e6-35e244e83a5b.png) ![](https://user-images.githubusercontent.com/12026239/162753644-bf2f4e7a-2367-4d48-87e6-35e244e83a5b.png)
## Version 0.0.4 - beta.1 - 2022-04-08 ## Version 0.0.4 - beta.1 - 04/08/2022
v0.0.4 - beta.1 is pre-release v0.0.4 - beta.1 is pre-release
New features New features
- Table-view database - Table-view database
- supported column types: Text, Checbox, Single-select, Multi-select, Numbers - supported column types: Text, Checkbox, Single-select, Multi-select, Numbers
- hide / delete columns - hide / delete columns
- insert rows - insert rows
## Version 0.0.3 - 2022-02-23 ## Version 0.0.3 - 02/23/2022
v0.0.3 is production ready, available on Linux, macOS, and Windows v0.0.3 is production ready, available on Linux, macOS, and Windows
New features New features

View File

@ -16,6 +16,18 @@
}, },
"cwd": "${workspaceRoot}/app_flowy" "cwd": "${workspaceRoot}/app_flowy"
}, },
{
// This task builds the Rust and Dart code of AppFlowy for android.
"name": "AF: Run Android",
"request": "launch",
"program": "./lib/main.dart",
"type": "dart",
"preLaunchTask": "AF: build_mobile_sdk",
"env": {
"RUST_LOG": "info"
},
"cwd": "${workspaceRoot}/app_flowy"
},
{ {
"name": "AF: Debug Rust", "name": "AF: Debug Rust",
"request": "attach", "request": "attach",
@ -48,6 +60,21 @@
}, },
"cwd": "${workspaceRoot}/app_flowy" "cwd": "${workspaceRoot}/app_flowy"
}, },
{
// 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 (Android)",
"request": "launch",
"program": "./lib/main.dart",
"type": "dart",
"preLaunchTask": "AF: Clean + Rebuild All (Android)",
"env": {
"RUST_LOG": "info"
},
"cwd": "${workspaceRoot}/app_flowy"
},
{ {
"name": "AF: Build All (rustlog: trace)", "name": "AF: Build All (rustlog: trace)",
"request": "launch", "request": "launch",
@ -59,6 +86,17 @@
}, },
"cwd": "${workspaceRoot}/app_flowy" "cwd": "${workspaceRoot}/app_flowy"
}, },
{
"name": "AF: Build All Android (rustlog: trace)",
"request": "launch",
"program": "./lib/main.dart",
"type": "dart",
"preLaunchTask": "AF: build_mobile_sdk",
"env": {
"RUST_LOG": "trace"
},
"cwd": "${workspaceRoot}/app_flowy"
},
{ {
"name": "AF: app_flowy (profile mode)", "name": "AF: app_flowy (profile mode)",
"request": "launch", "request": "launch",

View File

@ -27,6 +27,33 @@
"panel": "new" "panel": "new"
} }
}, },
{
"label": "AF: Clean + Rebuild All (Android)",
"type": "shell",
"dependsOrder": "sequence",
"dependsOn": [
"AF: Rust Clean",
"AF: Flutter Clean",
"AF: build_flowy_sdk_for_android",
"AF: Flutter Pub Get",
"AF: Flutter Package Get",
"AF: Generate Language Files",
"AF: Generate Freezed Files",
],
"presentation": {
"reveal": "always",
"panel": "new",
},
},
{
"label": "AF: build_flowy_sdk_for_android",
"type": "shell",
"command": "cargo make --profile development-android flowy-sdk-dev-android",
"group": "build",
"options": {
"cwd": "${workspaceFolder}"
}
},
{ {
"label": "AF: build_flowy_sdk", "label": "AF: build_flowy_sdk",
"type": "shell", "type": "shell",

View File

@ -22,7 +22,7 @@ CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true
CARGO_MAKE_CRATE_FS_NAME = "dart_ffi" CARGO_MAKE_CRATE_FS_NAME = "dart_ffi"
CARGO_MAKE_CRATE_NAME = "dart-ffi" CARGO_MAKE_CRATE_NAME = "dart-ffi"
LIB_NAME = "dart_ffi" LIB_NAME = "dart_ffi"
CURRENT_APP_VERSION = "0.0.4" CURRENT_APP_VERSION = "0.0.5"
FEATURES = "flutter" FEATURES = "flutter"
PRODUCT_NAME = "AppFlowy" PRODUCT_NAME = "AppFlowy"
# CRATE_TYPE: https://doc.rust-lang.org/reference/linkage.html # CRATE_TYPE: https://doc.rust-lang.org/reference/linkage.html
@ -161,6 +161,11 @@ TARGET_OS = "ios"
FLUTTER_OUTPUT_DIR = "Release" FLUTTER_OUTPUT_DIR = "Release"
PRODUCT_EXT = "ipa" PRODUCT_EXT = "ipa"
[env.development-android]
BUILD_FLAG = "debug"
TARGET_OS = "android"
CRATE_TYPE = "cdylib"
FLUTTER_OUTPUT_DIR = "Debug"
[tasks.setup-crate-type] [tasks.setup-crate-type]
private = true private = true

View File

@ -0,0 +1,64 @@
# Description
This is a guide on how to build the rust SDK for AppFlowy on android.
Compiling the sdk is easy it just needs a few tweaks.
When compiling for android we need the following pre-requisites:
- Android NDK Tools. (v24 has been tested).
- Cargo NDK. (@latest version).
**Getting the tools**
- Install cargo-ndk ```bash cargo install cargo-ndk```.
- [Download](https://developer.android.com/ndk/downloads/) Android NDK version 24.
- When downloading Android NDK you can get the compressed version as a standalone from the site.
Or you can download it through [Android Studio](https://developer.android.com/studio).
- After downloading the two you need to set the environment variables. For Windows that's a seperate process.
On MacOs and Linux the process is similar.
- The variables needed are '$ANDROID_NDK_HOME', this will point to where the NDK is located.
---
**Cargo Config File**
This code needs to be written in ~/.cargo/config, this helps cargo know where to locate the android tools(linker and archiver).
**NB** Keep in mind just replace 'user' with your own user name. Or just point it to the location of where you put the NDK.
```toml
[target.aarch64-linux-android]
ar = "/home/user/Android/Sdk/ndk/24.0.8215888/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ar"
linker = "/home/user/Android/Sdk/ndk/24.0.8215888/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android29-clang"
[target.armv7-linux-androideabi]
ar = "/home/user/Android/Sdk/ndk/24.0.8215888/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ar"
linker = "/home/user/Android/Sdk/ndk/24.0.8215888/toolchains/llvm/prebuilt/linux-x86_64/bin/armv7a-linux-androideabi29-clang"
[target.i686-linux-android]
ar = "/home/user/Android/Sdk/ndk/24.0.8215888/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ar"
linker = "/home/user/Android/Sdk/ndk/24.0.8215888/toolchains/llvm/prebuilt/linux-x86_64/bin/i686-linux-android29-clang"
[target.x86_64-linux-android]
ar = "/home/user/Android/Sdk/ndk/24.0.8215888/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ar"
linker = "/home/user/Android/Sdk/ndk/24.0.8215888/toolchains/llvm/prebuilt/linux-x86_64/bin/x86_64-linux-android29-clang"
```
**Clang Fix**
In order to get clang to work properly with version 24 you need to create this file.
libgcc.a, then add this one line.
```
INPUT(-lunwind)
```
**Folder path: 'Android/Sdk/ndk/24.0.8215888/toolchains/llvm/prebuilt/linux-x86_64/lib64/clang/14.0.1/lib/linux'.**
After that you have to copy this file into three different folders namely aarch64, arm, i386 and x86_64.
We have to do this so we Android NDK can find clang on our system, if we used NDK 22 we wouldnt have to do this process.
Though using NDK v22 will not give us alot of features to work with.
This github [issue](https://github.com/fzyzcjy/flutter_rust_bridge/issues/419) explains the reason why we are doing this.
---
**Android NDK**
After installing the NDK tools for android you should export the PATH to your config file
(.vimrc, .zshrc, .profile, .bashrc file), That way it can be found.
```vim
export PATH=/home/sean/Android/Sdk/ndk/24.0.8215888
```

View File

@ -26,7 +26,8 @@ apply plugin: 'kotlin-android'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
android { android {
compileSdkVersion 30 compileSdkVersion 31
ndkVersion "24.0.8215888"
compileOptions { compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_1_8
@ -39,21 +40,26 @@ android {
sourceSets { sourceSets {
main.java.srcDirs += 'src/main/kotlin' main.java.srcDirs += 'src/main/kotlin'
main.jniLibs.srcDirs += 'jniLibs/'
} }
defaultConfig { defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "com.example.app_flowy" applicationId "com.example.app_flowy"
minSdkVersion 16 minSdkVersion 19
targetSdkVersion 30 targetSdkVersion 31
versionCode flutterVersionCode.toInteger() versionCode flutterVersionCode.toInteger()
versionName flutterVersionName versionName flutterVersionName
multiDexEnabled true
} }
buildTypes { buildTypes {
release { release {
// TODO: Add your own signing config for the release build. // TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works. // Signing with the debug keys for now, so `flutter run --release` works.
minifyEnabled true
shrinkResources true
signingConfig signingConfigs.debug signingConfig signingConfigs.debug
} }
} }
@ -65,4 +71,5 @@ flutter {
dependencies { dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation "com.android.support:multidex:2.0.1"
} }

View File

@ -2,7 +2,8 @@
package="com.example.app_flowy"> package="com.example.app_flowy">
<application <application
android:label="app_flowy" android:label="app_flowy"
android:icon="@mipmap/ic_launcher"> android:icon="@mipmap/ic_launcher"
android:name="${applicationName}">
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:launchMode="singleTop" android:launchMode="singleTop"

View File

@ -1,5 +1,5 @@
buildscript { buildscript {
ext.kotlin_version = '1.3.50' ext.kotlin_version = '1.6.10'
repositories { repositories {
google() google()
mavenCentral() mavenCentral()

View File

@ -1,3 +1,4 @@
org.gradle.jvmargs=-Xmx1536M org.gradle.jvmargs=-Xmx1536M
android.useAndroidX=true android.useAndroidX=true
android.enableJetifier=true android.enableJetifier=true
org.gradle.caching=true

View File

@ -1,6 +1,5 @@
#Fri Jun 23 08:50:38 CEST 2017
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.1-all.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip

View File

@ -9,3 +9,19 @@ localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }
def flutterSdkPath = properties.getProperty("flutter.sdk") def flutterSdkPath = properties.getProperty("flutter.sdk")
assert flutterSdkPath != null, "flutter.sdk not set in local.properties" assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"
def flutterProjectRoot = rootProject.projectDir.parentFile.toPath()
def plugins = new Properties()
def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins')
if(pluginsFile.exists()){
pluginsFile.withReader('UTF-8'){reader -> plugins.load(reader)}
}
plugins.each{name, path ->
def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile()
include ":$name"
project(":$name").projectDir = pluginDirectory
}

View File

@ -141,5 +141,9 @@
"lightLabel": "Mode Clar", "lightLabel": "Mode Clar",
"darkLabel": "Mode Fosc" "darkLabel": "Mode Fosc"
} }
},
"sideBar": {
"openSidebar": "Open sidebar",
"closeSidebar": "Close sidebar"
} }
} }

View File

@ -141,6 +141,10 @@
"lightLabel": "Heller Modus", "lightLabel": "Heller Modus",
"darkLabel": "Dunkler Modus" "darkLabel": "Dunkler Modus"
} }
},
"sideBar": {
"openSidebar": "Open sidebar",
"closeSidebar": "Close sidebar"
} }
} }

View File

@ -95,7 +95,13 @@
"tooltip": { "tooltip": {
"lightMode": "Switch to Light mode", "lightMode": "Switch to Light mode",
"darkMode": "Switch to Dark mode", "darkMode": "Switch to Dark mode",
"openAsPage": "Open as a Page" "openAsPage": "Open as a Page",
"addNewRow": "Add a new row",
"openMenu": "Click to open menu"
},
"sideBar": {
"closeSidebar": "Close side bar",
"openSidebar": "Open side bar"
}, },
"notifications": { "notifications": {
"export": { "export": {
@ -183,7 +189,8 @@
"addSelectOption": "Add an option", "addSelectOption": "Add an option",
"optionTitle": "Options", "optionTitle": "Options",
"addOption": "Add option", "addOption": "Add option",
"editProperty": "Edit property" "editProperty": "Edit property",
"newColumn": "New column"
}, },
"row": { "row": {
"duplicate": "Duplicate", "duplicate": "Duplicate",
@ -215,5 +222,10 @@
"timeHintTextInTwelveHour": "12:00 AM", "timeHintTextInTwelveHour": "12:00 AM",
"timeHintTextInTwentyFourHour": "12:00" "timeHintTextInTwentyFourHour": "12:00"
} }
},
"board": {
"column": {
"create_new_card": "New"
}
} }
} }

View File

@ -213,5 +213,9 @@
"timeHintTextInTwelveHour": "12:00 AM", "timeHintTextInTwelveHour": "12:00 AM",
"timeHintTextInTwentyFourHour": "12:00" "timeHintTextInTwentyFourHour": "12:00"
} }
},
"sideBar": {
"openSidebar": "Open sidebar",
"closeSidebar": "Close sidebar"
} }
} }

View File

@ -141,5 +141,9 @@
"lightLabel": "Mode clair", "lightLabel": "Mode clair",
"darkLabel": "Mode sombre" "darkLabel": "Mode sombre"
} }
},
"sideBar": {
"openSidebar": "Open sidebar",
"closeSidebar": "Close sidebar"
} }
} }

View File

@ -142,6 +142,10 @@
"darkLabel": "Mode sombre" "darkLabel": "Mode sombre"
} }
}, },
"sideBar": {
"openSidebar": "Open sidebar",
"closeSidebar": "Close sidebar"
},
"grid": { "grid": {
"settings": { "settings": {
"filter": "Filtrer", "filter": "Filtrer",

View File

@ -141,5 +141,9 @@
"lightLabel": "Világos mód", "lightLabel": "Világos mód",
"darkLabel": "Éjjeli mód" "darkLabel": "Éjjeli mód"
} }
},
"sideBar": {
"openSidebar": "Open sidebar",
"closeSidebar": "Close sidebar"
} }
} }

View File

@ -214,5 +214,9 @@
"timeHintTextInTwelveHour": "12:00 AM", "timeHintTextInTwelveHour": "12:00 AM",
"timeHintTextInTwentyFourHour": "12:00" "timeHintTextInTwentyFourHour": "12:00"
} }
},
"sideBar": {
"openSidebar": "Open sidebar",
"closeSidebar": "Close sidebar"
} }
} }

View File

@ -147,5 +147,9 @@
}, },
"document":{ "document":{
"menuName":"Documento" "menuName":"Documento"
},
"sideBar": {
"openSidebar": "Open sidebar",
"closeSidebar": "Close sidebar"
} }
} }

View File

@ -195,5 +195,9 @@
"pannelTitle": "選択候補を検索 または 作成する", "pannelTitle": "選択候補を検索 または 作成する",
"searchOption": "選択候補を検索" "searchOption": "選択候補を検索"
} }
},
"sideBar": {
"openSidebar": "Open sidebar",
"closeSidebar": "Close sidebar"
} }
} }

View File

@ -141,5 +141,9 @@
"lightLabel": "Tryb Jasny", "lightLabel": "Tryb Jasny",
"darkLabel": "Tryb Ciemny" "darkLabel": "Tryb Ciemny"
} }
},
"sideBar": {
"openSidebar": "Open sidebar",
"closeSidebar": "Close sidebar"
} }
} }

View File

@ -141,6 +141,10 @@
"lightLabel": "Modo Claro", "lightLabel": "Modo Claro",
"darkLabel": "Modo Escuro" "darkLabel": "Modo Escuro"
} }
},
"sideBar": {
"openSidebar": "Open sidebar",
"closeSidebar": "Close sidebar"
} }
} }

View File

@ -141,6 +141,10 @@
"lightLabel": "Modo Claro", "lightLabel": "Modo Claro",
"darkLabel": "Modo Escuro" "darkLabel": "Modo Escuro"
} }
},
"sideBar": {
"openSidebar": "Open sidebar",
"closeSidebar": "Close sidebar"
} }
} }

View File

@ -203,6 +203,10 @@
"timeHintTextInTwelveHour": "12:00 AM", "timeHintTextInTwelveHour": "12:00 AM",
"timeHintTextInTwentyFourHour": "12:00" "timeHintTextInTwentyFourHour": "12:00"
} }
},
"sideBar": {
"openSidebar": "Open sidebar",
"closeSidebar": "Close sidebar"
} }
} }

View File

@ -141,5 +141,9 @@
"lightLabel": "Aydınlık Mod", "lightLabel": "Aydınlık Mod",
"darkLabel": "Karanlık Mod" "darkLabel": "Karanlık Mod"
} }
},
"sideBar": {
"openSidebar": "Open sidebar",
"closeSidebar": "Close sidebar"
} }
} }

View File

@ -93,8 +93,14 @@
"highlight": "高亮" "highlight": "高亮"
}, },
"tooltip": { "tooltip": {
"lightMode": "切换到灯光模式", "lightMode": "切换到亮色模式",
"darkMode": "切换到暗模式" "darkMode": "切换到暗色模式"
},
"notifications": {
"export": {
"markdown": "导出笔记为Markdown文档",
"path": "Documents/flowy"
}
}, },
"contactsPage": { "contactsPage": {
"title": "联系人", "title": "联系人",
@ -135,11 +141,82 @@
"menu": { "menu": {
"appearance": "外观", "appearance": "外观",
"language": "语言", "language": "语言",
"user": "用户",
"open": "打开设置" "open": "打开设置"
}, },
"appearance": { "appearance": {
"lightLabel": "日间模式", "lightLabel": "日间模式",
"darkLabel": "夜间模式" "darkLabel": "夜间模式"
} }
},
"sideBar": {
"openSidebar": "打开侧边栏",
"closeSidebar": "关闭侧边栏"
},
"grid": {
"settings": {
"filter": "过滤器",
"sortBy": "排序",
"Properties": "属性"
},
"field": {
"hide": "隐藏",
"insertLeft": "左侧插入",
"insertRight": "右侧插入",
"duplicate": "拷贝",
"delete": "删除",
"textFieldName": "文本",
"checkboxFieldName": "勾选框",
"dateFieldName": "日期",
"numberFieldName": "数字",
"singleSelectFieldName": "单项选择器",
"multiSelectFieldName": "多项选择器",
"urlFieldName": "链接",
"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": "搜索标签"
},
"menuName": "网格"
},
"document": {
"menuName": "文档",
"date": {
"timeHintTextInTwelveHour": "12:00 AM",
"timeHintTextInTwentyFourHour": "12:00"
}
} }
} }

View File

@ -214,5 +214,9 @@
"timeHintTextInTwelveHour": "12:00 AM", "timeHintTextInTwelveHour": "12:00 AM",
"timeHintTextInTwentyFourHour": "12:00" "timeHintTextInTwentyFourHour": "12:00"
} }
},
"sideBar": {
"openSidebar": "Open sidebar",
"closeSidebar": "Close sidebar"
} }
} }

View File

@ -31,10 +31,10 @@ class MoveWindowDetector extends StatefulWidget {
final Widget? child; final Widget? child;
@override @override
_MoveWindowDetectorState createState() => _MoveWindowDetectorState(); MoveWindowDetectorState createState() => MoveWindowDetectorState();
} }
class _MoveWindowDetectorState extends State<MoveWindowDetector> { class MoveWindowDetectorState extends State<MoveWindowDetector> {
double winX = 0; double winX = 0;
double winY = 0; double winY = 0;
@ -59,7 +59,8 @@ class _MoveWindowDetectorState extends State<MoveWindowDetector> {
final double dy = windowPos[1]; final double dy = windowPos[1];
final deltaX = details.globalPosition.dx - winX; final deltaX = details.globalPosition.dx - winX;
final deltaY = details.globalPosition.dy - winY; final deltaY = details.globalPosition.dy - winY;
await CocoaWindowChannel.instance.setWindowPosition(Offset(dx + deltaX, dy - deltaY)); await CocoaWindowChannel.instance
.setWindowPosition(Offset(dx + deltaX, dy - deltaY));
}, },
child: widget.child, child: widget.child,
); );

View File

@ -1,6 +1,7 @@
import 'package:app_flowy/startup/startup.dart'; import 'package:app_flowy/startup/startup.dart';
import 'package:app_flowy/user/presentation/splash_screen.dart'; import 'package:app_flowy/user/presentation/splash_screen.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:hotkey_manager/hotkey_manager.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class FlowyApp implements EntryPoint { class FlowyApp implements EntryPoint {
@ -14,5 +15,8 @@ void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
await EasyLocalization.ensureInitialized(); await EasyLocalization.ensureInitialized();
WidgetsFlutterBinding.ensureInitialized();
await hotKeyManager.unregisterAll();
await FlowyRunner.run(FlowyApp()); await FlowyRunner.run(FlowyApp());
} }

View File

@ -20,19 +20,19 @@ import 'group_controller.dart';
part 'board_bloc.freezed.dart'; part 'board_bloc.freezed.dart';
class BoardBloc extends Bloc<BoardEvent, BoardState> { class BoardBloc extends Bloc<BoardEvent, BoardState> {
final BoardDataController _dataController; final BoardDataController _gridDataController;
late final AFBoardDataController afBoardDataController; late final AFBoardDataController boardController;
final MoveRowFFIService _rowService; final MoveRowFFIService _rowService;
LinkedHashMap<String, GroupController> groupControllers = LinkedHashMap.new(); LinkedHashMap<String, GroupController> groupControllers = LinkedHashMap();
GridFieldCache get fieldCache => _dataController.fieldCache; GridFieldCache get fieldCache => _gridDataController.fieldCache;
String get gridId => _dataController.gridId; String get gridId => _gridDataController.gridId;
BoardBloc({required ViewPB view}) BoardBloc({required ViewPB view})
: _rowService = MoveRowFFIService(gridId: view.id), : _rowService = MoveRowFFIService(gridId: view.id),
_dataController = BoardDataController(view: view), _gridDataController = BoardDataController(view: view),
super(BoardState.initial(view.id)) { super(BoardState.initial(view.id)) {
afBoardDataController = AFBoardDataController( boardController = AFBoardDataController(
onMoveColumn: ( onMoveColumn: (
fromColumnId, fromColumnId,
fromIndex, fromIndex,
@ -69,31 +69,51 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
_startListening(); _startListening();
await _loadGrid(emit); await _loadGrid(emit);
}, },
createRow: (groupId) async { createBottomRow: (groupId) async {
final result = await _dataController.createBoardCard(groupId); final startRowId = groupControllers[groupId]?.lastRow()?.id;
final result = await _gridDataController.createBoardCard(
groupId,
startRowId: startRowId,
);
result.fold( result.fold(
(rowPB) { (_) {},
emit(state.copyWith(editingRow: some(rowPB)));
},
(err) => Log.error(err), (err) => Log.error(err),
); );
}, },
createHeaderRow: (String groupId) async {
final result = await _gridDataController.createBoardCard(groupId);
result.fold(
(_) {},
(err) => Log.error(err),
);
},
didCreateRow: (String groupId, RowPB row, int? index) {
emit(state.copyWith(
editingRow: Some(BoardEditingRow(
columnId: groupId,
row: row,
index: index,
)),
));
},
endEditRow: (rowId) { endEditRow: (rowId) {
assert(state.editingRow.isSome()); assert(state.editingRow.isSome());
state.editingRow.fold(() => null, (row) { state.editingRow.fold(() => null, (editingRow) {
assert(row.id == rowId); assert(editingRow.row.id == rowId);
emit(state.copyWith(editingRow: none())); emit(state.copyWith(editingRow: none()));
}); });
}, },
didReceiveGridUpdate: (GridPB grid) { didReceiveGridUpdate: (GridPB grid) {
emit(state.copyWith(grid: Some(grid))); emit(state.copyWith(grid: Some(grid)));
}, },
didReceiveRows: (List<RowInfo> rowInfos) {
emit(state.copyWith(rowInfos: rowInfos));
},
didReceiveError: (FlowyError error) { didReceiveError: (FlowyError error) {
emit(state.copyWith(noneOrError: some(error))); emit(state.copyWith(noneOrError: some(error)));
}, },
didReceiveGroups: (List<GroupPB> groups) {
emit(state.copyWith(
groupIds: groups.map((group) => group.groupId).toList(),
));
},
); );
}, },
); );
@ -126,7 +146,7 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
@override @override
Future<void> close() async { Future<void> close() async {
await _dataController.dispose(); await _gridDataController.dispose();
for (final controller in groupControllers.values) { for (final controller in groupControllers.values) {
controller.dispose(); controller.dispose();
} }
@ -135,7 +155,12 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
void initializeGroups(List<GroupPB> groups) { void initializeGroups(List<GroupPB> groups) {
for (final group in groups) { for (final group in groups) {
final delegate = GroupControllerDelegateImpl(afBoardDataController); final delegate = GroupControllerDelegateImpl(
controller: boardController,
onNewColumnItem: (groupId, row, index) {
add(BoardEvent.didCreateRow(groupId, row, index));
},
);
final controller = GroupController( final controller = GroupController(
gridId: state.gridId, gridId: state.gridId,
group: group, group: group,
@ -147,12 +172,12 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
} }
GridRowCache? getRowCache(String blockId) { GridRowCache? getRowCache(String blockId) {
final GridBlockCache? blockCache = _dataController.blocks[blockId]; final GridBlockCache? blockCache = _gridDataController.blocks[blockId];
return blockCache?.rowCache; return blockCache?.rowCache;
} }
void _startListening() { void _startListening() {
_dataController.addListener( _gridDataController.addListener(
onGridChanged: (grid) { onGridChanged: (grid) {
if (!isClosed) { if (!isClosed) {
add(BoardEvent.didReceiveGridUpdate(grid)); add(BoardEvent.didReceiveGridUpdate(grid));
@ -162,17 +187,31 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
List<AFBoardColumnData> columns = groups.map((group) { List<AFBoardColumnData> columns = groups.map((group) {
return AFBoardColumnData( return AFBoardColumnData(
id: group.groupId, id: group.groupId,
desc: group.desc, name: group.desc,
items: _buildRows(group.rows), items: _buildRows(group),
customData: group, customData: group,
); );
}).toList(); }).toList();
afBoardDataController.addColumns(columns); boardController.addColumns(columns);
initializeGroups(groups); initializeGroups(groups);
add(BoardEvent.didReceiveGroups(groups));
}, },
onRowsChanged: (List<RowInfo> rowInfos, RowsChangedReason reason) { onDeletedGroup: (groupIds) {
add(BoardEvent.didReceiveRows(rowInfos)); //
},
onInsertedGroup: (insertedGroups) {
//
},
onUpdatedGroup: (updatedGroups) {
//
for (final group in updatedGroups) {
final columnController =
boardController.getColumnController(group.groupId);
if (columnController != null) {
columnController.updateColumnName(group.desc);
}
}
}, },
onError: (err) { onError: (err) {
Log.error(err); Log.error(err);
@ -180,16 +219,19 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
); );
} }
List<AFColumnItem> _buildRows(List<RowPB> rows) { List<AFColumnItem> _buildRows(GroupPB group) {
final items = rows.map((row) { final items = group.rows.map((row) {
return BoardColumnItem(row: row); return BoardColumnItem(
row: row,
fieldId: group.fieldId,
);
}).toList(); }).toList();
return <AFColumnItem>[...items]; return <AFColumnItem>[...items];
} }
Future<void> _loadGrid(Emitter<BoardState> emit) async { Future<void> _loadGrid(Emitter<BoardState> emit) async {
final result = await _dataController.loadData(); final result = await _gridDataController.loadData();
result.fold( result.fold(
(grid) => emit( (grid) => emit(
state.copyWith(loadingState: GridLoadingState.finish(left(unit))), state.copyWith(loadingState: GridLoadingState.finish(left(unit))),
@ -203,15 +245,21 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
@freezed @freezed
class BoardEvent with _$BoardEvent { class BoardEvent with _$BoardEvent {
const factory BoardEvent.initial() = InitialGrid; const factory BoardEvent.initial() = _InitialBoard;
const factory BoardEvent.createRow(String groupId) = _CreateRow; const factory BoardEvent.createBottomRow(String groupId) = _CreateBottomRow;
const factory BoardEvent.createHeaderRow(String groupId) = _CreateHeaderRow;
const factory BoardEvent.didCreateRow(
String groupId,
RowPB row,
int? index,
) = _DidCreateRow;
const factory BoardEvent.endEditRow(String rowId) = _EndEditRow; const factory BoardEvent.endEditRow(String rowId) = _EndEditRow;
const factory BoardEvent.didReceiveError(FlowyError error) = _DidReceiveError; const factory BoardEvent.didReceiveError(FlowyError error) = _DidReceiveError;
const factory BoardEvent.didReceiveRows(List<RowInfo> rowInfos) =
_DidReceiveRows;
const factory BoardEvent.didReceiveGridUpdate( const factory BoardEvent.didReceiveGridUpdate(
GridPB grid, GridPB grid,
) = _DidReceiveGridUpdate; ) = _DidReceiveGridUpdate;
const factory BoardEvent.didReceiveGroups(List<GroupPB> groups) =
_DidReceiveGroups;
} }
@freezed @freezed
@ -219,16 +267,16 @@ class BoardState with _$BoardState {
const factory BoardState({ const factory BoardState({
required String gridId, required String gridId,
required Option<GridPB> grid, required Option<GridPB> grid,
required Option<RowPB> editingRow, required List<String> groupIds,
required List<RowInfo> rowInfos, required Option<BoardEditingRow> editingRow,
required GridLoadingState loadingState, required GridLoadingState loadingState,
required Option<FlowyError> noneOrError, required Option<FlowyError> noneOrError,
}) = _BoardState; }) = _BoardState;
factory BoardState.initial(String gridId) => BoardState( factory BoardState.initial(String gridId) => BoardState(
rowInfos: [],
grid: none(), grid: none(),
gridId: gridId, gridId: gridId,
groupIds: [],
editingRow: none(), editingRow: none(),
noneOrError: none(), noneOrError: none(),
loadingState: const _Loading(), loadingState: const _Loading(),
@ -268,39 +316,84 @@ class GridFieldEquatable extends Equatable {
class BoardColumnItem extends AFColumnItem { class BoardColumnItem extends AFColumnItem {
final RowPB row; final RowPB row;
BoardColumnItem({required this.row}); final String fieldId;
final bool requestFocus;
BoardColumnItem({
required this.row,
required this.fieldId,
this.requestFocus = false,
});
@override @override
String get id => row.id; String get id => row.id;
} }
class CreateCardItem extends AFColumnItem {
@override
String get id => '$CreateCardItem';
}
class GroupControllerDelegateImpl extends GroupControllerDelegate { class GroupControllerDelegateImpl extends GroupControllerDelegate {
final AFBoardDataController controller; final AFBoardDataController controller;
final void Function(String, RowPB, int?) onNewColumnItem;
GroupControllerDelegateImpl(this.controller); GroupControllerDelegateImpl({
required this.controller,
required this.onNewColumnItem,
});
@override @override
void insertRow(String groupId, RowPB row, int? index) { void insertRow(GroupPB group, RowPB row, int? index) {
final item = BoardColumnItem(row: row);
if (index != null) { if (index != null) {
controller.insertColumnItem(groupId, index, item); final item = BoardColumnItem(row: row, fieldId: group.fieldId);
controller.insertColumnItem(group.groupId, index, item);
} else { } else {
controller.addColumnItem(groupId, item); final item = BoardColumnItem(
row: row,
fieldId: group.fieldId,
);
controller.addColumnItem(group.groupId, item);
} }
} }
@override @override
void removeRow(String groupId, String rowId) { void removeRow(GroupPB group, String rowId) {
controller.removeColumnItem(groupId, rowId); controller.removeColumnItem(group.groupId, rowId);
} }
@override @override
void updateRow(String groupId, RowPB row) { void updateRow(GroupPB group, RowPB row) {
// controller.updateColumnItem(
group.groupId,
BoardColumnItem(
row: row,
fieldId: group.fieldId,
),
);
}
@override
void addNewRow(GroupPB group, RowPB row, int? index) {
final item = BoardColumnItem(
row: row,
fieldId: group.fieldId,
requestFocus: true,
);
if (index != null) {
controller.insertColumnItem(group.groupId, index, item);
} else {
controller.addColumnItem(group.groupId, item);
}
onNewColumnItem(group.groupId, row, index);
} }
} }
class BoardEditingRow {
String columnId;
RowPB row;
int? index;
BoardEditingRow({
required this.columnId,
required this.row,
required this.index,
});
}

View File

@ -10,9 +10,15 @@ import 'dart:async';
import 'package:dartz/dartz.dart'; import 'package:dartz/dartz.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/protobuf.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/protobuf.dart';
import 'board_listener.dart';
typedef OnFieldsChanged = void Function(UnmodifiableListView<FieldPB>); typedef OnFieldsChanged = void Function(UnmodifiableListView<FieldPB>);
typedef OnGridChanged = void Function(GridPB); typedef OnGridChanged = void Function(GridPB);
typedef DidLoadGroups = void Function(List<GroupPB>); typedef DidLoadGroups = void Function(List<GroupPB>);
typedef OnUpdatedGroup = void Function(List<GroupPB>);
typedef OnDeletedGroup = void Function(List<String>);
typedef OnInsertedGroup = void Function(List<InsertedGroupPB>);
typedef OnRowsChanged = void Function( typedef OnRowsChanged = void Function(
List<RowInfo>, List<RowInfo>,
RowsChangedReason, RowsChangedReason,
@ -23,6 +29,7 @@ class BoardDataController {
final String gridId; final String gridId;
final GridFFIService _gridFFIService; final GridFFIService _gridFFIService;
final GridFieldCache fieldCache; final GridFieldCache fieldCache;
final BoardListener _listener;
// key: the block id // key: the block id
final LinkedHashMap<String, GridBlockCache> _blocks; final LinkedHashMap<String, GridBlockCache> _blocks;
@ -44,16 +51,21 @@ class BoardDataController {
BoardDataController({required ViewPB view}) BoardDataController({required ViewPB view})
: gridId = view.id, : gridId = view.id,
_blocks = LinkedHashMap.new(), _listener = BoardListener(view.id),
// ignore: prefer_collection_literals
_blocks = LinkedHashMap(),
_gridFFIService = GridFFIService(gridId: view.id), _gridFFIService = GridFFIService(gridId: view.id),
fieldCache = GridFieldCache(gridId: view.id); fieldCache = GridFieldCache(gridId: view.id);
void addListener({ void addListener({
OnGridChanged? onGridChanged, required OnGridChanged onGridChanged,
OnFieldsChanged? onFieldsChanged, OnFieldsChanged? onFieldsChanged,
DidLoadGroups? didLoadGroups, required DidLoadGroups didLoadGroups,
OnRowsChanged? onRowsChanged, OnRowsChanged? onRowsChanged,
OnError? onError, required OnUpdatedGroup onUpdatedGroup,
required OnDeletedGroup onDeletedGroup,
required OnInsertedGroup onInsertedGroup,
required OnError? onError,
}) { }) {
_onGridChanged = onGridChanged; _onGridChanged = onGridChanged;
_onFieldsChanged = onFieldsChanged; _onFieldsChanged = onFieldsChanged;
@ -64,6 +76,25 @@ class BoardDataController {
fieldCache.addListener(onFields: (fields) { fieldCache.addListener(onFields: (fields) {
_onFieldsChanged?.call(UnmodifiableListView(fields)); _onFieldsChanged?.call(UnmodifiableListView(fields));
}); });
_listener.start(onBoardChanged: (result) {
result.fold(
(changeset) {
if (changeset.updateGroups.isNotEmpty) {
onUpdatedGroup.call(changeset.updateGroups);
}
if (changeset.insertedGroups.isNotEmpty) {
onInsertedGroup.call(changeset.insertedGroups);
}
if (changeset.deletedGroups.isNotEmpty) {
onDeletedGroup.call(changeset.deletedGroups);
}
},
(e) => _onError?.call(e),
);
});
} }
Future<Either<Unit, FlowyError>> loadData() async { Future<Either<Unit, FlowyError>> loadData() async {
@ -88,8 +119,9 @@ class BoardDataController {
); );
} }
Future<Either<RowPB, FlowyError>> createBoardCard(String groupId) { Future<Either<RowPB, FlowyError>> createBoardCard(String groupId,
return _gridFFIService.createBoardCard(groupId); {String? startRowId}) {
return _gridFFIService.createBoardCard(groupId, startRowId);
} }
Future<void> dispose() async { Future<void> dispose() async {

View File

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

View File

@ -79,7 +79,7 @@ class BoardDateCellState with _$BoardDateCellState {
String _dateStrFromCellData(DateCellDataPB? cellData) { String _dateStrFromCellData(DateCellDataPB? cellData) {
String dateStr = ""; String dateStr = "";
if (cellData != null) { if (cellData != null) {
dateStr = cellData.date + " " + cellData.time; dateStr = "${cellData.date} ${cellData.time}";
} }
return dateStr; return dateStr;
} }

View File

@ -68,7 +68,6 @@ class BoardSelectOptionCellState with _$BoardSelectOptionCellState {
factory BoardSelectOptionCellState.initial( factory BoardSelectOptionCellState.initial(
GridSelectOptionCellController context) { GridSelectOptionCellController context) {
final data = context.getCellData(); final data = context.getCellData();
return BoardSelectOptionCellState( return BoardSelectOptionCellState(
selectedOptions: data?.selectOptions ?? [], selectedOptions: data?.selectOptions ?? [],
); );

View File

@ -1,4 +1,5 @@
import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart'; import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
import 'dart:async'; import 'dart:async';
@ -20,6 +21,15 @@ class BoardTextCellBloc extends Bloc<BoardTextCellEvent, BoardTextCellState> {
didReceiveCellUpdate: (content) { didReceiveCellUpdate: (content) {
emit(state.copyWith(content: content)); emit(state.copyWith(content: content));
}, },
updateText: (text) {
if (text != state.content) {
cellController.saveCellData(text);
emit(state.copyWith(content: text));
}
},
enableEdit: (bool enabled) {
emit(state.copyWith(enableEdit: enabled));
},
); );
}, },
); );
@ -49,6 +59,8 @@ class BoardTextCellBloc extends Bloc<BoardTextCellEvent, BoardTextCellState> {
@freezed @freezed
class BoardTextCellEvent with _$BoardTextCellEvent { class BoardTextCellEvent with _$BoardTextCellEvent {
const factory BoardTextCellEvent.initial() = _InitialCell; const factory BoardTextCellEvent.initial() = _InitialCell;
const factory BoardTextCellEvent.updateText(String text) = _UpdateContent;
const factory BoardTextCellEvent.enableEdit(bool enabled) = _EnableEdit;
const factory BoardTextCellEvent.didReceiveCellUpdate(String cellContent) = const factory BoardTextCellEvent.didReceiveCellUpdate(String cellContent) =
_DidReceiveCellUpdate; _DidReceiveCellUpdate;
} }
@ -57,10 +69,12 @@ class BoardTextCellEvent with _$BoardTextCellEvent {
class BoardTextCellState with _$BoardTextCellState { class BoardTextCellState with _$BoardTextCellState {
const factory BoardTextCellState({ const factory BoardTextCellState({
required String content, required String content,
required bool enableEdit,
}) = _BoardTextCellState; }) = _BoardTextCellState;
factory BoardTextCellState.initial(GridCellController context) => factory BoardTextCellState.initial(GridCellController context) =>
BoardTextCellState( BoardTextCellState(
content: context.getCellData() ?? "", content: context.getCellData() ?? "",
enableEdit: false,
); );
} }

View File

@ -4,7 +4,6 @@ import 'package:app_flowy/plugins/grid/application/row/row_cache.dart';
import 'package:app_flowy/plugins/grid/application/row/row_service.dart'; import 'package:app_flowy/plugins/grid/application/row/row_service.dart';
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/block_entities.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/block_entities.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
import 'dart:async'; import 'dart:async';
@ -14,10 +13,12 @@ import 'card_data_controller.dart';
part 'card_bloc.freezed.dart'; part 'card_bloc.freezed.dart';
class BoardCardBloc extends Bloc<BoardCardEvent, BoardCardState> { class BoardCardBloc extends Bloc<BoardCardEvent, BoardCardState> {
final String fieldId;
final RowFFIService _rowService; final RowFFIService _rowService;
final CardDataController _dataController; final CardDataController _dataController;
BoardCardBloc({ BoardCardBloc({
required this.fieldId,
required String gridId, required String gridId,
required CardDataController dataController, required CardDataController dataController,
}) : _rowService = RowFFIService( }) : _rowService = RowFFIService(
@ -25,22 +26,22 @@ class BoardCardBloc extends Bloc<BoardCardEvent, BoardCardState> {
blockId: dataController.rowPB.blockId, blockId: dataController.rowPB.blockId,
), ),
_dataController = dataController, _dataController = dataController,
super(BoardCardState.initial( super(
dataController.rowPB, dataController.loadData())) { BoardCardState.initial(
dataController.rowPB,
_makeCells(fieldId, dataController.loadData()),
),
) {
on<BoardCardEvent>( on<BoardCardEvent>(
(event, emit) async { (event, emit) async {
await event.map( await event.when(
initial: (_InitialRow value) async { initial: () async {
await _startListening(); await _startListening();
}, },
didReceiveCells: (_DidReceiveCells value) async { didReceiveCells: (cells, reason) async {
final cells = value.gridCellMap.values
.map((e) => GridCellEquatable(e.field))
.toList();
emit(state.copyWith( emit(state.copyWith(
gridCellMap: value.gridCellMap, cells: cells,
cells: UnmodifiableListView(cells), changeReason: reason,
changeReason: value.reason,
)); ));
}, },
); );
@ -58,7 +59,7 @@ class BoardCardBloc extends Bloc<BoardCardEvent, BoardCardState> {
return RowInfo( return RowInfo(
gridId: _rowService.gridId, gridId: _rowService.gridId,
fields: UnmodifiableListView( fields: UnmodifiableListView(
state.cells.map((cell) => cell._field).toList(), state.cells.map((cell) => cell.identifier.field).toList(),
), ),
rowPB: state.rowPB, rowPB: state.rowPB,
); );
@ -66,8 +67,9 @@ class BoardCardBloc extends Bloc<BoardCardEvent, BoardCardState> {
Future<void> _startListening() async { Future<void> _startListening() async {
_dataController.addListener( _dataController.addListener(
onRowChanged: (cells, reason) { onRowChanged: (cellMap, reason) {
if (!isClosed) { if (!isClosed) {
final cells = _makeCells(fieldId, cellMap);
add(BoardCardEvent.didReceiveCells(cells, reason)); add(BoardCardEvent.didReceiveCells(cells, reason));
} }
}, },
@ -75,42 +77,52 @@ class BoardCardBloc extends Bloc<BoardCardEvent, BoardCardState> {
} }
} }
UnmodifiableListView<BoardCellEquatable> _makeCells(
String fieldId, GridCellMap originalCellMap) {
List<BoardCellEquatable> cells = [];
for (final entry in originalCellMap.entries) {
if (entry.value.fieldId != fieldId) {
cells.add(BoardCellEquatable(entry.value));
}
}
return UnmodifiableListView(cells);
}
@freezed @freezed
class BoardCardEvent with _$BoardCardEvent { class BoardCardEvent with _$BoardCardEvent {
const factory BoardCardEvent.initial() = _InitialRow; const factory BoardCardEvent.initial() = _InitialRow;
const factory BoardCardEvent.didReceiveCells( const factory BoardCardEvent.didReceiveCells(
GridCellMap gridCellMap, RowsChangedReason reason) = _DidReceiveCells; UnmodifiableListView<BoardCellEquatable> cells,
RowsChangedReason reason,
) = _DidReceiveCells;
} }
@freezed @freezed
class BoardCardState with _$BoardCardState { class BoardCardState with _$BoardCardState {
const factory BoardCardState({ const factory BoardCardState({
required RowPB rowPB, required RowPB rowPB,
required GridCellMap gridCellMap, required UnmodifiableListView<BoardCellEquatable> cells,
required UnmodifiableListView<GridCellEquatable> cells,
RowsChangedReason? changeReason, RowsChangedReason? changeReason,
}) = _BoardCardState; }) = _BoardCardState;
factory BoardCardState.initial(RowPB rowPB, GridCellMap cellDataMap) => factory BoardCardState.initial(
RowPB rowPB, UnmodifiableListView<BoardCellEquatable> cells) =>
BoardCardState( BoardCardState(
rowPB: rowPB, rowPB: rowPB,
gridCellMap: cellDataMap, cells: cells,
cells: UnmodifiableListView(
cellDataMap.values.map((e) => GridCellEquatable(e.field)).toList(),
),
); );
} }
class GridCellEquatable extends Equatable { class BoardCellEquatable extends Equatable {
final FieldPB _field; final GridCellIdentifier identifier;
const GridCellEquatable(FieldPB field) : _field = field; const BoardCellEquatable(this.identifier);
@override @override
List<Object?> get props => [ List<Object?> get props => [
_field.id, identifier.field.id,
_field.fieldType, identifier.field.fieldType,
_field.visibility, identifier.field.visibility,
_field.width, identifier.field.width,
]; ];
} }

View File

@ -1,15 +1,15 @@
import 'package:flowy_sdk/log.dart'; import 'package:flowy_sdk/log.dart';
import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/protobuf.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/protobuf.dart';
import 'group_listener.dart'; import 'group_listener.dart';
typedef OnGroupError = void Function(FlowyError); typedef OnGroupError = void Function(FlowyError);
abstract class GroupControllerDelegate { abstract class GroupControllerDelegate {
void removeRow(String groupId, String rowId); void removeRow(GroupPB group, String rowId);
void insertRow(String groupId, RowPB row, int? index); void insertRow(GroupPB group, RowPB row, int? index);
void updateRow(String groupId, RowPB row); void updateRow(GroupPB group, RowPB row);
void addNewRow(GroupPB group, RowPB row, int? index);
} }
class GroupController { class GroupController {
@ -31,13 +31,22 @@ class GroupController {
} }
} }
RowPB? lastRow() {
if (group.rows.isEmpty) return null;
return group.rows.last;
}
void startListening() { void startListening() {
_listener.start(onGroupChanged: (result) { _listener.start(onGroupChanged: (result) {
result.fold( result.fold(
(GroupRowsChangesetPB changeset) { (GroupChangesetPB changeset) {
for (final deletedRow in changeset.deletedRows) {
group.rows.removeWhere((rowPB) => rowPB.id == deletedRow);
delegate.removeRow(group, deletedRow);
}
for (final insertedRow in changeset.insertedRows) { for (final insertedRow in changeset.insertedRows) {
final index = insertedRow.hasIndex() ? insertedRow.index : null; final index = insertedRow.hasIndex() ? insertedRow.index : null;
if (insertedRow.hasIndex() && if (insertedRow.hasIndex() &&
group.rows.length > insertedRow.index) { group.rows.length > insertedRow.index) {
group.rows.insert(insertedRow.index, insertedRow.row); group.rows.insert(insertedRow.index, insertedRow.row);
@ -45,16 +54,11 @@ class GroupController {
group.rows.add(insertedRow.row); group.rows.add(insertedRow.row);
} }
delegate.insertRow( if (insertedRow.isNew) {
group.groupId, delegate.addNewRow(group, insertedRow.row, index);
insertedRow.row, } else {
index, delegate.insertRow(group, insertedRow.row, index);
);
} }
for (final deletedRow in changeset.deletedRows) {
group.rows.removeWhere((rowPB) => rowPB.id == deletedRow);
delegate.removeRow(group.groupId, deletedRow);
} }
for (final updatedRow in changeset.updatedRows) { for (final updatedRow in changeset.updatedRows) {
@ -66,7 +70,7 @@ class GroupController {
group.rows[index] = updatedRow; group.rows[index] = updatedRow;
} }
delegate.updateRow(group.groupId, updatedRow); delegate.updateRow(group, updatedRow);
} }
}, },
(err) => Log.error(err), (err) => Log.error(err),
@ -74,6 +78,29 @@ class GroupController {
}); });
} }
// GroupChangesetPB _transformChangeset(GroupChangesetPB changeset) {
// final insertedRows = changeset.insertedRows
// .where(
// (delete) => !changeset.deletedRows.contains(delete.row.id),
// )
// .toList();
// final deletedRows = changeset.deletedRows
// .where((deletedRowId) =>
// changeset.insertedRows
// .indexWhere((insert) => insert.row.id == deletedRowId) ==
// -1)
// .toList();
// return changeset.rebuild((rebuildChangeset) {
// rebuildChangeset.insertedRows.clear();
// rebuildChangeset.insertedRows.addAll(insertedRows);
// rebuildChangeset.deletedRows.clear();
// rebuildChangeset.deletedRows.addAll(deletedRows);
// });
// }
Future<void> dispose() async { Future<void> dispose() async {
_listener.stop(); _listener.stop();
} }

View File

@ -8,7 +8,7 @@ import 'package:flowy_sdk/protobuf/flowy-grid/group.pb.dart';
import 'package:dartz/dartz.dart'; import 'package:dartz/dartz.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/group_changeset.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/group_changeset.pb.dart';
typedef UpdateGroupNotifiedValue = Either<GroupRowsChangesetPB, FlowyError>; typedef UpdateGroupNotifiedValue = Either<GroupChangesetPB, FlowyError>;
class GroupListener { class GroupListener {
final GroupPB group; final GroupPB group;
@ -34,7 +34,7 @@ class GroupListener {
case GridNotification.DidUpdateGroup: case GridNotification.DidUpdateGroup:
result.fold( result.fold(
(payload) => _groupNotifier?.value = (payload) => _groupNotifier?.value =
left(GroupRowsChangesetPB.fromBuffer(payload)), left(GroupChangesetPB.fromBuffer(payload)),
(error) => _groupNotifier?.value = right(error), (error) => _groupNotifier?.value = right(error),
); );
break; break;

View File

@ -0,0 +1,46 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'dart:async';
import 'package:dartz/dartz.dart';
part 'board_setting_bloc.freezed.dart';
class BoardSettingBloc extends Bloc<BoardSettingEvent, BoardSettingState> {
final String gridId;
BoardSettingBloc({required this.gridId})
: super(BoardSettingState.initial()) {
on<BoardSettingEvent>(
(event, emit) async {
event.when(performAction: (action) {
emit(state.copyWith(selectedAction: Some(action)));
});
},
);
}
@override
Future<void> close() async {
return super.close();
}
}
@freezed
class BoardSettingEvent with _$BoardSettingEvent {
const factory BoardSettingEvent.performAction(BoardSettingAction action) =
_PerformAction;
}
@freezed
class BoardSettingState with _$BoardSettingState {
const factory BoardSettingState({
required Option<BoardSettingAction> selectedAction,
}) = _BoardSettingState;
factory BoardSettingState.initial() => BoardSettingState(
selectedAction: none(),
);
}
enum BoardSettingAction {
properties,
}

View File

@ -31,7 +31,7 @@ class BoardPluginBuilder implements PluginBuilder {
class BoardPluginConfig implements PluginConfig { class BoardPluginConfig implements PluginConfig {
@override @override
bool get creatable => false; bool get creatable => true;
} }
class BoardPlugin extends Plugin { class BoardPlugin extends Plugin {

View File

@ -2,6 +2,7 @@
import 'dart:collection'; import 'dart:collection';
import 'package:app_flowy/generated/locale_keys.g.dart';
import 'package:app_flowy/plugins/board/application/card/card_data_controller.dart'; import 'package:app_flowy/plugins/board/application/card/card_data_controller.dart';
import 'package:app_flowy/plugins/grid/application/row/row_cache.dart'; import 'package:app_flowy/plugins/grid/application/row/row_cache.dart';
import 'package:app_flowy/plugins/grid/application/field/field_cache.dart'; import 'package:app_flowy/plugins/grid/application/field/field_cache.dart';
@ -9,16 +10,22 @@ import 'package:app_flowy/plugins/grid/application/row/row_data_controller.dart'
import 'package:app_flowy/plugins/grid/presentation/widgets/cell/cell_builder.dart'; import 'package:app_flowy/plugins/grid/presentation/widgets/cell/cell_builder.dart';
import 'package:app_flowy/plugins/grid/presentation/widgets/row/row_detail.dart'; import 'package:app_flowy/plugins/grid/presentation/widgets/row/row_detail.dart';
import 'package:appflowy_board/appflowy_board.dart'; import 'package:appflowy_board/appflowy_board.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/image.dart';
import 'package:flowy_infra/theme.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_infra_ui/flowy_infra_ui_web.dart'; import 'package:flowy_infra_ui/flowy_infra_ui_web.dart';
import 'package:flowy_infra_ui/widget/error_page.dart'; import 'package:flowy_infra_ui/widget/error_page.dart';
import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/block_entities.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/block_entities.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/group.pbserver.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import '../../grid/application/row/row_cache.dart'; import '../../grid/application/row/row_cache.dart';
import '../application/board_bloc.dart'; import '../application/board_bloc.dart';
import 'card/card.dart'; import 'card/card.dart';
import 'card/card_cell_builder.dart'; import 'card/card_cell_builder.dart';
import 'toolbar/board_toolbar.dart';
class BoardPage extends StatelessWidget { class BoardPage extends StatelessWidget {
final ViewPB view; final ViewPB view;
@ -30,13 +37,15 @@ class BoardPage extends StatelessWidget {
create: (context) => create: (context) =>
BoardBloc(view: view)..add(const BoardEvent.initial()), BoardBloc(view: view)..add(const BoardEvent.initial()),
child: BlocBuilder<BoardBloc, BoardState>( child: BlocBuilder<BoardBloc, BoardState>(
buildWhen: (previous, current) =>
previous.loadingState != current.loadingState,
builder: (context, state) { builder: (context, state) {
return state.loadingState.map( return state.loadingState.map(
loading: (_) => loading: (_) =>
const Center(child: CircularProgressIndicator.adaptive()), const Center(child: CircularProgressIndicator.adaptive()),
finish: (result) { finish: (result) {
return result.successOrFail.fold( return result.successOrFail.fold(
(_) => BoardContent(), (_) => const BoardContent(),
(err) => FlowyErrorPage(err.toString()), (err) => FlowyErrorPage(err.toString()),
); );
}, },
@ -47,67 +56,172 @@ class BoardPage extends StatelessWidget {
} }
} }
class BoardContent extends StatelessWidget { class BoardContent extends StatefulWidget {
const BoardContent({Key? key}) : super(key: key);
@override
State<BoardContent> createState() => _BoardContentState();
}
class _BoardContentState extends State<BoardContent> {
late ScrollController scrollController;
late AFBoardScrollManager scrollManager;
final config = AFBoardConfig( final config = AFBoardConfig(
columnBackgroundColor: HexColor.fromHex('#F7F8FC'), columnBackgroundColor: HexColor.fromHex('#F7F8FC'),
); );
BoardContent({Key? key}) : super(key: key); @override
void initState() {
scrollController = ScrollController();
scrollManager = AFBoardScrollManager();
super.initState();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocBuilder<BoardBloc, BoardState>( return BlocListener<BoardBloc, BoardState>(
listener: (context, state) => _handleEditState(state, context),
child: BlocBuilder<BoardBloc, BoardState>(
buildWhen: (previous, current) =>
previous.groupIds.length != current.groupIds.length,
builder: (context, state) { builder: (context, state) {
final theme = context.read<AppTheme>();
return Container( return Container(
color: Colors.white, color: theme.surface,
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 20), padding: const EdgeInsets.symmetric(horizontal: 20),
child: Column(
children: [
const _ToolbarBlocAdaptor(),
Expanded(
child: AFBoard( child: AFBoard(
// key: UniqueKey(), scrollManager: scrollManager,
scrollController: ScrollController(), scrollController: scrollController,
dataController: context.read<BoardBloc>().afBoardDataController, dataController: context.read<BoardBloc>().boardController,
headerBuilder: _buildHeader, headerBuilder: _buildHeader,
footBuilder: _buildFooter, footBuilder: _buildFooter,
cardBuilder: (_, data) => _buildCard(context, data), cardBuilder: (_, column, columnItem) => _buildCard(
columnConstraints: const BoxConstraints.tightFor(width: 240), context,
column,
columnItem,
),
columnConstraints:
const BoxConstraints.tightFor(width: 300),
config: AFBoardConfig( config: AFBoardConfig(
columnBackgroundColor: HexColor.fromHex('#F7F8FC'), columnBackgroundColor: HexColor.fromHex('#F7F8FC'),
), ),
), ),
), ),
],
),
),
); );
}, },
),
);
}
void _handleEditState(BoardState state, BuildContext context) {
state.editingRow.fold(
() => null,
(editingRow) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (editingRow.index != null) {
context
.read<BoardBloc>()
.add(BoardEvent.endEditRow(editingRow.row.id));
} else {
scrollManager.scrollToBottom(editingRow.columnId, () {
context
.read<BoardBloc>()
.add(BoardEvent.endEditRow(editingRow.row.id));
});
}
});
},
); );
} }
Widget _buildHeader(BuildContext context, AFBoardColumnData columnData) { @override
void dispose() {
scrollController.dispose();
super.dispose();
}
Widget _buildHeader(
BuildContext context,
AFBoardColumnData columnData,
) {
return AppFlowyColumnHeader( return AppFlowyColumnHeader(
icon: const Icon(Icons.lightbulb_circle), title: Flexible(
title: Text(columnData.desc), fit: FlexFit.tight,
addIcon: const Icon(Icons.add, size: 20), child: FlowyText.medium(
moreIcon: const Icon(Icons.more_horiz, size: 20), columnData.headerData.columnName,
fontSize: 14,
overflow: TextOverflow.clip,
color: context.read<AppTheme>().textColor,
),
),
addIcon: SizedBox(
height: 20,
width: 20,
child: svgWidget(
"home/add",
color: context.read<AppTheme>().iconColor,
),
),
onAddButtonClick: () {
context.read<BoardBloc>().add(
BoardEvent.createHeaderRow(columnData.id),
);
},
height: 50, height: 50,
margin: config.columnItemPadding, margin: config.headerPadding,
); );
} }
Widget _buildFooter(BuildContext context, AFBoardColumnData columnData) { Widget _buildFooter(BuildContext context, AFBoardColumnData columnData) {
final group = columnData.customData as GroupPB;
if (group.isDefault) {
return const SizedBox();
} else {
return AppFlowyColumnFooter( return AppFlowyColumnFooter(
icon: const Icon(Icons.add, size: 20), icon: SizedBox(
title: const Text('New'), height: 20,
width: 20,
child: svgWidget(
"home/add",
color: context.read<AppTheme>().iconColor,
),
),
title: FlowyText.medium(
LocaleKeys.board_column_create_new_card.tr(),
fontSize: 14,
color: context.read<AppTheme>().textColor,
),
height: 50, height: 50,
margin: config.columnItemPadding, margin: config.footerPadding,
onAddButtonClick: () { onAddButtonClick: () {
context.read<BoardBloc>().add(BoardEvent.createRow(columnData.id)); context.read<BoardBloc>().add(
}); BoardEvent.createBottomRow(columnData.id),
);
},
);
}
} }
Widget _buildCard(BuildContext context, AFColumnItem item) { Widget _buildCard(
final rowPB = (item as BoardColumnItem).row; BuildContext context,
AFBoardColumnData column,
AFColumnItem columnItem,
) {
final boardColumnItem = columnItem as BoardColumnItem;
final rowPB = boardColumnItem.row;
final rowCache = context.read<BoardBloc>().getRowCache(rowPB.blockId); final rowCache = context.read<BoardBloc>().getRowCache(rowPB.blockId);
/// Return placeholder widget if the rowCache is null. /// Return placeholder widget if the rowCache is null.
if (rowCache == null) return SizedBox(key: ObjectKey(item)); if (rowCache == null) return SizedBox(key: ObjectKey(columnItem));
final fieldCache = context.read<BoardBloc>().fieldCache; final fieldCache = context.read<BoardBloc>().fieldCache;
final gridId = context.read<BoardBloc>().gridId; final gridId = context.read<BoardBloc>().gridId;
@ -118,21 +232,25 @@ class BoardContent extends StatelessWidget {
); );
final cellBuilder = BoardCellBuilder(cardController); final cellBuilder = BoardCellBuilder(cardController);
final isEditing = context.read<BoardBloc>().state.editingRow.fold( bool isEditing = false;
() => false, context.read<BoardBloc>().state.editingRow.fold(
(editingRow) => editingRow.id == rowPB.id, () => null,
(editingRow) {
isEditing = editingRow.row.id == columnItem.row.id;
},
); );
return AppFlowyColumnItemCard( return AppFlowyColumnItemCard(
key: ObjectKey(item), key: ValueKey(columnItem.id),
margin: config.cardPadding,
decoration: _makeBoxDecoration(context),
child: BoardCard( child: BoardCard(
gridId: gridId, gridId: gridId,
groupId: column.id,
fieldId: boardColumnItem.fieldId,
isEditing: isEditing, isEditing: isEditing,
cellBuilder: cellBuilder, cellBuilder: cellBuilder,
dataController: cardController, dataController: cardController,
onEditEditing: (rowId) {
context.read<BoardBloc>().add(BoardEvent.endEditRow(rowId));
},
openCard: (context) => _openCard( openCard: (context) => _openCard(
gridId, gridId,
fieldCache, fieldCache,
@ -144,6 +262,16 @@ class BoardContent extends StatelessWidget {
); );
} }
BoxDecoration _makeBoxDecoration(BuildContext context) {
final theme = context.read<AppTheme>();
final borderSide = BorderSide(color: theme.shader6, width: 1.0);
return BoxDecoration(
color: theme.surface,
border: Border.fromBorderSide(borderSide),
borderRadius: const BorderRadius.all(Radius.circular(6)),
);
}
void _openCard(String gridId, GridFieldCache fieldCache, RowPB rowPB, void _openCard(String gridId, GridFieldCache fieldCache, RowPB rowPB,
GridRowCache rowCache, BuildContext context) { GridRowCache rowCache, BuildContext context) {
final rowInfo = RowInfo( final rowInfo = RowInfo(
@ -165,7 +293,27 @@ class BoardContent extends StatelessWidget {
cellBuilder: GridCellBuilder(delegate: dataController), cellBuilder: GridCellBuilder(delegate: dataController),
dataController: dataController, dataController: dataController,
); );
}); },
);
}
}
class _ToolbarBlocAdaptor extends StatelessWidget {
const _ToolbarBlocAdaptor({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return BlocBuilder<BoardBloc, BoardState>(
builder: (context, state) {
final bloc = context.read<BoardBloc>();
final toolbarContext = BoardToolbarContext(
viewId: bloc.gridId,
fieldCache: bloc.fieldCache,
);
return BoardToolbar(toolbarContext: toolbarContext);
},
);
} }
} }

View File

@ -0,0 +1,62 @@
import 'package:app_flowy/plugins/grid/application/prelude.dart';
import 'package:flowy_infra/notifier.dart';
abstract class FocusableBoardCell {
set becomeFocus(bool isFocus);
}
class EditableCellNotifier {
final Notifier becomeFirstResponder = Notifier();
final Notifier resignFirstResponder = Notifier();
EditableCellNotifier();
}
class EditableRowNotifier {
Map<EditableCellId, EditableCellNotifier> cells = {};
void insertCell(
GridCellIdentifier cellIdentifier,
EditableCellNotifier notifier,
) {
cells[EditableCellId.from(cellIdentifier)] = notifier;
}
void becomeFirstResponder() {
for (final notifier in cells.values) {
notifier.becomeFirstResponder.notify();
}
}
void resignFirstResponder() {
for (final notifier in cells.values) {
notifier.resignFirstResponder.notify();
}
}
void dispose() {
for (final notifier in cells.values) {
notifier.resignFirstResponder.notify();
}
cells.clear();
}
}
abstract class EditableCell {
EditableCellNotifier? get editableNotifier;
}
class EditableCellId {
String fieldId;
String rowId;
EditableCellId(this.rowId, this.fieldId);
factory EditableCellId.from(GridCellIdentifier cellIdentifier) =>
EditableCellId(
cellIdentifier.rowId,
cellIdentifier.fieldId,
);
}

View File

@ -6,9 +6,11 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
class BoardCheckboxCell extends StatefulWidget { class BoardCheckboxCell extends StatefulWidget {
final String groupId;
final GridCellControllerBuilder cellControllerBuilder; final GridCellControllerBuilder cellControllerBuilder;
const BoardCheckboxCell({ const BoardCheckboxCell({
required this.groupId,
required this.cellControllerBuilder, required this.cellControllerBuilder,
Key? key, Key? key,
}) : super(key: key); }) : super(key: key);
@ -34,6 +36,8 @@ class _BoardCheckboxCellState extends State<BoardCheckboxCell> {
return BlocProvider.value( return BlocProvider.value(
value: _cellBloc, value: _cellBloc,
child: BlocBuilder<BoardCheckboxCellBloc, BoardCheckboxCellState>( child: BlocBuilder<BoardCheckboxCellBloc, BoardCheckboxCellState>(
buildWhen: (previous, current) =>
previous.isSelected != current.isSelected,
builder: (context, state) { builder: (context, state) {
final icon = state.isSelected final icon = state.isSelected
? svgWidget('editor/editor_check') ? svgWidget('editor/editor_check')

View File

@ -1,13 +1,16 @@
import 'package:app_flowy/plugins/board/application/card/board_date_cell_bloc.dart'; import 'package:app_flowy/plugins/board/application/card/board_date_cell_bloc.dart';
import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart'; import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart';
import 'package:flowy_infra/theme.dart';
import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
class BoardDateCell extends StatefulWidget { class BoardDateCell extends StatefulWidget {
final String groupId;
final GridCellControllerBuilder cellControllerBuilder; final GridCellControllerBuilder cellControllerBuilder;
const BoardDateCell({ const BoardDateCell({
required this.groupId,
required this.cellControllerBuilder, required this.cellControllerBuilder,
Key? key, Key? key,
}) : super(key: key); }) : super(key: key);
@ -34,6 +37,7 @@ class _BoardDateCellState extends State<BoardDateCell> {
return BlocProvider.value( return BlocProvider.value(
value: _cellBloc, value: _cellBloc,
child: BlocBuilder<BoardDateCellBloc, BoardDateCellState>( child: BlocBuilder<BoardDateCellBloc, BoardDateCellState>(
buildWhen: (previous, current) => previous.dateStr != current.dateStr,
builder: (context, state) { builder: (context, state) {
if (state.dateStr.isEmpty) { if (state.dateStr.isEmpty) {
return const SizedBox(); return const SizedBox();
@ -42,7 +46,8 @@ class _BoardDateCellState extends State<BoardDateCell> {
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
child: FlowyText.regular( child: FlowyText.regular(
state.dateStr, state.dateStr,
fontSize: 14, fontSize: 13,
color: context.read<AppTheme>().shader3,
), ),
); );
} }

View File

@ -5,9 +5,11 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
class BoardNumberCell extends StatefulWidget { class BoardNumberCell extends StatefulWidget {
final String groupId;
final GridCellControllerBuilder cellControllerBuilder; final GridCellControllerBuilder cellControllerBuilder;
const BoardNumberCell({ const BoardNumberCell({
required this.groupId,
required this.cellControllerBuilder, required this.cellControllerBuilder,
Key? key, Key? key,
}) : super(key: key); }) : super(key: key);
@ -34,13 +36,14 @@ class _BoardNumberCellState extends State<BoardNumberCell> {
return BlocProvider.value( return BlocProvider.value(
value: _cellBloc, value: _cellBloc,
child: BlocBuilder<BoardNumberCellBloc, BoardNumberCellState>( child: BlocBuilder<BoardNumberCellBloc, BoardNumberCellState>(
buildWhen: (previous, current) => previous.content != current.content,
builder: (context, state) { builder: (context, state) {
if (state.content.isEmpty) { if (state.content.isEmpty) {
return const SizedBox(); return const SizedBox();
} else { } else {
return Align( return Align(
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
child: FlowyText.regular( child: FlowyText.medium(
state.content, state.content,
fontSize: 14, fontSize: 14,
), ),

View File

@ -1,14 +1,22 @@
import 'package:app_flowy/plugins/board/application/card/board_select_option_cell_bloc.dart'; import 'package:app_flowy/plugins/board/application/card/board_select_option_cell_bloc.dart';
import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart'; import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart';
import 'package:app_flowy/plugins/grid/presentation/widgets/cell/select_option_cell/extension.dart'; import 'package:app_flowy/plugins/grid/presentation/widgets/cell/select_option_cell/extension.dart';
import 'package:app_flowy/plugins/grid/presentation/widgets/cell/select_option_cell/select_option_editor.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
class BoardSelectOptionCell extends StatefulWidget { import 'board_cell.dart';
class BoardSelectOptionCell extends StatefulWidget with EditableCell {
final String groupId;
final GridCellControllerBuilder cellControllerBuilder; final GridCellControllerBuilder cellControllerBuilder;
@override
final EditableCellNotifier? editableNotifier;
const BoardSelectOptionCell({ const BoardSelectOptionCell({
required this.groupId,
required this.cellControllerBuilder, required this.cellControllerBuilder,
this.editableNotifier,
Key? key, Key? key,
}) : super(key: key); }) : super(key: key);
@ -33,23 +41,41 @@ class _BoardSelectOptionCellState extends State<BoardSelectOptionCell> {
return BlocProvider.value( return BlocProvider.value(
value: _cellBloc, value: _cellBloc,
child: BlocBuilder<BoardSelectOptionCellBloc, BoardSelectOptionCellState>( child: BlocBuilder<BoardSelectOptionCellBloc, BoardSelectOptionCellState>(
buildWhen: (previous, current) {
return previous.selectedOptions != current.selectedOptions;
},
builder: (context, state) { builder: (context, state) {
if (state.selectedOptions
.where((element) => element.id == widget.groupId)
.isNotEmpty ||
state.selectedOptions.isEmpty) {
return const SizedBox();
} else {
final children = state.selectedOptions final children = state.selectedOptions
.map((option) => SelectOptionTag.fromOption( .map(
(option) => SelectOptionTag.fromOption(
context: context, context: context,
option: option, option: option,
))
.toList();
return Align(
alignment: Alignment.centerLeft,
child: AbsorbPointer(
child: Wrap(
children: children,
spacing: 4,
runSpacing: 2,
), ),
)
.toList();
return IntrinsicHeight(
child: Stack(
alignment: AlignmentDirectional.center,
fit: StackFit.expand,
children: [
Padding(
padding: const EdgeInsets.symmetric(vertical: 6),
child: Wrap(spacing: 4, runSpacing: 2, children: children),
),
_SelectOptionDialog(
controller: widget.cellControllerBuilder.build(),
),
],
), ),
); );
}
}, },
), ),
); );
@ -61,3 +87,23 @@ class _BoardSelectOptionCellState extends State<BoardSelectOptionCell> {
super.dispose(); super.dispose();
} }
} }
class _SelectOptionDialog extends StatelessWidget {
final GridSelectOptionCellController _controller;
const _SelectOptionDialog({
Key? key,
required IGridCellController controller,
}) : _controller = controller as GridSelectOptionCellController,
super(key: key);
@override
Widget build(BuildContext context) {
return InkWell(onTap: () {
SelectOptionCellEditor.show(
context,
_controller,
() {},
);
});
}
}

View File

@ -1,13 +1,27 @@
import 'package:app_flowy/plugins/board/application/card/board_text_cell_bloc.dart'; import 'package:app_flowy/plugins/board/application/card/board_text_cell_bloc.dart';
import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart'; import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart';
import 'package:app_flowy/plugins/grid/presentation/widgets/cell/cell_builder.dart';
import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
class BoardTextCell extends StatefulWidget { import 'board_cell.dart';
import 'define.dart';
class BoardTextCell extends StatefulWidget with EditableCell {
final String groupId;
final bool isFocus;
@override
final EditableCellNotifier? editableNotifier;
final GridCellControllerBuilder cellControllerBuilder; final GridCellControllerBuilder cellControllerBuilder;
const BoardTextCell({required this.cellControllerBuilder, Key? key})
: super(key: key); const BoardTextCell({
required this.groupId,
required this.cellControllerBuilder,
this.editableNotifier,
this.isFocus = false,
Key? key,
}) : super(key: key);
@override @override
State<BoardTextCell> createState() => _BoardTextCellState(); State<BoardTextCell> createState() => _BoardTextCellState();
@ -15,14 +29,48 @@ class BoardTextCell extends StatefulWidget {
class _BoardTextCellState extends State<BoardTextCell> { class _BoardTextCellState extends State<BoardTextCell> {
late BoardTextCellBloc _cellBloc; late BoardTextCellBloc _cellBloc;
late TextEditingController _controller;
bool focusWhenInit = false;
SingleListenerFocusNode focusNode = SingleListenerFocusNode();
@override @override
void initState() { void initState() {
final cellController = final cellController =
widget.cellControllerBuilder.build() as GridCellController; widget.cellControllerBuilder.build() as GridCellController;
_cellBloc = BoardTextCellBloc(cellController: cellController) _cellBloc = BoardTextCellBloc(cellController: cellController)
..add(const BoardTextCellEvent.initial()); ..add(const BoardTextCellEvent.initial());
_controller = TextEditingController(text: _cellBloc.state.content);
focusWhenInit = widget.isFocus;
if (widget.isFocus) {
focusNode.requestFocus();
}
focusNode.addListener(() {
if (!focusNode.hasFocus) {
_cellBloc.add(const BoardTextCellEvent.enableEdit(false));
if (focusWhenInit) {
setState(() {
focusWhenInit = false;
});
}
}
});
widget.editableNotifier?.becomeFirstResponder.addListener(() {
if (!mounted) return;
WidgetsBinding.instance.addPostFrameCallback((_) {
focusNode.requestFocus();
});
_cellBloc.add(const BoardTextCellEvent.enableEdit(true));
});
widget.editableNotifier?.resignFirstResponder.addListener(() {
if (!mounted) return;
_cellBloc.add(const BoardTextCellEvent.enableEdit(false));
});
super.initState(); super.initState();
} }
@ -30,32 +78,75 @@ class _BoardTextCellState extends State<BoardTextCell> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocProvider.value( return BlocProvider.value(
value: _cellBloc, value: _cellBloc,
child: BlocListener<BoardTextCellBloc, BoardTextCellState>(
listener: (context, state) {
if (_controller.text != state.content) {
_controller.text = state.content;
}
},
child: BlocBuilder<BoardTextCellBloc, BoardTextCellState>( child: BlocBuilder<BoardTextCellBloc, BoardTextCellState>(
builder: (context, state) { builder: (context, state) {
if (state.content.isEmpty) { if (state.content.isEmpty &&
state.enableEdit == false &&
focusWhenInit == false) {
return const SizedBox(); return const SizedBox();
}
//
Widget child;
if (state.enableEdit || focusWhenInit) {
child = _buildTextField();
} else { } else {
return Align( child = _buildText(state);
alignment: Alignment.centerLeft, }
child: ConstrainedBox( return Align(alignment: Alignment.centerLeft, child: child);
constraints: BoxConstraints.loose( },
const Size(double.infinity, 100),
),
child: FlowyText.regular(
state.content,
fontSize: 14,
), ),
), ),
); );
} }
},
), Future<void> focusChanged() async {
); _cellBloc.add(BoardTextCellEvent.updateText(_controller.text));
} }
@override @override
Future<void> dispose() async { Future<void> dispose() async {
_cellBloc.close(); _cellBloc.close();
_controller.dispose();
focusNode.dispose();
super.dispose(); super.dispose();
} }
Widget _buildText(BoardTextCellState state) {
return Padding(
padding: EdgeInsets.symmetric(
vertical: BoardSizes.cardCellVPadding,
),
child: FlowyText.medium(state.content, fontSize: 14),
);
}
Widget _buildTextField() {
return TextField(
controller: _controller,
focusNode: focusNode,
onChanged: (value) => focusChanged(),
onEditingComplete: () => focusNode.unfocus(),
maxLines: 1,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
fontFamily: 'Mulish',
),
decoration: InputDecoration(
// Magic number 4 makes the textField take up the same space as FlowyText
contentPadding: EdgeInsets.symmetric(
vertical: BoardSizes.cardCellVPadding + 4,
),
border: InputBorder.none,
isDense: true,
),
);
}
} }

View File

@ -5,9 +5,11 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
class BoardUrlCell extends StatefulWidget { class BoardUrlCell extends StatefulWidget {
final String groupId;
final GridCellControllerBuilder cellControllerBuilder; final GridCellControllerBuilder cellControllerBuilder;
const BoardUrlCell({ const BoardUrlCell({
required this.groupId,
required this.cellControllerBuilder, required this.cellControllerBuilder,
Key? key, Key? key,
}) : super(key: key); }) : super(key: key);
@ -34,6 +36,7 @@ class _BoardUrlCellState extends State<BoardUrlCell> {
return BlocProvider.value( return BlocProvider.value(
value: _cellBloc, value: _cellBloc,
child: BlocBuilder<BoardURLCellBloc, BoardURLCellState>( child: BlocBuilder<BoardURLCellBloc, BoardURLCellState>(
buildWhen: (previous, current) => previous.content != current.content,
builder: (context, state) { builder: (context, state) {
if (state.content.isEmpty) { if (state.content.isEmpty) {
return const SizedBox(); return const SizedBox();

View File

@ -7,25 +7,26 @@ import 'package:flowy_infra/theme.dart';
import 'package:flowy_infra_ui/flowy_infra_ui_web.dart'; import 'package:flowy_infra_ui/flowy_infra_ui_web.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'board_cell.dart';
import 'card_cell_builder.dart'; import 'card_cell_builder.dart';
import 'card_container.dart'; import 'card_container.dart';
typedef OnEndEditing = void Function(String rowId);
class BoardCard extends StatefulWidget { class BoardCard extends StatefulWidget {
final String gridId; final String gridId;
final String groupId;
final String fieldId;
final bool isEditing; final bool isEditing;
final CardDataController dataController; final CardDataController dataController;
final BoardCellBuilder cellBuilder; final BoardCellBuilder cellBuilder;
final OnEndEditing onEditEditing;
final void Function(BuildContext) openCard; final void Function(BuildContext) openCard;
const BoardCard({ const BoardCard({
required this.gridId, required this.gridId,
required this.groupId,
required this.fieldId,
required this.isEditing, required this.isEditing,
required this.dataController, required this.dataController,
required this.cellBuilder, required this.cellBuilder,
required this.onEditEditing,
required this.openCard, required this.openCard,
Key? key, Key? key,
}) : super(key: key); }) : super(key: key);
@ -36,13 +37,16 @@ class BoardCard extends StatefulWidget {
class _BoardCardState extends State<BoardCard> { class _BoardCardState extends State<BoardCard> {
late BoardCardBloc _cardBloc; late BoardCardBloc _cardBloc;
late EditableRowNotifier rowNotifier;
@override @override
void initState() { void initState() {
rowNotifier = EditableRowNotifier();
_cardBloc = BoardCardBloc( _cardBloc = BoardCardBloc(
gridId: widget.gridId, gridId: widget.gridId,
fieldId: widget.fieldId,
dataController: widget.dataController, dataController: widget.dataController,
); )..add(const BoardCardEvent.initial());
super.initState(); super.initState();
} }
@ -51,16 +55,28 @@ class _BoardCardState extends State<BoardCard> {
return BlocProvider.value( return BlocProvider.value(
value: _cardBloc, value: _cardBloc,
child: BlocBuilder<BoardCardBloc, BoardCardState>( child: BlocBuilder<BoardCardBloc, BoardCardState>(
buildWhen: (previous, current) {
return previous.cells.length != current.cells.length;
},
builder: (context, state) { builder: (context, state) {
return BoardCardContainer( return BoardCardContainer(
accessoryBuilder: (context) { accessoryBuilder: (context) {
return [const _CardMoreOption()]; return [
_CardEditOption(
startEditing: () => rowNotifier.becomeFirstResponder(),
),
const _CardMoreOption(),
];
}, },
onTap: (context) { onTap: (context) {
widget.openCard(context); widget.openCard(context);
}, },
child: Column( child: Column(
children: _makeCells(context, state.gridCellMap), mainAxisSize: MainAxisSize.min,
children: _makeCells(
context,
state.cells.map((cell) => cell.identifier).toList(),
),
), ),
); );
}, },
@ -68,16 +84,42 @@ class _BoardCardState extends State<BoardCard> {
); );
} }
List<Widget> _makeCells(BuildContext context, GridCellMap cellMap) { List<Widget> _makeCells(
return cellMap.values.map( BuildContext context,
(cellId) { List<GridCellIdentifier> cells,
final child = widget.cellBuilder.buildCell(cellId); ) {
return Padding( final List<Widget> children = [];
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 5), cells.asMap().forEach(
(int index, GridCellIdentifier cellId) {
final cellNotifier = EditableCellNotifier();
Widget child = widget.cellBuilder.buildCell(
widget.groupId,
cellId,
widget.isEditing,
cellNotifier,
);
if (index == 0) {
rowNotifier.insertCell(cellId, cellNotifier);
}
child = Padding(
key: cellId.key(),
padding: const EdgeInsets.only(left: 4, right: 4),
child: child, child: child,
); );
children.add(child);
}, },
).toList(); );
return children;
}
@override
Future<void> dispose() async {
rowNotifier.dispose();
_cardBloc.close();
super.dispose();
} }
} }
@ -86,7 +128,11 @@ class _CardMoreOption extends StatelessWidget with CardAccessory {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return svgWidget('home/details', color: context.read<AppTheme>().iconColor); return Padding(
padding: const EdgeInsets.all(3.0),
child:
svgWidget('grid/details', color: context.read<AppTheme>().iconColor),
);
} }
@override @override
@ -96,3 +142,27 @@ class _CardMoreOption extends StatelessWidget with CardAccessory {
).show(context, direction: AnchorDirection.bottomWithCenterAligned); ).show(context, direction: AnchorDirection.bottomWithCenterAligned);
} }
} }
class _CardEditOption extends StatelessWidget with CardAccessory {
final VoidCallback startEditing;
const _CardEditOption({
required this.startEditing,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(3.0),
child: svgWidget(
'editor/edit',
color: context.read<AppTheme>().iconColor,
),
);
}
@override
void onTap(BuildContext context) {
startEditing();
}
}

View File

@ -2,6 +2,7 @@ import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_servic
import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'board_cell.dart';
import 'board_checkbox_cell.dart'; import 'board_checkbox_cell.dart';
import 'board_date_cell.dart'; import 'board_date_cell.dart';
import 'board_number_cell.dart'; import 'board_number_cell.dart';
@ -19,7 +20,12 @@ class BoardCellBuilder {
BoardCellBuilder(this.delegate); BoardCellBuilder(this.delegate);
Widget buildCell(GridCellIdentifier cellId) { Widget buildCell(
String groupId,
GridCellIdentifier cellId,
bool isEditing,
EditableCellNotifier cellNotifier,
) {
final cellControllerBuilder = GridCellControllerBuilder( final cellControllerBuilder = GridCellControllerBuilder(
delegate: delegate, delegate: delegate,
cellId: cellId, cellId: cellId,
@ -30,36 +36,46 @@ class BoardCellBuilder {
switch (cellId.fieldType) { switch (cellId.fieldType) {
case FieldType.Checkbox: case FieldType.Checkbox:
return BoardCheckboxCell( return BoardCheckboxCell(
groupId: groupId,
cellControllerBuilder: cellControllerBuilder, cellControllerBuilder: cellControllerBuilder,
key: key, key: key,
); );
case FieldType.DateTime: case FieldType.DateTime:
return BoardDateCell( return BoardDateCell(
groupId: groupId,
cellControllerBuilder: cellControllerBuilder, cellControllerBuilder: cellControllerBuilder,
key: key, key: key,
); );
case FieldType.SingleSelect: case FieldType.SingleSelect:
return BoardSelectOptionCell( return BoardSelectOptionCell(
groupId: groupId,
cellControllerBuilder: cellControllerBuilder, cellControllerBuilder: cellControllerBuilder,
key: key, key: key,
); );
case FieldType.MultiSelect: case FieldType.MultiSelect:
return BoardSelectOptionCell( return BoardSelectOptionCell(
groupId: groupId,
cellControllerBuilder: cellControllerBuilder, cellControllerBuilder: cellControllerBuilder,
editableNotifier: cellNotifier,
key: key, key: key,
); );
case FieldType.Number: case FieldType.Number:
return BoardNumberCell( return BoardNumberCell(
groupId: groupId,
cellControllerBuilder: cellControllerBuilder, cellControllerBuilder: cellControllerBuilder,
key: key, key: key,
); );
case FieldType.RichText: case FieldType.RichText:
return BoardTextCell( return BoardTextCell(
groupId: groupId,
cellControllerBuilder: cellControllerBuilder, cellControllerBuilder: cellControllerBuilder,
isFocus: isEditing,
editableNotifier: cellNotifier,
key: key, key: key,
); );
case FieldType.URL: case FieldType.URL:
return BoardUrlCell( return BoardUrlCell(
groupId: groupId,
cellControllerBuilder: cellControllerBuilder, cellControllerBuilder: cellControllerBuilder,
key: key, key: key,
); );

View File

@ -26,8 +26,8 @@ class BoardCardContainer extends StatelessWidget {
final accessories = accessoryBuilder!(context); final accessories = accessoryBuilder!(context);
if (accessories.isNotEmpty) { if (accessories.isNotEmpty) {
container = _CardEnterRegion( container = _CardEnterRegion(
child: container,
accessories: accessories, accessories: accessories,
child: container,
); );
} }
} }
@ -69,25 +69,48 @@ class CardAccessoryContainer extends StatelessWidget {
style: HoverStyle( style: HoverStyle(
hoverColor: theme.hover, hoverColor: theme.hover,
backgroundColor: theme.surface, backgroundColor: theme.surface,
borderRadius: BorderRadius.zero,
), ),
builder: (_, onHover) => Container( builder: (_, onHover) => SizedBox(
width: 26, width: 24,
height: 26, height: 24,
padding: const EdgeInsets.all(3),
child: accessory, child: accessory,
), ),
); );
return GestureDetector( return GestureDetector(
child: hover,
behavior: HitTestBehavior.opaque, behavior: HitTestBehavior.opaque,
onTap: () => accessory.onTap(context), onTap: () => accessory.onTap(context),
child: hover,
); );
}).toList(); }).toList();
return Wrap(children: children, spacing: 6); return Container(
clipBehavior: Clip.hardEdge,
decoration: _makeBoxDecoration(context),
child: Row(children: children),
);
} }
} }
BoxDecoration _makeBoxDecoration(BuildContext context) {
final theme = context.read<AppTheme>();
final borderSide = BorderSide(color: theme.shader6, width: 1.0);
return BoxDecoration(
color: Colors.transparent,
border: Border.fromBorderSide(borderSide),
// boxShadow: const [
// BoxShadow(
// color: Colors.transparent,
// spreadRadius: 0,
// blurRadius: 5,
// offset: Offset.zero,
// )
// ],
borderRadius: const BorderRadius.all(Radius.circular(4)),
);
}
class _CardEnterRegion extends StatelessWidget { class _CardEnterRegion extends StatelessWidget {
final Widget child; final Widget child;
final List<CardAccessory> accessories; final List<CardAccessory> accessories;
@ -102,8 +125,9 @@ class _CardEnterRegion extends StatelessWidget {
builder: (context, onEnter, _) { builder: (context, onEnter, _) {
List<Widget> children = [child]; List<Widget> children = [child];
if (onEnter) { if (onEnter) {
children.add(CardAccessoryContainer(accessories: accessories) children.add(CardAccessoryContainer(
.positioned(right: 0)); accessories: accessories,
).positioned(right: 0));
} }
return MouseRegion( return MouseRegion(
@ -116,7 +140,7 @@ class _CardEnterRegion extends StatelessWidget {
.onEnter = false, .onEnter = false,
child: IntrinsicHeight( child: IntrinsicHeight(
child: Stack( child: Stack(
alignment: AlignmentDirectional.center, alignment: AlignmentDirectional.topEnd,
fit: StackFit.expand, fit: StackFit.expand,
children: children, children: children,
)), )),

View File

@ -0,0 +1,3 @@
class BoardSizes {
static double get cardCellVPadding => 6;
}

View File

@ -0,0 +1,168 @@
import 'package:app_flowy/generated/locale_keys.g.dart';
import 'package:app_flowy/plugins/board/application/toolbar/board_setting_bloc.dart';
import 'package:app_flowy/plugins/grid/application/field/field_cache.dart';
import 'package:app_flowy/plugins/grid/presentation/layout/sizes.dart';
import 'package:app_flowy/plugins/grid/presentation/widgets/toolbar/grid_property.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/image.dart';
import 'package:flowy_infra/theme.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/button.dart';
import 'package:flowy_infra_ui/style_widget/scrolling/styled_list.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'board_toolbar.dart';
class BoardSettingContext {
final String viewId;
final GridFieldCache fieldCache;
BoardSettingContext({
required this.viewId,
required this.fieldCache,
});
factory BoardSettingContext.from(BoardToolbarContext toolbarContext) =>
BoardSettingContext(
viewId: toolbarContext.viewId,
fieldCache: toolbarContext.fieldCache,
);
}
class BoardSettingList extends StatelessWidget {
final BoardSettingContext settingContext;
final Function(BoardSettingAction, BoardSettingContext) onAction;
const BoardSettingList({
required this.settingContext,
required this.onAction,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => BoardSettingBloc(gridId: settingContext.viewId),
child: BlocListener<BoardSettingBloc, BoardSettingState>(
listenWhen: (previous, current) =>
previous.selectedAction != current.selectedAction,
listener: (context, state) {
state.selectedAction.foldLeft(null, (_, action) {
FlowyOverlay.of(context).remove(identifier());
onAction(action, settingContext);
});
},
child: BlocBuilder<BoardSettingBloc, BoardSettingState>(
builder: (context, state) {
return _renderList();
},
),
),
);
}
Widget _renderList() {
final cells = BoardSettingAction.values.map((action) {
return _SettingItem(action: action);
}).toList();
return SizedBox(
width: 140,
child: ListView.separated(
shrinkWrap: true,
controller: ScrollController(),
itemCount: cells.length,
separatorBuilder: (context, index) {
return VSpace(GridSize.typeOptionSeparatorHeight);
},
physics: StyledScrollPhysics(),
itemBuilder: (BuildContext context, int index) {
return cells[index];
},
),
);
}
static void show(BuildContext context, BoardSettingContext settingContext) {
final list = BoardSettingList(
settingContext: settingContext,
onAction: (action, settingContext) {
switch (action) {
case BoardSettingAction.properties:
GridPropertyList(
gridId: settingContext.viewId,
fieldCache: settingContext.fieldCache)
.show(context);
break;
}
},
);
FlowyOverlay.of(context).insertWithAnchor(
widget: OverlayContainer(
constraints: BoxConstraints.loose(const Size(140, 400)),
child: list,
),
identifier: identifier(),
anchorContext: context,
anchorDirection: AnchorDirection.bottomRight,
style: FlowyOverlayStyle(blur: false),
);
}
static String identifier() {
return (BoardSettingList).toString();
}
}
class _SettingItem extends StatelessWidget {
final BoardSettingAction action;
const _SettingItem({
required this.action,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final theme = context.read<AppTheme>();
final isSelected = context
.read<BoardSettingBloc>()
.state
.selectedAction
.foldLeft(false, (_, selectedAction) => selectedAction == action);
return SizedBox(
height: 30,
child: FlowyButton(
isSelected: isSelected,
text: FlowyText.medium(action.title(),
fontSize: 12, color: theme.textColor),
hoverColor: theme.hover,
onTap: () {
context
.read<BoardSettingBloc>()
.add(BoardSettingEvent.performAction(action));
},
leftIcon: svgWidget(action.iconName(), color: theme.iconColor),
),
);
}
}
extension _GridSettingExtension on BoardSettingAction {
String iconName() {
switch (this) {
case BoardSettingAction.properties:
return 'grid/setting/properties';
}
}
String title() {
switch (this) {
case BoardSettingAction.properties:
return LocaleKeys.grid_settings_Properties.tr();
}
}
}

View File

@ -0,0 +1,60 @@
import 'package:app_flowy/plugins/grid/application/field/field_cache.dart';
import 'package:flowy_infra/image.dart';
import 'package:flowy_infra/theme.dart';
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
import 'package:flutter/widgets.dart';
import 'package:provider/provider.dart';
import 'board_setting.dart';
class BoardToolbarContext {
final String viewId;
final GridFieldCache fieldCache;
BoardToolbarContext({
required this.viewId,
required this.fieldCache,
});
}
class BoardToolbar extends StatelessWidget {
final BoardToolbarContext toolbarContext;
const BoardToolbar({
required this.toolbarContext,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return SizedBox(
height: 40,
child: Row(
children: [
_SettingButton(
settingContext: BoardSettingContext.from(toolbarContext),
),
],
),
);
}
}
class _SettingButton extends StatelessWidget {
final BoardSettingContext settingContext;
const _SettingButton({required this.settingContext, Key? key})
: super(key: key);
@override
Widget build(BuildContext context) {
final theme = context.read<AppTheme>();
return FlowyIconButton(
hoverColor: theme.hover,
width: 22,
onPressed: () => BoardSettingList.show(context, settingContext),
icon: Padding(
padding: const EdgeInsets.symmetric(vertical: 3.0, horizontal: 3.0),
child: svgWidget("grid/setting/setting"),
),
);
}
}

View File

@ -40,11 +40,11 @@ class DocumentBanner extends StatelessWidget {
downColor: theme.main1, downColor: theme.main1,
outlineColor: Colors.white, outlineColor: Colors.white,
borderRadius: Corners.s8Border, borderRadius: Corners.s8Border,
onPressed: onRestore,
child: FlowyText.medium( child: FlowyText.medium(
LocaleKeys.deletePagePrompt_restore.tr(), LocaleKeys.deletePagePrompt_restore.tr(),
color: Colors.white, color: Colors.white,
fontSize: 14), fontSize: 14)),
onPressed: onRestore),
const HSpace(20), const HSpace(20),
BaseStyledButton( BaseStyledButton(
minWidth: 220, minWidth: 220,
@ -55,11 +55,11 @@ class DocumentBanner extends StatelessWidget {
downColor: theme.main1, downColor: theme.main1,
outlineColor: Colors.white, outlineColor: Colors.white,
borderRadius: Corners.s8Border, borderRadius: Corners.s8Border,
onPressed: onDelete,
child: FlowyText.medium( child: FlowyText.medium(
LocaleKeys.deletePagePrompt_deletePermanent.tr(), LocaleKeys.deletePagePrompt_deletePermanent.tr(),
color: Colors.white, color: Colors.white,
fontSize: 14), fontSize: 14)),
onPressed: onDelete),
], ],
), ),
), ),

View File

@ -16,7 +16,10 @@ class EditorCheckboxBuilder extends QuillCheckboxBuilder {
EditorCheckboxBuilder(this.theme); EditorCheckboxBuilder(this.theme);
@override @override
Widget build({required BuildContext context, required bool isChecked, required ValueChanged<bool> onChanged}) { Widget build(
{required BuildContext context,
required bool isChecked,
required ValueChanged<bool> onChanged}) {
return FlowyEditorCheckbox( return FlowyEditorCheckbox(
theme: theme, theme: theme,
isChecked: isChecked, isChecked: isChecked,
@ -37,10 +40,10 @@ class FlowyEditorCheckbox extends StatefulWidget {
}) : super(key: key); }) : super(key: key);
@override @override
_FlowyEditorCheckboxState createState() => _FlowyEditorCheckboxState(); FlowyEditorCheckboxState createState() => FlowyEditorCheckboxState();
} }
class _FlowyEditorCheckboxState extends State<FlowyEditorCheckbox> { class FlowyEditorCheckboxState extends State<FlowyEditorCheckbox> {
late bool isChecked; late bool isChecked;
@override @override
@ -51,7 +54,9 @@ class _FlowyEditorCheckboxState extends State<FlowyEditorCheckbox> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final icon = isChecked ? svgWidget('editor/editor_check') : svgWidget('editor/editor_uncheck'); final icon = isChecked
? svgWidget('editor/editor_check')
: svgWidget('editor/editor_uncheck');
return Align( return Align(
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
child: FlowyIconButton( child: FlowyIconButton(

View File

@ -28,10 +28,10 @@ class FlowyCheckListButton extends StatefulWidget {
final String tooltipText; final String tooltipText;
@override @override
_FlowyCheckListButtonState createState() => _FlowyCheckListButtonState(); FlowyCheckListButtonState createState() => FlowyCheckListButtonState();
} }
class _FlowyCheckListButtonState extends State<FlowyCheckListButton> { class FlowyCheckListButtonState extends State<FlowyCheckListButton> {
bool? _isToggled; bool? _isToggled;
Style get _selectionStyle => widget.controller.getSelectionStyle(); Style get _selectionStyle => widget.controller.getSelectionStyle();

View File

@ -24,10 +24,10 @@ class FlowyColorButton extends StatefulWidget {
final QuillIconTheme? iconTheme; final QuillIconTheme? iconTheme;
@override @override
_FlowyColorButtonState createState() => _FlowyColorButtonState(); FlowyColorButtonState createState() => FlowyColorButtonState();
} }
class _FlowyColorButtonState extends State<FlowyColorButton> { class FlowyColorButtonState extends State<FlowyColorButton> {
late bool _isToggledColor; late bool _isToggledColor;
late bool _isToggledBackground; late bool _isToggledBackground;
late bool _isWhite; late bool _isWhite;
@ -37,10 +37,14 @@ class _FlowyColorButtonState extends State<FlowyColorButton> {
void _didChangeEditingValue() { void _didChangeEditingValue() {
setState(() { setState(() {
_isToggledColor = _getIsToggledColor(widget.controller.getSelectionStyle().attributes); _isToggledColor =
_isToggledBackground = _getIsToggledBackground(widget.controller.getSelectionStyle().attributes); _getIsToggledColor(widget.controller.getSelectionStyle().attributes);
_isWhite = _isToggledColor && _selectionStyle.attributes['color']!.value == '#ffffff'; _isToggledBackground = _getIsToggledBackground(
_isWhitebackground = _isToggledBackground && _selectionStyle.attributes['background']!.value == '#ffffff'; widget.controller.getSelectionStyle().attributes);
_isWhite = _isToggledColor &&
_selectionStyle.attributes['color']!.value == '#ffffff';
_isWhitebackground = _isToggledBackground &&
_selectionStyle.attributes['background']!.value == '#ffffff';
}); });
} }
@ -49,8 +53,10 @@ class _FlowyColorButtonState extends State<FlowyColorButton> {
super.initState(); super.initState();
_isToggledColor = _getIsToggledColor(_selectionStyle.attributes); _isToggledColor = _getIsToggledColor(_selectionStyle.attributes);
_isToggledBackground = _getIsToggledBackground(_selectionStyle.attributes); _isToggledBackground = _getIsToggledBackground(_selectionStyle.attributes);
_isWhite = _isToggledColor && _selectionStyle.attributes['color']!.value == '#ffffff'; _isWhite = _isToggledColor &&
_isWhitebackground = _isToggledBackground && _selectionStyle.attributes['background']!.value == '#ffffff'; _selectionStyle.attributes['color']!.value == '#ffffff';
_isWhitebackground = _isToggledBackground &&
_selectionStyle.attributes['background']!.value == '#ffffff';
widget.controller.addListener(_didChangeEditingValue); widget.controller.addListener(_didChangeEditingValue);
} }
@ -69,9 +75,12 @@ class _FlowyColorButtonState extends State<FlowyColorButton> {
oldWidget.controller.removeListener(_didChangeEditingValue); oldWidget.controller.removeListener(_didChangeEditingValue);
widget.controller.addListener(_didChangeEditingValue); widget.controller.addListener(_didChangeEditingValue);
_isToggledColor = _getIsToggledColor(_selectionStyle.attributes); _isToggledColor = _getIsToggledColor(_selectionStyle.attributes);
_isToggledBackground = _getIsToggledBackground(_selectionStyle.attributes); _isToggledBackground =
_isWhite = _isToggledColor && _selectionStyle.attributes['color']!.value == '#ffffff'; _getIsToggledBackground(_selectionStyle.attributes);
_isWhitebackground = _isToggledBackground && _selectionStyle.attributes['background']!.value == '#ffffff'; _isWhite = _isToggledColor &&
_selectionStyle.attributes['color']!.value == '#ffffff';
_isWhitebackground = _isToggledBackground &&
_selectionStyle.attributes['background']!.value == '#ffffff';
} }
} }
@ -88,7 +97,8 @@ class _FlowyColorButtonState extends State<FlowyColorButton> {
final fillColor = _isToggledColor && !widget.background && _isWhite final fillColor = _isToggledColor && !widget.background && _isWhite
? stringToColor('#ffffff') ? stringToColor('#ffffff')
: (widget.iconTheme?.iconUnselectedFillColor ?? theme.canvasColor); : (widget.iconTheme?.iconUnselectedFillColor ?? theme.canvasColor);
final fillColorBackground = _isToggledBackground && widget.background && _isWhitebackground final fillColorBackground =
_isToggledBackground && widget.background && _isWhitebackground
? stringToColor('#ffffff') ? stringToColor('#ffffff')
: (widget.iconTheme?.iconUnselectedFillColor ?? theme.canvasColor); : (widget.iconTheme?.iconUnselectedFillColor ?? theme.canvasColor);
@ -99,7 +109,8 @@ class _FlowyColorButtonState extends State<FlowyColorButton> {
highlightElevation: 0, highlightElevation: 0,
hoverElevation: 0, hoverElevation: 0,
size: widget.iconSize * kIconButtonFactor, size: widget.iconSize * kIconButtonFactor,
icon: Icon(widget.icon, size: widget.iconSize, color: theme.iconTheme.color), icon: Icon(widget.icon,
size: widget.iconSize, color: theme.iconTheme.color),
fillColor: widget.background ? fillColorBackground : fillColor, fillColor: widget.background ? fillColorBackground : fillColor,
onPressed: _showColorPicker, onPressed: _showColorPicker,
), ),
@ -112,13 +123,16 @@ class _FlowyColorButtonState extends State<FlowyColorButton> {
hex = hex.substring(2); hex = hex.substring(2);
} }
hex = '#$hex'; hex = '#$hex';
widget.controller.formatSelection(widget.background ? BackgroundAttribute(hex) : ColorAttribute(hex)); widget.controller.formatSelection(
widget.background ? BackgroundAttribute(hex) : ColorAttribute(hex));
Navigator.of(context).pop(); Navigator.of(context).pop();
} }
void _showColorPicker() { void _showColorPicker() {
final style = widget.controller.getSelectionStyle(); final style = widget.controller.getSelectionStyle();
final values = style.values.where((v) => v.key == Attribute.background.key).map((v) => v.value); final values = style.values
.where((v) => v.key == Attribute.background.key)
.map((v) => v.value);
int initialColor = 0; int initialColor = 0;
if (values.isNotEmpty) { if (values.isNotEmpty) {
assert(values.length == 1); assert(values.length == 1);
@ -160,7 +174,9 @@ class FlowyColorPicker extends StatefulWidget {
]; ];
final Function(Color?) onColorChanged; final Function(Color?) onColorChanged;
final int initialColor; final int initialColor;
FlowyColorPicker({Key? key, required this.onColorChanged, this.initialColor = 0}) : super(key: key); FlowyColorPicker(
{Key? key, required this.onColorChanged, this.initialColor = 0})
: super(key: key);
@override @override
State<FlowyColorPicker> createState() => _FlowyColorPickerState(); State<FlowyColorPicker> createState() => _FlowyColorPickerState();
@ -178,8 +194,10 @@ class _FlowyColorPickerState extends State<FlowyColorPicker> {
const double crossAxisSpacing = 10; const double crossAxisSpacing = 10;
final numberOfRows = (widget.colors.length / crossAxisCount).ceil(); final numberOfRows = (widget.colors.length / crossAxisCount).ceil();
const perRowHeight = ((width - ((crossAxisCount - 1) * mainAxisSpacing)) / crossAxisCount); const perRowHeight =
final totalHeight = numberOfRows * perRowHeight + numberOfRows * crossAxisSpacing; ((width - ((crossAxisCount - 1) * mainAxisSpacing)) / crossAxisCount);
final totalHeight =
numberOfRows * perRowHeight + numberOfRows * crossAxisSpacing;
return Container( return Container(
constraints: BoxConstraints.tightFor(width: width, height: totalHeight), constraints: BoxConstraints.tightFor(width: width, height: totalHeight),
@ -198,7 +216,8 @@ class _FlowyColorPickerState extends State<FlowyColorPicker> {
delegate: SliverChildBuilderDelegate( delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) { (BuildContext context, int index) {
if (widget.colors.length > index) { if (widget.colors.length > index) {
final isSelected = widget.colors[index] == widget.initialColor; final isSelected =
widget.colors[index] == widget.initialColor;
return ColorItem( return ColorItem(
color: Color(widget.colors[index]), color: Color(widget.colors[index]),
onPressed: widget.onColorChanged, onPressed: widget.onColorChanged,
@ -242,7 +261,8 @@ class ColorItem extends StatelessWidget {
); );
} else { } else {
return RawMaterialButton( return RawMaterialButton(
shape: const CircleBorder(side: BorderSide(color: Colors.white, width: 8)) + shape: const CircleBorder(
side: BorderSide(color: Colors.white, width: 8)) +
CircleBorder(side: BorderSide(color: color, width: 4)), CircleBorder(side: BorderSide(color: color, width: 4)),
onPressed: () { onPressed: () {
if (isSelected) { if (isSelected) {

View File

@ -16,10 +16,10 @@ class FlowyHeaderStyleButton extends StatefulWidget {
final double iconSize; final double iconSize;
@override @override
_FlowyHeaderStyleButtonState createState() => _FlowyHeaderStyleButtonState(); FlowyHeaderStyleButtonState createState() => FlowyHeaderStyleButtonState();
} }
class _FlowyHeaderStyleButtonState extends State<FlowyHeaderStyleButton> { class FlowyHeaderStyleButtonState extends State<FlowyHeaderStyleButton> {
Attribute? _value; Attribute? _value;
Style get _selectionStyle => widget.controller.getSelectionStyle(); Style get _selectionStyle => widget.controller.getSelectionStyle();
@ -28,22 +28,27 @@ class _FlowyHeaderStyleButtonState extends State<FlowyHeaderStyleButton> {
void initState() { void initState() {
super.initState(); super.initState();
setState(() { setState(() {
_value = _selectionStyle.attributes[Attribute.header.key] ?? Attribute.header; _value =
_selectionStyle.attributes[Attribute.header.key] ?? Attribute.header;
}); });
widget.controller.addListener(_didChangeEditingValue); widget.controller.addListener(_didChangeEditingValue);
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final _valueToText = <Attribute, String>{ final valueToText = <Attribute, String>{
Attribute.h1: 'H1', Attribute.h1: 'H1',
Attribute.h2: 'H2', Attribute.h2: 'H2',
Attribute.h3: 'H3', Attribute.h3: 'H3',
}; };
final _valueAttribute = <Attribute>[Attribute.h1, Attribute.h2, Attribute.h3]; final valueAttribute = <Attribute>[
final _valueString = <String>['H1', 'H2', 'H3']; Attribute.h1,
final _attributeImageName = <String>['editor/H1', 'editor/H2', 'editor/H3']; Attribute.h2,
Attribute.h3
];
final valueString = <String>['H1', 'H2', 'H3'];
final attributeImageName = <String>['editor/H1', 'editor/H2', 'editor/H3'];
return Row( return Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@ -52,18 +57,18 @@ class _FlowyHeaderStyleButtonState extends State<FlowyHeaderStyleButton> {
// _valueToText[_value] == _valueString[index] ? svg('editor/H1', color: Colors.white) : svg('editor/H1'); // _valueToText[_value] == _valueString[index] ? svg('editor/H1', color: Colors.white) : svg('editor/H1');
final headerTitle = "${LocaleKeys.toolbar_header.tr()} ${index + 1}"; final headerTitle = "${LocaleKeys.toolbar_header.tr()} ${index + 1}";
final _isToggled = _valueToText[_value] == _valueString[index]; final isToggled = valueToText[_value] == valueString[index];
return ToolbarIconButton( return ToolbarIconButton(
onPressed: () { onPressed: () {
if (_isToggled) { if (isToggled) {
widget.controller.formatSelection(Attribute.header); widget.controller.formatSelection(Attribute.header);
} else { } else {
widget.controller.formatSelection(_valueAttribute[index]); widget.controller.formatSelection(valueAttribute[index]);
} }
}, },
width: widget.iconSize * kIconButtonFactor, width: widget.iconSize * kIconButtonFactor,
iconName: _attributeImageName[index], iconName: attributeImageName[index],
isToggled: _isToggled, isToggled: isToggled,
tooltipText: headerTitle, tooltipText: headerTitle,
); );
}), }),
@ -72,7 +77,8 @@ class _FlowyHeaderStyleButtonState extends State<FlowyHeaderStyleButton> {
void _didChangeEditingValue() { void _didChangeEditingValue() {
setState(() { setState(() {
_value = _selectionStyle.attributes[Attribute.header.key] ?? Attribute.header; _value =
_selectionStyle.attributes[Attribute.header.key] ?? Attribute.header;
}); });
} }
@ -82,7 +88,8 @@ class _FlowyHeaderStyleButtonState extends State<FlowyHeaderStyleButton> {
if (oldWidget.controller != widget.controller) { if (oldWidget.controller != widget.controller) {
oldWidget.controller.removeListener(_didChangeEditingValue); oldWidget.controller.removeListener(_didChangeEditingValue);
widget.controller.addListener(_didChangeEditingValue); widget.controller.addListener(_didChangeEditingValue);
_value = _selectionStyle.attributes[Attribute.header.key] ?? Attribute.header; _value =
_selectionStyle.attributes[Attribute.header.key] ?? Attribute.header;
} }
} }

View File

@ -19,10 +19,10 @@ class FlowyLinkStyleButton extends StatefulWidget {
final double iconSize; final double iconSize;
@override @override
_FlowyLinkStyleButtonState createState() => _FlowyLinkStyleButtonState(); FlowyLinkStyleButtonState createState() => FlowyLinkStyleButtonState();
} }
class _FlowyLinkStyleButtonState extends State<FlowyLinkStyleButton> { class FlowyLinkStyleButtonState extends State<FlowyLinkStyleButton> {
void _didChangeSelection() { void _didChangeSelection() {
setState(() {}); setState(() {});
} }
@ -75,7 +75,9 @@ class _FlowyLinkStyleButtonState extends State<FlowyLinkStyleButton> {
void _openLinkDialog(BuildContext context) { void _openLinkDialog(BuildContext context) {
final style = widget.controller.getSelectionStyle(); final style = widget.controller.getSelectionStyle();
final values = style.values.where((v) => v.key == Attribute.link.key).map((v) => v.value); final values = style.values
.where((v) => v.key == Attribute.link.key)
.map((v) => v.value);
String value = ""; String value = "";
if (values.isNotEmpty) { if (values.isNotEmpty) {
assert(values.length == 1); assert(values.length == 1);

View File

@ -21,10 +21,10 @@ class FlowyToggleStyleButton extends StatefulWidget {
}) : super(key: key); }) : super(key: key);
@override @override
_ToggleStyleButtonState createState() => _ToggleStyleButtonState(); ToggleStyleButtonState createState() => ToggleStyleButtonState();
} }
class _ToggleStyleButtonState extends State<FlowyToggleStyleButton> { class ToggleStyleButtonState extends State<FlowyToggleStyleButton> {
bool? _isToggled; bool? _isToggled;
Style get _selectionStyle => widget.controller.getSelectionStyle(); Style get _selectionStyle => widget.controller.getSelectionStyle();
@override @override
@ -77,6 +77,8 @@ class _ToggleStyleButtonState extends State<FlowyToggleStyleButton> {
} }
void _toggleAttribute() { void _toggleAttribute() {
widget.controller.formatSelection(_isToggled! ? Attribute.clone(widget.attribute, null) : widget.attribute); widget.controller.formatSelection(_isToggled!
? Attribute.clone(widget.attribute, null)
: widget.attribute);
} }
} }

View File

@ -32,7 +32,8 @@ class EditorToolbar extends StatelessWidget implements PreferredSizeWidget {
return Container( return Container(
color: Theme.of(context).canvasColor, color: Theme.of(context).canvasColor,
constraints: BoxConstraints.tightFor(height: preferredSize.height), constraints: BoxConstraints.tightFor(height: preferredSize.height),
child: ToolbarButtonList(buttons: children).padding(horizontal: 4, vertical: 4), child: ToolbarButtonList(buttons: children)
.padding(horizontal: 4, vertical: 4),
); );
} }
@ -168,10 +169,11 @@ class ToolbarButtonList extends StatefulWidget {
final List<Widget> buttons; final List<Widget> buttons;
@override @override
_ToolbarButtonListState createState() => _ToolbarButtonListState(); ToolbarButtonListState createState() => ToolbarButtonListState();
} }
class _ToolbarButtonListState extends State<ToolbarButtonList> with WidgetsBindingObserver { class ToolbarButtonListState extends State<ToolbarButtonList>
with WidgetsBindingObserver {
final ScrollController _controller = ScrollController(); final ScrollController _controller = ScrollController();
bool _showLeftArrow = false; bool _showLeftArrow = false;
bool _showRightArrow = false; bool _showRightArrow = false;
@ -196,7 +198,8 @@ class _ToolbarButtonListState extends State<ToolbarButtonList> with WidgetsBindi
return LayoutBuilder( return LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) { builder: (BuildContext context, BoxConstraints constraints) {
List<Widget> children = []; List<Widget> children = [];
double width = (widget.buttons.length + 2) * defaultIconSize * kIconButtonFactor; double width =
(widget.buttons.length + 2) * defaultIconSize * kIconButtonFactor;
final isFit = constraints.maxWidth > width; final isFit = constraints.maxWidth > width;
if (!isFit) { if (!isFit) {
children.add(_buildLeftArrow()); children.add(_buildLeftArrow());
@ -233,8 +236,10 @@ class _ToolbarButtonListState extends State<ToolbarButtonList> with WidgetsBindi
void _handleScroll() { void _handleScroll() {
if (!mounted) return; if (!mounted) return;
setState(() { setState(() {
_showLeftArrow = _controller.position.minScrollExtent != _controller.position.pixels; _showLeftArrow =
_showRightArrow = _controller.position.maxScrollExtent != _controller.position.pixels; _controller.position.minScrollExtent != _controller.position.pixels;
_showRightArrow =
_controller.position.maxScrollExtent != _controller.position.pixels;
}); });
} }

View File

@ -11,13 +11,15 @@ typedef UpdateFieldNotifiedValue = Either<Unit, FlowyError>;
class CellListener { class CellListener {
final String rowId; final String rowId;
final String fieldId; final String fieldId;
PublishNotifier<UpdateFieldNotifiedValue>? _updateCellNotifier = PublishNotifier(); PublishNotifier<UpdateFieldNotifiedValue>? _updateCellNotifier =
PublishNotifier();
GridNotificationListener? _listener; GridNotificationListener? _listener;
CellListener({required this.rowId, required this.fieldId}); CellListener({required this.rowId, required this.fieldId});
void start({required void Function(UpdateFieldNotifiedValue) onCellChanged}) { void start({required void Function(UpdateFieldNotifiedValue) onCellChanged}) {
_updateCellNotifier?.addPublishListener(onCellChanged); _updateCellNotifier?.addPublishListener(onCellChanged);
_listener = GridNotificationListener(objectId: "$rowId:$fieldId", handler: _handler); _listener = GridNotificationListener(
objectId: "$rowId:$fieldId", handler: _handler);
} }
void _handler(GridNotification ty, Either<Uint8List, FlowyError> result) { void _handler(GridNotification ty, Either<Uint8List, FlowyError> result) {

View File

@ -33,10 +33,17 @@ class GridCellCache {
required this.gridId, required this.gridId,
}); });
void remove(String fieldId) { void removeCellWithFieldId(String fieldId) {
_cellDataByFieldId.remove(fieldId); _cellDataByFieldId.remove(fieldId);
} }
void remove(GridCellCacheKey key) {
var map = _cellDataByFieldId[key.fieldId];
if (map != null) {
map.remove(key.rowId);
}
}
void insert<T extends GridCell>(GridCellCacheKey key, T value) { void insert<T extends GridCell>(GridCellCacheKey key, T value) {
var map = _cellDataByFieldId[key.fieldId]; var map = _cellDataByFieldId[key.fieldId];
if (map == null) { if (map == null) {

View File

@ -24,7 +24,8 @@ class GridCellDataLoader<T> {
Future<T?> loadData() { Future<T?> loadData() {
final fut = service.getCell(cellId: cellId); final fut = service.getCell(cellId: cellId);
return fut.then( return fut.then(
(result) => result.fold((GridCellPB cell) { (result) => result.fold(
(GridCellPB cell) {
try { try {
return parser.parserData(cell.data); return parser.parserData(cell.data);
} catch (e, s) { } catch (e, s) {
@ -32,10 +33,12 @@ class GridCellDataLoader<T> {
Log.error('Stack trace \n $s'); Log.error('Stack trace \n $s');
return null; return null;
} }
}, (err) { },
(err) {
Log.error(err); Log.error(err);
return null; return null;
}), },
),
); );
} }
} }
@ -58,7 +61,8 @@ class DateCellDataParser implements IGridCellDataParser<DateCellDataPB> {
} }
} }
class SelectOptionCellDataParser implements IGridCellDataParser<SelectOptionCellDataPB> { class SelectOptionCellDataParser
implements IGridCellDataParser<SelectOptionCellDataPB> {
@override @override
SelectOptionCellDataPB? parserData(List<int> data) { SelectOptionCellDataPB? parserData(List<int> data) {
if (data.isEmpty) { if (data.isEmpty) {

View File

@ -71,6 +71,6 @@ class GridCellIdentifier with _$GridCellIdentifier {
FieldType get fieldType => field.fieldType; FieldType get fieldType => field.fieldType;
ValueKey key() { ValueKey key() {
return ValueKey(rowId + fieldId + "${field.fieldType}"); return ValueKey("$rowId$fieldId${field.fieldType}");
} }
} }

View File

@ -190,7 +190,10 @@ class IGridCellController<T, D> extends Equatable {
/// cell display: $12 /// cell display: $12
_cellListener?.start(onCellChanged: (result) { _cellListener?.start(onCellChanged: (result) {
result.fold( result.fold(
(_) => _loadData(), (_) {
_cellsCache.remove(_cacheKey);
_loadData();
},
(err) => Log.error(err), (err) => Log.error(err),
); );
}); });
@ -279,8 +282,8 @@ class IGridCellController<T, D> extends Equatable {
_loadDataOperation?.cancel(); _loadDataOperation?.cancel();
_loadDataOperation = Timer(const Duration(milliseconds: 10), () { _loadDataOperation = Timer(const Duration(milliseconds: 10), () {
_cellDataLoader.loadData().then((data) { _cellDataLoader.loadData().then((data) {
_cellDataNotifier?.value = data;
_cellsCache.insert(_cacheKey, GridCell(object: data)); _cellsCache.insert(_cacheKey, GridCell(object: data));
_cellDataNotifier?.value = data;
}); });
}); });
} }

View File

@ -119,13 +119,13 @@ class DateCalBloc extends Bloc<DateCalEvent, DateCalState> {
} }
String timeFormatPrompt(FlowyError error) { String timeFormatPrompt(FlowyError error) {
String msg = LocaleKeys.grid_field_invalidTimeFormat.tr() + ". "; String msg = "${LocaleKeys.grid_field_invalidTimeFormat.tr()}. ";
switch (state.dateTypeOptionPB.timeFormat) { switch (state.dateTypeOptionPB.timeFormat) {
case TimeFormat.TwelveHour: case TimeFormat.TwelveHour:
msg = msg + "e.g. 01: 00 AM"; msg = "${msg}e.g. 01: 00 AM";
break; break;
case TimeFormat.TwentyFourHour: case TimeFormat.TwentyFourHour:
msg = msg + "e.g. 13: 00"; msg = "${msg}e.g. 13: 00";
break; break;
default: default:
break; break;

View File

@ -79,7 +79,7 @@ class DateCellState with _$DateCellState {
String _dateStrFromCellData(DateCellDataPB? cellData) { String _dateStrFromCellData(DateCellDataPB? cellData) {
String dateStr = ""; String dateStr = "";
if (cellData != null) { if (cellData != null) {
dateStr = cellData.date + " " + cellData.time; dateStr = "${cellData.date} ${cellData.time}";
} }
return dateStr; return dateStr;
} }

View File

@ -1,12 +1,14 @@
import 'dart:async'; import 'dart:async';
import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart';
import 'package:collection/collection.dart';
import 'package:dartz/dartz.dart'; import 'package:dartz/dartz.dart';
import 'package:flowy_sdk/log.dart'; import 'package:flowy_sdk/log.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/select_option.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/select_option.pb.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart';
import 'select_option_service.dart'; import 'select_option_service.dart';
import 'package:collection/collection.dart';
part 'select_option_editor_bloc.freezed.dart'; part 'select_option_editor_bloc.freezed.dart';

View File

@ -46,7 +46,8 @@ class GridDataController {
GridDataController({required ViewPB view}) GridDataController({required ViewPB view})
: gridId = view.id, : gridId = view.id,
_blocks = LinkedHashMap.new(), // ignore: prefer_collection_literals
_blocks = LinkedHashMap(),
_gridFFIService = GridFFIService(gridId: view.id), _gridFFIService = GridFFIService(gridId: view.id),
fieldCache = GridFieldCache(gridId: view.id); fieldCache = GridFieldCache(gridId: view.id);

View File

@ -27,10 +27,18 @@ class GridFFIService {
return GridEventCreateTableRow(payload).send(); return GridEventCreateTableRow(payload).send();
} }
Future<Either<RowPB, FlowyError>> createBoardCard(String groupId) { Future<Either<RowPB, FlowyError>> createBoardCard(
String groupId,
String? startRowId,
) {
CreateBoardCardPayloadPB payload = CreateBoardCardPayloadPB.create() CreateBoardCardPayloadPB payload = CreateBoardCardPayloadPB.create()
..gridId = gridId ..gridId = gridId
..groupId = groupId; ..groupId = groupId;
if (startRowId != null) {
payload.startRowId = startRowId;
}
return GridEventCreateBoardCard(payload).send(); return GridEventCreateBoardCard(payload).send();
} }

View File

@ -52,7 +52,8 @@ class GridRowCache {
// //
notifier.onRowFieldsChanged(() => _rowChangeReasonNotifier notifier.onRowFieldsChanged(() => _rowChangeReasonNotifier
.receive(const RowsChangedReason.fieldDidChange())); .receive(const RowsChangedReason.fieldDidChange()));
notifier.onRowFieldChanged((field) => _cellCache.remove(field.id)); notifier.onRowFieldChanged(
(field) => _cellCache.removeCellWithFieldId(field.id));
_rowInfos = block.rows.map((rowPB) => buildGridRow(rowPB)).toList(); _rowInfos = block.rows.map((rowPB) => buildGridRow(rowPB)).toList();
} }
@ -209,7 +210,8 @@ class GridRowCache {
} }
GridCellMap _makeGridCells(String rowId, RowPB? row) { GridCellMap _makeGridCells(String rowId, RowPB? row) {
var cellDataMap = GridCellMap.new(); // ignore: prefer_collection_literals
var cellDataMap = GridCellMap();
for (final field in _fieldNotifier.fields) { for (final field in _fieldNotifier.fields) {
if (field.visibility) { if (field.visibility) {
cellDataMap[field.id] = GridCellIdentifier( cellDataMap[field.id] = GridCellIdentifier(

View File

@ -190,12 +190,12 @@ class CellAccessoryContainer extends StatelessWidget {
), ),
); );
return GestureDetector( return GestureDetector(
child: hover,
behavior: HitTestBehavior.opaque, behavior: HitTestBehavior.opaque,
onTap: () => accessory.onTap(), onTap: () => accessory.onTap(),
child: hover,
); );
}).toList(); }).toList();
return Wrap(children: children, spacing: 6); return Wrap(spacing: 6, children: children);
} }
} }

View File

@ -44,8 +44,8 @@ class CellContainer extends StatelessWidget {
if (accessories.isNotEmpty) { if (accessories.isNotEmpty) {
container = _GridCellEnterRegion( container = _GridCellEnterRegion(
child: container,
accessories: accessories, accessories: accessories,
child: container,
); );
} }
} }

View File

@ -297,9 +297,8 @@ class _DateTypeOptionButton extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = context.watch<AppTheme>(); final theme = context.watch<AppTheme>();
final title = LocaleKeys.grid_field_dateFormat.tr() + final title =
" &" + "${LocaleKeys.grid_field_dateFormat.tr()} &${LocaleKeys.grid_field_timeFormat.tr()}";
LocaleKeys.grid_field_timeFormat.tr();
return BlocSelector<DateCalBloc, DateCalState, DateTypeOptionPB>( return BlocSelector<DateCalBloc, DateCalState, DateTypeOptionPB>(
selector: (state) => state.dateTypeOptionPB, selector: (state) => state.dateTypeOptionPB,
builder: (context, dateTypeOptionPB) { builder: (context, dateTypeOptionPB) {
@ -406,8 +405,8 @@ class _CalDateTimeSettingState extends State<_CalDateTimeSetting> {
overlayIdentifier = child.toString(); overlayIdentifier = child.toString();
FlowyOverlay.of(context).insertWithAnchor( FlowyOverlay.of(context).insertWithAnchor(
widget: OverlayContainer( widget: OverlayContainer(
child: child,
constraints: BoxConstraints.loose(const Size(460, 440)), constraints: BoxConstraints.loose(const Size(460, 440)),
child: child,
), ),
identifier: overlayIdentifier!, identifier: overlayIdentifier!,
anchorContext: context, anchorContext: context,

View File

@ -91,8 +91,11 @@ class SelectOptionTag extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ChoiceChip( return ChoiceChip(
pressElevation: 1, pressElevation: 1,
label: label: FlowyText.medium(
FlowyText.medium(name, fontSize: 12, overflow: TextOverflow.ellipsis), name,
fontSize: 12,
overflow: TextOverflow.clip,
),
selectedColor: color, selectedColor: color,
backgroundColor: color, backgroundColor: color,
labelPadding: const EdgeInsets.symmetric(horizontal: 6), labelPadding: const EdgeInsets.symmetric(horizontal: 6),

View File

@ -178,14 +178,14 @@ class _SelectOptionWrapState extends State<SelectOptionWrap> {
child = Align( child = Align(
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
child: Wrap( child: Wrap(
spacing: 4,
runSpacing: 2,
children: widget.selectOptions children: widget.selectOptions
.map((option) => SelectOptionTag.fromOption( .map((option) => SelectOptionTag.fromOption(
context: context, context: context,
option: option, option: option,
)) ))
.toList(), .toList(),
spacing: 4,
runSpacing: 2,
), ),
); );
} }

View File

@ -75,8 +75,8 @@ class SelectOptionCellEditor extends StatelessWidget with FlowyOverlayDelegate {
// //
FlowyOverlay.of(context).insertWithAnchor( FlowyOverlay.of(context).insertWithAnchor(
widget: OverlayContainer( widget: OverlayContainer(
child: SizedBox(width: _editorPannelWidth, child: editor),
constraints: BoxConstraints.loose(const Size(_editorPannelWidth, 300)), constraints: BoxConstraints.loose(const Size(_editorPannelWidth, 300)),
child: SizedBox(width: _editorPannelWidth, child: editor),
), ),
identifier: SelectOptionCellEditor.identifier(), identifier: SelectOptionCellEditor.identifier(),
anchorContext: context, anchorContext: context,

View File

@ -108,7 +108,7 @@ class SelectOptionTextField extends StatelessWidget {
child: SingleChildScrollView( child: SingleChildScrollView(
controller: sc, controller: sc,
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
child: Wrap(children: children, spacing: 4), child: Wrap(spacing: 4, children: children),
), ),
); );
} }

View File

@ -30,11 +30,11 @@ class URLCellEditor extends StatefulWidget with FlowyOverlayDelegate {
// //
FlowyOverlay.of(context).insertWithAnchor( FlowyOverlay.of(context).insertWithAnchor(
widget: OverlayContainer( widget: OverlayContainer(
constraints: BoxConstraints.loose(const Size(300, 160)),
child: SizedBox( child: SizedBox(
width: 200, width: 200,
child: Padding(padding: const EdgeInsets.all(6), child: editor), child: Padding(padding: const EdgeInsets.all(6), child: editor),
), ),
constraints: BoxConstraints.loose(const Size(300, 160)),
), ),
identifier: URLCellEditor.identifier(), identifier: URLCellEditor.identifier(),
anchorContext: context, anchorContext: context,

View File

@ -1,7 +1,9 @@
import 'package:app_flowy/generated/locale_keys.g.dart';
import 'package:app_flowy/plugins/grid/application/field/field_cache.dart'; import 'package:app_flowy/plugins/grid/application/field/field_cache.dart';
import 'package:app_flowy/plugins/grid/application/field/type_option/type_option_context.dart'; import 'package:app_flowy/plugins/grid/application/field/type_option/type_option_context.dart';
import 'package:app_flowy/startup/startup.dart'; import 'package:app_flowy/startup/startup.dart';
import 'package:app_flowy/plugins/grid/application/prelude.dart'; import 'package:app_flowy/plugins/grid/application/prelude.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:appflowy_popover/popover.dart'; import 'package:appflowy_popover/popover.dart';
import 'package:flowy_infra/image.dart'; import 'package:flowy_infra/image.dart';
import 'package:flowy_infra/theme.dart'; import 'package:flowy_infra/theme.dart';
@ -178,7 +180,10 @@ class CreateFieldButton extends StatelessWidget {
triggerActions: PopoverTriggerActionFlags.click, triggerActions: PopoverTriggerActionFlags.click,
direction: PopoverDirection.bottomWithRightAligned, direction: PopoverDirection.bottomWithRightAligned,
child: FlowyButton( child: FlowyButton(
text: const FlowyText.medium('New column', fontSize: 12), text: FlowyText.medium(
LocaleKeys.grid_field_newColumn.tr(),
fontSize: 12,
),
hoverColor: theme.shader6, hoverColor: theme.shader6,
onTap: () {}, onTap: () {},
leftIcon: svgWidget("home/add"), leftIcon: svgWidget("home/add"),

View File

@ -101,10 +101,10 @@ class NumberTypeOptionWidget extends TypeOptionWidget {
} }
} }
typedef _SelectNumberFormatCallback = Function(NumberFormat format); typedef SelectNumberFormatCallback = Function(NumberFormat format);
class NumberFormatList extends StatelessWidget { class NumberFormatList extends StatelessWidget {
final _SelectNumberFormatCallback onSelected; final SelectNumberFormatCallback onSelected;
final NumberFormat selectedFormat; final NumberFormat selectedFormat;
const NumberFormatList( const NumberFormatList(
{required this.selectedFormat, required this.onSelected, Key? key}) {required this.selectedFormat, required this.onSelected, Key? key})

View File

@ -14,6 +14,8 @@ import '../cell/cell_accessory.dart';
import '../cell/cell_container.dart'; import '../cell/cell_container.dart';
import '../cell/prelude.dart'; import '../cell/prelude.dart';
import 'row_action_sheet.dart'; import 'row_action_sheet.dart';
import "package:app_flowy/generated/locale_keys.g.dart";
import 'package:easy_localization/easy_localization.dart';
class GridRowWidget extends StatefulWidget { class GridRowWidget extends StatefulWidget {
final RowInfo rowInfo; final RowInfo rowInfo;
@ -122,10 +124,13 @@ class _InsertRowButton extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = context.watch<AppTheme>(); final theme = context.watch<AppTheme>();
return FlowyIconButton( return FlowyIconButton(
tooltipText: LocaleKeys.tooltip_addNewRow.tr(),
hoverColor: theme.hover, hoverColor: theme.hover,
width: 20, width: 20,
height: 30, height: 30,
onPressed: () => context.read<RowBloc>().add(const RowEvent.createRow()), onPressed: () => context.read<RowBloc>().add(
const RowEvent.createRow(),
),
iconPadding: const EdgeInsets.all(3), iconPadding: const EdgeInsets.all(3),
icon: svgWidget("home/add"), icon: svgWidget("home/add"),
); );
@ -139,6 +144,7 @@ class _DeleteRowButton extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = context.watch<AppTheme>(); final theme = context.watch<AppTheme>();
return FlowyIconButton( return FlowyIconButton(
tooltipText: LocaleKeys.tooltip_openMenu.tr(),
hoverColor: theme.hover, hoverColor: theme.hover,
width: 20, width: 20,
height: 30, height: 30,
@ -184,7 +190,6 @@ class RowContent extends StatelessWidget {
return CellContainer( return CellContainer(
width: cellId.field.width.toDouble(), width: cellId.field.width.toDouble(),
child: child,
rowStateNotifier: rowStateNotifier:
Provider.of<RegionStateNotifier>(context, listen: false), Provider.of<RegionStateNotifier>(context, listen: false),
accessoryBuilder: (buildContext) { accessoryBuilder: (buildContext) {
@ -202,6 +207,7 @@ class RowContent extends StatelessWidget {
} }
return accessories; return accessories;
}, },
child: child,
); );
}, },
).toList(); ).toList();

View File

@ -59,8 +59,8 @@ class GridRowActionSheet extends StatelessWidget {
}) { }) {
FlowyOverlay.of(overlayContext).insertWithAnchor( FlowyOverlay.of(overlayContext).insertWithAnchor(
widget: OverlayContainer( widget: OverlayContainer(
child: this,
constraints: BoxConstraints.loose(const Size(140, 200)), constraints: BoxConstraints.loose(const Size(140, 200)),
child: this,
), ),
identifier: GridRowActionSheet.identifier(), identifier: GridRowActionSheet.identifier(),
anchorContext: overlayContext, anchorContext: overlayContext,

View File

@ -5,8 +5,10 @@ import 'package:app_flowy/plugins/grid/application/row/row_detail_bloc.dart';
import 'package:flowy_infra/image.dart'; import 'package:flowy_infra/image.dart';
import 'package:flowy_infra/theme.dart'; import 'package:flowy_infra/theme.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.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/icon_button.dart'; import 'package:flowy_infra_ui/style_widget/icon_button.dart';
import 'package:flowy_infra_ui/style_widget/scrolling/styled_scroll_bar.dart'; import 'package:flowy_infra_ui/style_widget/scrolling/styled_scroll_bar.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_infra_ui/widget/spacing.dart'; import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:app_flowy/generated/locale_keys.g.dart'; import 'package:app_flowy/generated/locale_keys.g.dart';
@ -61,7 +63,12 @@ class _RowDetailPageState extends State<RowDetailPage> {
children: const [Spacer(), _CloseButton()], children: const [Spacer(), _CloseButton()],
), ),
), ),
Expanded(child: _PropertyList(cellBuilder: widget.cellBuilder)), Expanded(
child: _PropertyList(
cellBuilder: widget.cellBuilder,
viewId: widget.dataController.rowInfo.gridId,
),
),
], ],
), ),
), ),
@ -88,9 +95,11 @@ class _CloseButton extends StatelessWidget {
} }
class _PropertyList extends StatelessWidget { class _PropertyList extends StatelessWidget {
final String viewId;
final GridCellBuilder cellBuilder; final GridCellBuilder cellBuilder;
final ScrollController _scrollController; final ScrollController _scrollController;
_PropertyList({ _PropertyList({
required this.viewId,
required this.cellBuilder, required this.cellBuilder,
Key? key, Key? key,
}) : _scrollController = ScrollController(), }) : _scrollController = ScrollController(),
@ -101,7 +110,10 @@ class _PropertyList extends StatelessWidget {
return BlocBuilder<RowDetailBloc, RowDetailState>( return BlocBuilder<RowDetailBloc, RowDetailState>(
buildWhen: (previous, current) => previous.gridCells != current.gridCells, buildWhen: (previous, current) => previous.gridCells != current.gridCells,
builder: (context, state) { builder: (context, state) {
return ScrollbarListStack( return Column(
children: [
Expanded(
child: ScrollbarListStack(
axis: Axis.vertical, axis: Axis.vertical,
controller: _scrollController, controller: _scrollController,
barSize: GridSize.scrollBarSize, barSize: GridSize.scrollBarSize,
@ -118,6 +130,46 @@ class _PropertyList extends StatelessWidget {
return const VSpace(2); return const VSpace(2);
}, },
), ),
),
),
_CreateFieldButton(viewId: viewId),
],
);
},
);
}
}
class _CreateFieldButton extends StatelessWidget {
final String viewId;
const _CreateFieldButton({required this.viewId, Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final theme = context.read<AppTheme>();
return Popover(
triggerActions: PopoverTriggerActionFlags.click,
child: SizedBox(
height: 40,
child: FlowyButton(
text: FlowyText.medium(
LocaleKeys.grid_field_newColumn.tr(),
fontSize: 12,
),
hoverColor: theme.shader6,
onTap: () {},
leftIcon: svgWidget("home/add"),
),
),
popupBuilder: (BuildContext context) {
return OverlayContainer(
constraints: BoxConstraints.loose(const Size(240, 200)),
child: FieldEditor(
gridId: viewId,
fieldName: "",
typeOptionLoader: NewFieldTypeOptionLoader(gridId: viewId),
),
); );
}, },
); );
@ -150,9 +202,9 @@ class _RowDetailCellState extends State<_RowDetailCell> {
behavior: HitTestBehavior.translucent, behavior: HitTestBehavior.translucent,
onTap: () => cell.beginFocus.notify(), onTap: () => cell.beginFocus.notify(),
child: AccessoryHover( child: AccessoryHover(
child: cell,
contentPadding: contentPadding:
const EdgeInsets.symmetric(horizontal: 10, vertical: 12), const EdgeInsets.symmetric(horizontal: 10, vertical: 12),
child: cell,
), ),
); );

View File

@ -1,5 +1,4 @@
import 'package:app_flowy/plugins/grid/application/setting/setting_bloc.dart'; import 'package:app_flowy/plugins/grid/application/setting/setting_bloc.dart';
import 'package:appflowy_popover/popover.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/image.dart'; import 'package:flowy_infra/image.dart';
import 'package:flowy_infra/theme.dart'; import 'package:flowy_infra/theme.dart';

View File

@ -91,12 +91,12 @@ class _TrashPageState extends State<TrashPage> {
builder: (context, state) { builder: (context, state) {
return SizedBox.expand( return SizedBox.expand(
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [ children: [
_renderTopBar(context, theme, state), _renderTopBar(context, theme, state),
const VSpace(32), const VSpace(32),
_renderTrashList(context, state), _renderTrashList(context, state),
], ],
mainAxisAlignment: MainAxisAlignment.start,
).padding(horizontal: horizontalPadding, vertical: 48), ).padding(horizontal: horizontalPadding, vertical: 48),
); );
}, },

View File

@ -20,11 +20,10 @@ class InitAppWidgetTask extends LaunchTask {
final setting = await UserSettingsService().getAppearanceSettings(); final setting = await UserSettingsService().getAppearanceSettings();
final settingModel = AppearanceSettingModel(setting); final settingModel = AppearanceSettingModel(setting);
final app = ApplicationWidget( final app = ApplicationWidget(
child: widget,
settingModel: settingModel, settingModel: settingModel,
child: widget,
); );
BlocOverrides.runZoned( Bloc.observer = ApplicationBlocObserver();
() {
runApp( runApp(
EasyLocalization( EasyLocalization(
supportedLocales: const [ supportedLocales: const [
@ -51,9 +50,6 @@ class InitAppWidgetTask extends LaunchTask {
child: app, child: app,
), ),
); );
},
blocObserver: ApplicationBlocObserver(),
);
return Future(() => {}); return Future(() => {});
} }

View File

@ -28,16 +28,19 @@ class AuthRouter {
); );
} }
void pushHomeScreen(BuildContext context, UserProfilePB profile, CurrentWorkspaceSettingPB workspaceSetting) { void pushHomeScreen(BuildContext context, UserProfilePB profile,
CurrentWorkspaceSettingPB workspaceSetting) {
Navigator.push( Navigator.push(
context, context,
PageRoutes.fade(() => HomeScreen(profile, workspaceSetting), RouteDurations.slow.inMilliseconds * .001), PageRoutes.fade(() => HomeScreen(profile, workspaceSetting),
RouteDurations.slow.inMilliseconds * .001),
); );
} }
} }
class SplashRoute { class SplashRoute {
Future<void> pushWelcomeScreen(BuildContext context, UserProfilePB userProfile) async { Future<void> pushWelcomeScreen(
BuildContext context, UserProfilePB userProfile) async {
final screen = WelcomeScreen(userProfile: userProfile); final screen = WelcomeScreen(userProfile: userProfile);
final workspaceId = await Navigator.of(context).push( final workspaceId = await Navigator.of(context).push(
PageRoutes.fade( PageRoutes.fade(
@ -46,20 +49,24 @@ class SplashRoute {
), ),
); );
// ignore: use_build_context_synchronously
pushHomeScreen(context, userProfile, workspaceId); pushHomeScreen(context, userProfile, workspaceId);
} }
void pushHomeScreen(BuildContext context, UserProfilePB userProfile, CurrentWorkspaceSettingPB workspaceSetting) { void pushHomeScreen(BuildContext context, UserProfilePB userProfile,
CurrentWorkspaceSettingPB workspaceSetting) {
Navigator.push( Navigator.push(
context, context,
PageRoutes.fade(() => HomeScreen(userProfile, workspaceSetting), RouteDurations.slow.inMilliseconds * .001), PageRoutes.fade(() => HomeScreen(userProfile, workspaceSetting),
RouteDurations.slow.inMilliseconds * .001),
); );
} }
void pushSignInScreen(BuildContext context) { void pushSignInScreen(BuildContext context) {
Navigator.push( Navigator.push(
context, context,
PageRoutes.fade(() => SignInScreen(router: getIt<AuthRouter>()), RouteDurations.slow.inMilliseconds * .001), PageRoutes.fade(() => SignInScreen(router: getIt<AuthRouter>()),
RouteDurations.slow.inMilliseconds * .001),
); );
} }

View File

@ -94,6 +94,7 @@ class SignUpPrompt extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = context.watch<AppTheme>(); final theme = context.watch<AppTheme>();
return Row( return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Text(LocaleKeys.signIn_dontHaveAnAccount.tr(), style: TextStyle(color: theme.shader3, fontSize: 12)), Text(LocaleKeys.signIn_dontHaveAnAccount.tr(), style: TextStyle(color: theme.shader3, fontSize: 12)),
TextButton( TextButton(
@ -107,7 +108,6 @@ class SignUpPrompt extends StatelessWidget {
), ),
), ),
], ],
mainAxisAlignment: MainAxisAlignment.center,
); );
} }
} }

View File

@ -86,6 +86,7 @@ class SignUpPrompt extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = context.watch<AppTheme>(); final theme = context.watch<AppTheme>();
return Row( return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Text( Text(
LocaleKeys.signUp_alreadyHaveAnAccount.tr(), LocaleKeys.signUp_alreadyHaveAnAccount.tr(),
@ -97,7 +98,6 @@ class SignUpPrompt extends StatelessWidget {
child: Text(LocaleKeys.signIn_buttonText.tr(), style: TextStyle(color: theme.main1)), child: Text(LocaleKeys.signIn_buttonText.tr(), style: TextStyle(color: theme.main1)),
), ),
], ],
mainAxisAlignment: MainAxisAlignment.center,
); );
} }
} }

View File

@ -399,8 +399,8 @@ class AutolinkExtensionSyntax extends InlineSyntax {
} }
} }
class _DelimiterRun { class DelimiterRun {
_DelimiterRun._( DelimiterRun._(
{this.char, {this.char,
this.length, this.length,
this.isLeftFlanking, this.isLeftFlanking,
@ -420,8 +420,7 @@ class _DelimiterRun {
final bool? isFollowedByPunctuation; final bool? isFollowedByPunctuation;
// ignore: prefer_constructors_over_static_methods // ignore: prefer_constructors_over_static_methods
static _DelimiterRun? tryParse( static DelimiterRun? tryParse(InlineParser parser, int runStart, int runEnd) {
InlineParser parser, int runStart, int runEnd) {
bool leftFlanking, bool leftFlanking,
rightFlanking, rightFlanking,
precededByPunctuation, precededByPunctuation,
@ -466,7 +465,7 @@ class _DelimiterRun {
return null; return null;
} }
return _DelimiterRun._( return DelimiterRun._(
char: parser.charAt(runStart), char: parser.charAt(runStart),
length: runEnd - runStart + 1, length: runEnd - runStart + 1,
isLeftFlanking: leftFlanking, isLeftFlanking: leftFlanking,
@ -516,7 +515,7 @@ class TagSyntax extends InlineSyntax {
return true; return true;
} }
final delimiterRun = _DelimiterRun.tryParse(parser, matchStart, matchEnd); final delimiterRun = DelimiterRun.tryParse(parser, matchStart, matchEnd);
if (delimiterRun != null && delimiterRun.canOpen) { if (delimiterRun != null && delimiterRun.canOpen) {
parser.openTag(TagState(parser.pos, matchEnd + 1, this, delimiterRun)); parser.openTag(TagState(parser.pos, matchEnd + 1, this, delimiterRun));
return true; return true;
@ -531,7 +530,7 @@ class TagSyntax extends InlineSyntax {
final matchStart = parser.pos; final matchStart = parser.pos;
final matchEnd = parser.pos + runLength - 1; final matchEnd = parser.pos + runLength - 1;
final openingRunLength = state.endPos - state.startPos; final openingRunLength = state.endPos - state.startPos;
final delimiterRun = _DelimiterRun.tryParse(parser, matchStart, matchEnd); final delimiterRun = DelimiterRun.tryParse(parser, matchStart, matchEnd);
if (openingRunLength == 1 && runLength == 1) { if (openingRunLength == 1 && runLength == 1) {
parser.addNode(Element('em', state.children)); parser.addNode(Element('em', state.children));
@ -579,7 +578,7 @@ class StrikethroughSyntax extends TagSyntax {
final runLength = match.group(0)!.length; final runLength = match.group(0)!.length;
final matchStart = parser.pos; final matchStart = parser.pos;
final matchEnd = parser.pos + runLength - 1; final matchEnd = parser.pos + runLength - 1;
final delimiterRun = _DelimiterRun.tryParse(parser, matchStart, matchEnd)!; final delimiterRun = DelimiterRun.tryParse(parser, matchStart, matchEnd)!;
if (!delimiterRun.isRightFlanking!) { if (!delimiterRun.isRightFlanking!) {
return false; return false;
} }
@ -1170,7 +1169,7 @@ class TagState {
/// The children of this node. Will be `null` for text nodes. /// The children of this node. Will be `null` for text nodes.
final List<Node> children; final List<Node> children;
final _DelimiterRun? openingDelimiterRun; final DelimiterRun? openingDelimiterRun;
/// Attempts to close this tag by matching the current text against its end /// Attempts to close this tag by matching the current text against its end
/// pattern. /// pattern.
@ -1193,7 +1192,7 @@ class TagState {
final closingMatchStart = parser.pos; final closingMatchStart = parser.pos;
final closingMatchEnd = parser.pos + runLength - 1; final closingMatchEnd = parser.pos + runLength - 1;
final closingDelimiterRun = final closingDelimiterRun =
_DelimiterRun.tryParse(parser, closingMatchStart, closingMatchEnd); DelimiterRun.tryParse(parser, closingMatchStart, closingMatchEnd);
if (closingDelimiterRun != null && closingDelimiterRun.canClose) { if (closingDelimiterRun != null && closingDelimiterRun.canClose) {
// Emphasis rules #9 and #10: // Emphasis rules #9 and #10:
final oneRunOpensAndCloses = final oneRunOpensAndCloses =

View File

@ -1,5 +1,7 @@
import 'package:app_flowy/startup/plugin/plugin.dart'; import 'package:app_flowy/startup/plugin/plugin.dart';
import 'package:app_flowy/workspace/application/home/home_bloc.dart'; import 'package:app_flowy/workspace/application/home/home_bloc.dart';
import 'package:app_flowy/workspace/presentation/home/hotkeys.dart';
import 'package:app_flowy/workspace/application/view/view_ext.dart'; import 'package:app_flowy/workspace/application/view/view_ext.dart';
import 'package:app_flowy/workspace/presentation/widgets/edit_panel/panel_animation.dart'; import 'package:app_flowy/workspace/presentation/widgets/edit_panel/panel_animation.dart';
import 'package:app_flowy/workspace/presentation/widgets/float_bubble/question_bubble.dart'; import 'package:app_flowy/workspace/presentation/widgets/float_bubble/question_bubble.dart';
@ -54,6 +56,7 @@ class _HomeScreenState extends State<HomeScreen> {
}, },
), ),
], ],
child: HomeHotKeys(
child: Scaffold( child: Scaffold(
body: BlocListener<HomeBloc, HomeState>( body: BlocListener<HomeBloc, HomeState>(
listenWhen: (p, c) => p.unauthorized != c.unauthorized, listenWhen: (p, c) => p.unauthorized != c.unauthorized,
@ -80,7 +83,7 @@ class _HomeScreenState extends State<HomeScreen> {
}, },
), ),
), ),
), )),
); );
} }
@ -145,6 +148,7 @@ class _HomeScreenState extends State<HomeScreen> {
return FocusTraversalGroup(child: RepaintBoundary(child: homeMenu)); return FocusTraversalGroup(child: RepaintBoundary(child: homeMenu));
} }
Widget _buildEditPanel( Widget _buildEditPanel(
{required HomeState homeState, {required HomeState homeState,
required BuildContext context, required BuildContext context,

View File

@ -58,10 +58,10 @@ class FadingIndexedStack extends StatefulWidget {
}) : super(key: key); }) : super(key: key);
@override @override
_FadingIndexedStackState createState() => _FadingIndexedStackState(); FadingIndexedStackState createState() => FadingIndexedStackState();
} }
class _FadingIndexedStackState extends State<FadingIndexedStack> { class FadingIndexedStackState extends State<FadingIndexedStack> {
double _targetOpacity = 1; double _targetOpacity = 1;
@override @override

View File

@ -0,0 +1,32 @@
import 'dart:io';
import 'package:app_flowy/startup/startup.dart';
import 'package:app_flowy/workspace/application/home/home_bloc.dart';
import 'package:app_flowy/workspace/presentation/home/home_stack.dart';
import 'package:flutter/material.dart';
import 'package:hotkey_manager/hotkey_manager.dart';
import 'package:provider/provider.dart';
class HomeHotKeys extends StatelessWidget {
final Widget child;
const HomeHotKeys({required this.child, Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
HotKey hotKey = HotKey(
KeyCode.backslash,
modifiers: [Platform.isMacOS ? KeyModifier.meta : KeyModifier.control],
// Set hotkey scope (default is HotKeyScope.system)
scope: HotKeyScope.inapp, // Set as inapp-wide hotkey.
);
hotKeyManager.register(
hotKey,
keyDownHandler: (hotKey) {
context.read<HomeBloc>().add(const HomeEvent.collapseMenu());
getIt<HomeStackManager>().collapsedNotifier.value =
!getIt<HomeStackManager>().collapsedNotifier.currentValue!;
},
);
return child;
}
}

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