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 apt-get update
sudo apt-get install -y dart curl build-essential libsqlite3-dev libssl-dev clang cmake ninja-build pkg-config libgtk-3-dev
sudo apt-get install keybinder-3.0
elif [ "$RUNNER_OS" == "macOS" ]; then
echo 'do nothing'
fi

View File

@ -4,10 +4,14 @@ on:
push:
branches:
- "main"
paths:
- "frontend/app_flowy/packages/appflowy_editor"
pull_request:
branches:
- "main"
paths:
- "frontend/app_flowy/packages/appflowy_editor"
env:
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 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 keybinder-3.0
source $HOME/.cargo/env
cargo install --force cargo-make
cargo install --force duckscript_cli

View File

@ -1,6 +1,29 @@
# 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
- Upgrade to Flutter 3.0
- Native support for M1 chip
@ -12,12 +35,12 @@
- 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
- Row record open as a page
- Auto resize the height of the row in the grid
- Support more number formats
- Search column options, supporting Single select, Multi-select, and number format
- 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)
@ -27,7 +50,7 @@
- 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
- Insert / delete rows
@ -35,16 +58,16 @@
- Edit property
![](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
New features
- 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
- 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
New features

View File

@ -16,6 +16,18 @@
},
"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",
"request": "attach",
@ -48,6 +60,21 @@
},
"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)",
"request": "launch",
@ -59,6 +86,17 @@
},
"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)",
"request": "launch",

View File

@ -27,6 +27,33 @@
"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",
"type": "shell",

View File

@ -22,7 +22,7 @@ CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true
CARGO_MAKE_CRATE_FS_NAME = "dart_ffi"
CARGO_MAKE_CRATE_NAME = "dart-ffi"
LIB_NAME = "dart_ffi"
CURRENT_APP_VERSION = "0.0.4"
CURRENT_APP_VERSION = "0.0.5"
FEATURES = "flutter"
PRODUCT_NAME = "AppFlowy"
# CRATE_TYPE: https://doc.rust-lang.org/reference/linkage.html
@ -161,6 +161,11 @@ TARGET_OS = "ios"
FLUTTER_OUTPUT_DIR = "Release"
PRODUCT_EXT = "ipa"
[env.development-android]
BUILD_FLAG = "debug"
TARGET_OS = "android"
CRATE_TYPE = "cdylib"
FLUTTER_OUTPUT_DIR = "Debug"
[tasks.setup-crate-type]
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"
android {
compileSdkVersion 30
compileSdkVersion 31
ndkVersion "24.0.8215888"
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
@ -39,21 +40,26 @@ android {
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
main.jniLibs.srcDirs += 'jniLibs/'
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "com.example.app_flowy"
minSdkVersion 16
targetSdkVersion 30
minSdkVersion 19
targetSdkVersion 31
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
multiDexEnabled true
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
minifyEnabled true
shrinkResources true
signingConfig signingConfigs.debug
}
}
@ -65,4 +71,5 @@ flutter {
dependencies {
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">
<application
android:label="app_flowy"
android:icon="@mipmap/ic_launcher">
android:icon="@mipmap/ic_launcher"
android:name="${applicationName}">
<activity
android:name=".MainActivity"
android:launchMode="singleTop"

View File

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

View File

@ -1,3 +1,4 @@
org.gradle.jvmargs=-Xmx1536M
android.useAndroidX=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
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.1-all.zip
zipStoreBase=GRADLE_USER_HOME
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")
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
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",
"darkLabel": "Mode Fosc"
}
},
"sideBar": {
"openSidebar": "Open sidebar",
"closeSidebar": "Close sidebar"
}
}

View File

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

View File

@ -95,7 +95,13 @@
"tooltip": {
"lightMode": "Switch to Light 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": {
"export": {
@ -183,7 +189,8 @@
"addSelectOption": "Add an option",
"optionTitle": "Options",
"addOption": "Add option",
"editProperty": "Edit property"
"editProperty": "Edit property",
"newColumn": "New column"
},
"row": {
"duplicate": "Duplicate",
@ -215,5 +222,10 @@
"timeHintTextInTwelveHour": "12:00 AM",
"timeHintTextInTwentyFourHour": "12:00"
}
},
"board": {
"column": {
"create_new_card": "New"
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -93,8 +93,14 @@
"highlight": "高亮"
},
"tooltip": {
"lightMode": "切换到灯光模式",
"darkMode": "切换到暗模式"
"lightMode": "切换到亮色模式",
"darkMode": "切换到暗色模式"
},
"notifications": {
"export": {
"markdown": "导出笔记为Markdown文档",
"path": "Documents/flowy"
}
},
"contactsPage": {
"title": "联系人",
@ -135,11 +141,82 @@
"menu": {
"appearance": "外观",
"language": "语言",
"user": "用户",
"open": "打开设置"
},
"appearance": {
"lightLabel": "日间模式",
"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",
"timeHintTextInTwentyFourHour": "12:00"
}
},
"sideBar": {
"openSidebar": "Open sidebar",
"closeSidebar": "Close sidebar"
}
}

View File

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

View File

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

View File

@ -20,19 +20,19 @@ import 'group_controller.dart';
part 'board_bloc.freezed.dart';
class BoardBloc extends Bloc<BoardEvent, BoardState> {
final BoardDataController _dataController;
late final AFBoardDataController afBoardDataController;
final BoardDataController _gridDataController;
late final AFBoardDataController boardController;
final MoveRowFFIService _rowService;
LinkedHashMap<String, GroupController> groupControllers = LinkedHashMap.new();
LinkedHashMap<String, GroupController> groupControllers = LinkedHashMap();
GridFieldCache get fieldCache => _dataController.fieldCache;
String get gridId => _dataController.gridId;
GridFieldCache get fieldCache => _gridDataController.fieldCache;
String get gridId => _gridDataController.gridId;
BoardBloc({required ViewPB view})
: _rowService = MoveRowFFIService(gridId: view.id),
_dataController = BoardDataController(view: view),
_gridDataController = BoardDataController(view: view),
super(BoardState.initial(view.id)) {
afBoardDataController = AFBoardDataController(
boardController = AFBoardDataController(
onMoveColumn: (
fromColumnId,
fromIndex,
@ -69,31 +69,51 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
_startListening();
await _loadGrid(emit);
},
createRow: (groupId) async {
final result = await _dataController.createBoardCard(groupId);
createBottomRow: (groupId) async {
final startRowId = groupControllers[groupId]?.lastRow()?.id;
final result = await _gridDataController.createBoardCard(
groupId,
startRowId: startRowId,
);
result.fold(
(rowPB) {
emit(state.copyWith(editingRow: some(rowPB)));
},
(_) {},
(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) {
assert(state.editingRow.isSome());
state.editingRow.fold(() => null, (row) {
assert(row.id == rowId);
state.editingRow.fold(() => null, (editingRow) {
assert(editingRow.row.id == rowId);
emit(state.copyWith(editingRow: none()));
});
},
didReceiveGridUpdate: (GridPB grid) {
emit(state.copyWith(grid: Some(grid)));
},
didReceiveRows: (List<RowInfo> rowInfos) {
emit(state.copyWith(rowInfos: rowInfos));
},
didReceiveError: (FlowyError error) {
emit(state.copyWith(noneOrError: some(error)));
},
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
Future<void> close() async {
await _dataController.dispose();
await _gridDataController.dispose();
for (final controller in groupControllers.values) {
controller.dispose();
}
@ -135,7 +155,12 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
void initializeGroups(List<GroupPB> 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(
gridId: state.gridId,
group: group,
@ -147,12 +172,12 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
}
GridRowCache? getRowCache(String blockId) {
final GridBlockCache? blockCache = _dataController.blocks[blockId];
final GridBlockCache? blockCache = _gridDataController.blocks[blockId];
return blockCache?.rowCache;
}
void _startListening() {
_dataController.addListener(
_gridDataController.addListener(
onGridChanged: (grid) {
if (!isClosed) {
add(BoardEvent.didReceiveGridUpdate(grid));
@ -162,17 +187,31 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
List<AFBoardColumnData> columns = groups.map((group) {
return AFBoardColumnData(
id: group.groupId,
desc: group.desc,
items: _buildRows(group.rows),
name: group.desc,
items: _buildRows(group),
customData: group,
);
}).toList();
afBoardDataController.addColumns(columns);
boardController.addColumns(columns);
initializeGroups(groups);
add(BoardEvent.didReceiveGroups(groups));
},
onRowsChanged: (List<RowInfo> rowInfos, RowsChangedReason reason) {
add(BoardEvent.didReceiveRows(rowInfos));
onDeletedGroup: (groupIds) {
//
},
onInsertedGroup: (insertedGroups) {
//
},
onUpdatedGroup: (updatedGroups) {
//
for (final group in updatedGroups) {
final columnController =
boardController.getColumnController(group.groupId);
if (columnController != null) {
columnController.updateColumnName(group.desc);
}
}
},
onError: (err) {
Log.error(err);
@ -180,16 +219,19 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
);
}
List<AFColumnItem> _buildRows(List<RowPB> rows) {
final items = rows.map((row) {
return BoardColumnItem(row: row);
List<AFColumnItem> _buildRows(GroupPB group) {
final items = group.rows.map((row) {
return BoardColumnItem(
row: row,
fieldId: group.fieldId,
);
}).toList();
return <AFColumnItem>[...items];
}
Future<void> _loadGrid(Emitter<BoardState> emit) async {
final result = await _dataController.loadData();
final result = await _gridDataController.loadData();
result.fold(
(grid) => emit(
state.copyWith(loadingState: GridLoadingState.finish(left(unit))),
@ -203,15 +245,21 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
@freezed
class BoardEvent with _$BoardEvent {
const factory BoardEvent.initial() = InitialGrid;
const factory BoardEvent.createRow(String groupId) = _CreateRow;
const factory BoardEvent.initial() = _InitialBoard;
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.didReceiveError(FlowyError error) = _DidReceiveError;
const factory BoardEvent.didReceiveRows(List<RowInfo> rowInfos) =
_DidReceiveRows;
const factory BoardEvent.didReceiveGridUpdate(
GridPB grid,
) = _DidReceiveGridUpdate;
const factory BoardEvent.didReceiveGroups(List<GroupPB> groups) =
_DidReceiveGroups;
}
@freezed
@ -219,16 +267,16 @@ class BoardState with _$BoardState {
const factory BoardState({
required String gridId,
required Option<GridPB> grid,
required Option<RowPB> editingRow,
required List<RowInfo> rowInfos,
required List<String> groupIds,
required Option<BoardEditingRow> editingRow,
required GridLoadingState loadingState,
required Option<FlowyError> noneOrError,
}) = _BoardState;
factory BoardState.initial(String gridId) => BoardState(
rowInfos: [],
grid: none(),
gridId: gridId,
groupIds: [],
editingRow: none(),
noneOrError: none(),
loadingState: const _Loading(),
@ -268,39 +316,84 @@ class GridFieldEquatable extends Equatable {
class BoardColumnItem extends AFColumnItem {
final RowPB row;
BoardColumnItem({required this.row});
final String fieldId;
final bool requestFocus;
BoardColumnItem({
required this.row,
required this.fieldId,
this.requestFocus = false,
});
@override
String get id => row.id;
}
class CreateCardItem extends AFColumnItem {
@override
String get id => '$CreateCardItem';
}
class GroupControllerDelegateImpl extends GroupControllerDelegate {
final AFBoardDataController controller;
final void Function(String, RowPB, int?) onNewColumnItem;
GroupControllerDelegateImpl(this.controller);
GroupControllerDelegateImpl({
required this.controller,
required this.onNewColumnItem,
});
@override
void insertRow(String groupId, RowPB row, int? index) {
final item = BoardColumnItem(row: row);
void insertRow(GroupPB group, RowPB row, int? index) {
if (index != null) {
controller.insertColumnItem(groupId, index, item);
final item = BoardColumnItem(row: row, fieldId: group.fieldId);
controller.insertColumnItem(group.groupId, index, item);
} else {
controller.addColumnItem(groupId, item);
final item = BoardColumnItem(
row: row,
fieldId: group.fieldId,
);
controller.addColumnItem(group.groupId, item);
}
}
@override
void removeRow(String groupId, String rowId) {
controller.removeColumnItem(groupId, rowId);
void removeRow(GroupPB group, String rowId) {
controller.removeColumnItem(group.groupId, rowId);
}
@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:flowy_sdk/protobuf/flowy-grid/protobuf.dart';
import 'board_listener.dart';
typedef OnFieldsChanged = void Function(UnmodifiableListView<FieldPB>);
typedef OnGridChanged = void Function(GridPB);
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(
List<RowInfo>,
RowsChangedReason,
@ -23,6 +29,7 @@ class BoardDataController {
final String gridId;
final GridFFIService _gridFFIService;
final GridFieldCache fieldCache;
final BoardListener _listener;
// key: the block id
final LinkedHashMap<String, GridBlockCache> _blocks;
@ -44,16 +51,21 @@ class BoardDataController {
BoardDataController({required ViewPB view})
: gridId = view.id,
_blocks = LinkedHashMap.new(),
_listener = BoardListener(view.id),
// ignore: prefer_collection_literals
_blocks = LinkedHashMap(),
_gridFFIService = GridFFIService(gridId: view.id),
fieldCache = GridFieldCache(gridId: view.id);
void addListener({
OnGridChanged? onGridChanged,
required OnGridChanged onGridChanged,
OnFieldsChanged? onFieldsChanged,
DidLoadGroups? didLoadGroups,
required DidLoadGroups didLoadGroups,
OnRowsChanged? onRowsChanged,
OnError? onError,
required OnUpdatedGroup onUpdatedGroup,
required OnDeletedGroup onDeletedGroup,
required OnInsertedGroup onInsertedGroup,
required OnError? onError,
}) {
_onGridChanged = onGridChanged;
_onFieldsChanged = onFieldsChanged;
@ -64,6 +76,25 @@ class BoardDataController {
fieldCache.addListener(onFields: (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 {
@ -88,8 +119,9 @@ class BoardDataController {
);
}
Future<Either<RowPB, FlowyError>> createBoardCard(String groupId) {
return _gridFFIService.createBoardCard(groupId);
Future<Either<RowPB, FlowyError>> createBoardCard(String groupId,
{String? startRowId}) {
return _gridFFIService.createBoardCard(groupId, startRowId);
}
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 dateStr = "";
if (cellData != null) {
dateStr = cellData.date + " " + cellData.time;
dateStr = "${cellData.date} ${cellData.time}";
}
return dateStr;
}

View File

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

View File

@ -1,4 +1,5 @@
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:freezed_annotation/freezed_annotation.dart';
import 'dart:async';
@ -20,6 +21,15 @@ class BoardTextCellBloc extends Bloc<BoardTextCellEvent, BoardTextCellState> {
didReceiveCellUpdate: (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
class BoardTextCellEvent with _$BoardTextCellEvent {
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) =
_DidReceiveCellUpdate;
}
@ -57,10 +69,12 @@ class BoardTextCellEvent with _$BoardTextCellEvent {
class BoardTextCellState with _$BoardTextCellState {
const factory BoardTextCellState({
required String content,
required bool enableEdit,
}) = _BoardTextCellState;
factory BoardTextCellState.initial(GridCellController context) =>
BoardTextCellState(
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:equatable/equatable.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/block_entities.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'dart:async';
@ -14,10 +13,12 @@ import 'card_data_controller.dart';
part 'card_bloc.freezed.dart';
class BoardCardBloc extends Bloc<BoardCardEvent, BoardCardState> {
final String fieldId;
final RowFFIService _rowService;
final CardDataController _dataController;
BoardCardBloc({
required this.fieldId,
required String gridId,
required CardDataController dataController,
}) : _rowService = RowFFIService(
@ -25,22 +26,22 @@ class BoardCardBloc extends Bloc<BoardCardEvent, BoardCardState> {
blockId: dataController.rowPB.blockId,
),
_dataController = dataController,
super(BoardCardState.initial(
dataController.rowPB, dataController.loadData())) {
super(
BoardCardState.initial(
dataController.rowPB,
_makeCells(fieldId, dataController.loadData()),
),
) {
on<BoardCardEvent>(
(event, emit) async {
await event.map(
initial: (_InitialRow value) async {
await event.when(
initial: () async {
await _startListening();
},
didReceiveCells: (_DidReceiveCells value) async {
final cells = value.gridCellMap.values
.map((e) => GridCellEquatable(e.field))
.toList();
didReceiveCells: (cells, reason) async {
emit(state.copyWith(
gridCellMap: value.gridCellMap,
cells: UnmodifiableListView(cells),
changeReason: value.reason,
cells: cells,
changeReason: reason,
));
},
);
@ -58,7 +59,7 @@ class BoardCardBloc extends Bloc<BoardCardEvent, BoardCardState> {
return RowInfo(
gridId: _rowService.gridId,
fields: UnmodifiableListView(
state.cells.map((cell) => cell._field).toList(),
state.cells.map((cell) => cell.identifier.field).toList(),
),
rowPB: state.rowPB,
);
@ -66,8 +67,9 @@ class BoardCardBloc extends Bloc<BoardCardEvent, BoardCardState> {
Future<void> _startListening() async {
_dataController.addListener(
onRowChanged: (cells, reason) {
onRowChanged: (cellMap, reason) {
if (!isClosed) {
final cells = _makeCells(fieldId, cellMap);
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
class BoardCardEvent with _$BoardCardEvent {
const factory BoardCardEvent.initial() = _InitialRow;
const factory BoardCardEvent.didReceiveCells(
GridCellMap gridCellMap, RowsChangedReason reason) = _DidReceiveCells;
UnmodifiableListView<BoardCellEquatable> cells,
RowsChangedReason reason,
) = _DidReceiveCells;
}
@freezed
class BoardCardState with _$BoardCardState {
const factory BoardCardState({
required RowPB rowPB,
required GridCellMap gridCellMap,
required UnmodifiableListView<GridCellEquatable> cells,
required UnmodifiableListView<BoardCellEquatable> cells,
RowsChangedReason? changeReason,
}) = _BoardCardState;
factory BoardCardState.initial(RowPB rowPB, GridCellMap cellDataMap) =>
factory BoardCardState.initial(
RowPB rowPB, UnmodifiableListView<BoardCellEquatable> cells) =>
BoardCardState(
rowPB: rowPB,
gridCellMap: cellDataMap,
cells: UnmodifiableListView(
cellDataMap.values.map((e) => GridCellEquatable(e.field)).toList(),
),
cells: cells,
);
}
class GridCellEquatable extends Equatable {
final FieldPB _field;
class BoardCellEquatable extends Equatable {
final GridCellIdentifier identifier;
const GridCellEquatable(FieldPB field) : _field = field;
const BoardCellEquatable(this.identifier);
@override
List<Object?> get props => [
_field.id,
_field.fieldType,
_field.visibility,
_field.width,
identifier.field.id,
identifier.field.fieldType,
identifier.field.visibility,
identifier.field.width,
];
}

View File

@ -1,15 +1,15 @@
import 'package:flowy_sdk/log.dart';
import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/protobuf.dart';
import 'group_listener.dart';
typedef OnGroupError = void Function(FlowyError);
abstract class GroupControllerDelegate {
void removeRow(String groupId, String rowId);
void insertRow(String groupId, RowPB row, int? index);
void updateRow(String groupId, RowPB row);
void removeRow(GroupPB group, String rowId);
void insertRow(GroupPB group, RowPB row, int? index);
void updateRow(GroupPB group, RowPB row);
void addNewRow(GroupPB group, RowPB row, int? index);
}
class GroupController {
@ -31,13 +31,22 @@ class GroupController {
}
}
RowPB? lastRow() {
if (group.rows.isEmpty) return null;
return group.rows.last;
}
void startListening() {
_listener.start(onGroupChanged: (result) {
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) {
final index = insertedRow.hasIndex() ? insertedRow.index : null;
if (insertedRow.hasIndex() &&
group.rows.length > insertedRow.index) {
group.rows.insert(insertedRow.index, insertedRow.row);
@ -45,16 +54,11 @@ class GroupController {
group.rows.add(insertedRow.row);
}
delegate.insertRow(
group.groupId,
insertedRow.row,
index,
);
}
for (final deletedRow in changeset.deletedRows) {
group.rows.removeWhere((rowPB) => rowPB.id == deletedRow);
delegate.removeRow(group.groupId, deletedRow);
if (insertedRow.isNew) {
delegate.addNewRow(group, insertedRow.row, index);
} else {
delegate.insertRow(group, insertedRow.row, index);
}
}
for (final updatedRow in changeset.updatedRows) {
@ -66,7 +70,7 @@ class GroupController {
group.rows[index] = updatedRow;
}
delegate.updateRow(group.groupId, updatedRow);
delegate.updateRow(group, updatedRow);
}
},
(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 {
_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:flowy_sdk/protobuf/flowy-grid/group_changeset.pb.dart';
typedef UpdateGroupNotifiedValue = Either<GroupRowsChangesetPB, FlowyError>;
typedef UpdateGroupNotifiedValue = Either<GroupChangesetPB, FlowyError>;
class GroupListener {
final GroupPB group;
@ -34,7 +34,7 @@ class GroupListener {
case GridNotification.DidUpdateGroup:
result.fold(
(payload) => _groupNotifier?.value =
left(GroupRowsChangesetPB.fromBuffer(payload)),
left(GroupChangesetPB.fromBuffer(payload)),
(error) => _groupNotifier?.value = right(error),
);
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 {
@override
bool get creatable => false;
bool get creatable => true;
}
class BoardPlugin extends Plugin {

View File

@ -2,6 +2,7 @@
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/grid/application/row/row_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/row/row_detail.dart';
import 'package:appflowy_board/appflowy_board.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/image.dart';
import 'package:flowy_infra/theme.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_infra_ui/flowy_infra_ui_web.dart';
import 'package:flowy_infra_ui/widget/error_page.dart';
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/group.pbserver.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../grid/application/row/row_cache.dart';
import '../application/board_bloc.dart';
import 'card/card.dart';
import 'card/card_cell_builder.dart';
import 'toolbar/board_toolbar.dart';
class BoardPage extends StatelessWidget {
final ViewPB view;
@ -30,13 +37,15 @@ class BoardPage extends StatelessWidget {
create: (context) =>
BoardBloc(view: view)..add(const BoardEvent.initial()),
child: BlocBuilder<BoardBloc, BoardState>(
buildWhen: (previous, current) =>
previous.loadingState != current.loadingState,
builder: (context, state) {
return state.loadingState.map(
loading: (_) =>
const Center(child: CircularProgressIndicator.adaptive()),
finish: (result) {
return result.successOrFail.fold(
(_) => BoardContent(),
(_) => const BoardContent(),
(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(
columnBackgroundColor: HexColor.fromHex('#F7F8FC'),
);
BoardContent({Key? key}) : super(key: key);
@override
void initState() {
scrollController = ScrollController();
scrollManager = AFBoardScrollManager();
super.initState();
}
@override
Widget build(BuildContext context) {
return BlocBuilder<BoardBloc, BoardState>(
builder: (context, state) {
return Container(
color: Colors.white,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 20),
child: AFBoard(
// key: UniqueKey(),
scrollController: ScrollController(),
dataController: context.read<BoardBloc>().afBoardDataController,
headerBuilder: _buildHeader,
footBuilder: _buildFooter,
cardBuilder: (_, data) => _buildCard(context, data),
columnConstraints: const BoxConstraints.tightFor(width: 240),
config: AFBoardConfig(
columnBackgroundColor: HexColor.fromHex('#F7F8FC'),
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) {
final theme = context.read<AppTheme>();
return Container(
color: theme.surface,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Column(
children: [
const _ToolbarBlocAdaptor(),
Expanded(
child: AFBoard(
scrollManager: scrollManager,
scrollController: scrollController,
dataController: context.read<BoardBloc>().boardController,
headerBuilder: _buildHeader,
footBuilder: _buildFooter,
cardBuilder: (_, column, columnItem) => _buildCard(
context,
column,
columnItem,
),
columnConstraints:
const BoxConstraints.tightFor(width: 300),
config: AFBoardConfig(
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(
icon: const Icon(Icons.lightbulb_circle),
title: Text(columnData.desc),
addIcon: const Icon(Icons.add, size: 20),
moreIcon: const Icon(Icons.more_horiz, size: 20),
title: Flexible(
fit: FlexFit.tight,
child: FlowyText.medium(
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,
margin: config.columnItemPadding,
margin: config.headerPadding,
);
}
Widget _buildFooter(BuildContext context, AFBoardColumnData columnData) {
return AppFlowyColumnFooter(
icon: const Icon(Icons.add, size: 20),
title: const Text('New'),
final group = columnData.customData as GroupPB;
if (group.isDefault) {
return const SizedBox();
} else {
return AppFlowyColumnFooter(
icon: SizedBox(
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,
margin: config.columnItemPadding,
margin: config.footerPadding,
onAddButtonClick: () {
context.read<BoardBloc>().add(BoardEvent.createRow(columnData.id));
});
context.read<BoardBloc>().add(
BoardEvent.createBottomRow(columnData.id),
);
},
);
}
}
Widget _buildCard(BuildContext context, AFColumnItem item) {
final rowPB = (item as BoardColumnItem).row;
Widget _buildCard(
BuildContext context,
AFBoardColumnData column,
AFColumnItem columnItem,
) {
final boardColumnItem = columnItem as BoardColumnItem;
final rowPB = boardColumnItem.row;
final rowCache = context.read<BoardBloc>().getRowCache(rowPB.blockId);
/// Return placeholder widget if the rowCache is null.
if (rowCache == null) return SizedBox(key: ObjectKey(item));
if (rowCache == null) return SizedBox(key: ObjectKey(columnItem));
final fieldCache = context.read<BoardBloc>().fieldCache;
final gridId = context.read<BoardBloc>().gridId;
@ -118,21 +232,25 @@ class BoardContent extends StatelessWidget {
);
final cellBuilder = BoardCellBuilder(cardController);
final isEditing = context.read<BoardBloc>().state.editingRow.fold(
() => false,
(editingRow) => editingRow.id == rowPB.id,
);
bool isEditing = false;
context.read<BoardBloc>().state.editingRow.fold(
() => null,
(editingRow) {
isEditing = editingRow.row.id == columnItem.row.id;
},
);
return AppFlowyColumnItemCard(
key: ObjectKey(item),
key: ValueKey(columnItem.id),
margin: config.cardPadding,
decoration: _makeBoxDecoration(context),
child: BoardCard(
gridId: gridId,
groupId: column.id,
fieldId: boardColumnItem.fieldId,
isEditing: isEditing,
cellBuilder: cellBuilder,
dataController: cardController,
onEditEditing: (rowId) {
context.read<BoardBloc>().add(BoardEvent.endEditRow(rowId));
},
openCard: (context) => _openCard(
gridId,
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,
GridRowCache rowCache, BuildContext context) {
final rowInfo = RowInfo(
@ -159,13 +287,33 @@ class BoardContent extends StatelessWidget {
);
FlowyOverlay.show(
context: context,
builder: (BuildContext context) {
return RowDetailPage(
cellBuilder: GridCellBuilder(delegate: dataController),
dataController: dataController,
);
});
context: context,
builder: (BuildContext context) {
return RowDetailPage(
cellBuilder: GridCellBuilder(delegate: 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';
class BoardCheckboxCell extends StatefulWidget {
final String groupId;
final GridCellControllerBuilder cellControllerBuilder;
const BoardCheckboxCell({
required this.groupId,
required this.cellControllerBuilder,
Key? key,
}) : super(key: key);
@ -34,6 +36,8 @@ class _BoardCheckboxCellState extends State<BoardCheckboxCell> {
return BlocProvider.value(
value: _cellBloc,
child: BlocBuilder<BoardCheckboxCellBloc, BoardCheckboxCellState>(
buildWhen: (previous, current) =>
previous.isSelected != current.isSelected,
builder: (context, state) {
final icon = state.isSelected
? 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/grid/application/cell/cell_service/cell_service.dart';
import 'package:flowy_infra/theme.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class BoardDateCell extends StatefulWidget {
final String groupId;
final GridCellControllerBuilder cellControllerBuilder;
const BoardDateCell({
required this.groupId,
required this.cellControllerBuilder,
Key? key,
}) : super(key: key);
@ -34,6 +37,7 @@ class _BoardDateCellState extends State<BoardDateCell> {
return BlocProvider.value(
value: _cellBloc,
child: BlocBuilder<BoardDateCellBloc, BoardDateCellState>(
buildWhen: (previous, current) => previous.dateStr != current.dateStr,
builder: (context, state) {
if (state.dateStr.isEmpty) {
return const SizedBox();
@ -42,7 +46,8 @@ class _BoardDateCellState extends State<BoardDateCell> {
alignment: Alignment.centerLeft,
child: FlowyText.regular(
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';
class BoardNumberCell extends StatefulWidget {
final String groupId;
final GridCellControllerBuilder cellControllerBuilder;
const BoardNumberCell({
required this.groupId,
required this.cellControllerBuilder,
Key? key,
}) : super(key: key);
@ -34,13 +36,14 @@ class _BoardNumberCellState extends State<BoardNumberCell> {
return BlocProvider.value(
value: _cellBloc,
child: BlocBuilder<BoardNumberCellBloc, BoardNumberCellState>(
buildWhen: (previous, current) => previous.content != current.content,
builder: (context, state) {
if (state.content.isEmpty) {
return const SizedBox();
} else {
return Align(
alignment: Alignment.centerLeft,
child: FlowyText.regular(
child: FlowyText.medium(
state.content,
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/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/select_option_editor.dart';
import 'package:flutter/material.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;
@override
final EditableCellNotifier? editableNotifier;
const BoardSelectOptionCell({
required this.groupId,
required this.cellControllerBuilder,
this.editableNotifier,
Key? key,
}) : super(key: key);
@ -33,23 +41,41 @@ class _BoardSelectOptionCellState extends State<BoardSelectOptionCell> {
return BlocProvider.value(
value: _cellBloc,
child: BlocBuilder<BoardSelectOptionCellBloc, BoardSelectOptionCellState>(
buildWhen: (previous, current) {
return previous.selectedOptions != current.selectedOptions;
},
builder: (context, state) {
final children = state.selectedOptions
.map((option) => SelectOptionTag.fromOption(
if (state.selectedOptions
.where((element) => element.id == widget.groupId)
.isNotEmpty ||
state.selectedOptions.isEmpty) {
return const SizedBox();
} else {
final children = state.selectedOptions
.map(
(option) => SelectOptionTag.fromOption(
context: context,
option: option,
))
.toList();
return Align(
alignment: Alignment.centerLeft,
child: AbsorbPointer(
child: Wrap(
children: children,
spacing: 4,
runSpacing: 2,
),
)
.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();
}
}
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/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:flutter/material.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;
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
State<BoardTextCell> createState() => _BoardTextCellState();
@ -15,14 +29,48 @@ class BoardTextCell extends StatefulWidget {
class _BoardTextCellState extends State<BoardTextCell> {
late BoardTextCellBloc _cellBloc;
late TextEditingController _controller;
bool focusWhenInit = false;
SingleListenerFocusNode focusNode = SingleListenerFocusNode();
@override
void initState() {
final cellController =
widget.cellControllerBuilder.build() as GridCellController;
_cellBloc = BoardTextCellBloc(cellController: cellController)
..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();
}
@ -30,32 +78,75 @@ class _BoardTextCellState extends State<BoardTextCell> {
Widget build(BuildContext context) {
return BlocProvider.value(
value: _cellBloc,
child: BlocBuilder<BoardTextCellBloc, BoardTextCellState>(
builder: (context, state) {
if (state.content.isEmpty) {
return const SizedBox();
} else {
return Align(
alignment: Alignment.centerLeft,
child: ConstrainedBox(
constraints: BoxConstraints.loose(
const Size(double.infinity, 100),
),
child: FlowyText.regular(
state.content,
fontSize: 14,
),
),
);
child: BlocListener<BoardTextCellBloc, BoardTextCellState>(
listener: (context, state) {
if (_controller.text != state.content) {
_controller.text = state.content;
}
},
child: BlocBuilder<BoardTextCellBloc, BoardTextCellState>(
builder: (context, state) {
if (state.content.isEmpty &&
state.enableEdit == false &&
focusWhenInit == false) {
return const SizedBox();
}
//
Widget child;
if (state.enableEdit || focusWhenInit) {
child = _buildTextField();
} else {
child = _buildText(state);
}
return Align(alignment: Alignment.centerLeft, child: child);
},
),
),
);
}
Future<void> focusChanged() async {
_cellBloc.add(BoardTextCellEvent.updateText(_controller.text));
}
@override
Future<void> dispose() async {
_cellBloc.close();
_controller.dispose();
focusNode.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';
class BoardUrlCell extends StatefulWidget {
final String groupId;
final GridCellControllerBuilder cellControllerBuilder;
const BoardUrlCell({
required this.groupId,
required this.cellControllerBuilder,
Key? key,
}) : super(key: key);
@ -34,6 +36,7 @@ class _BoardUrlCellState extends State<BoardUrlCell> {
return BlocProvider.value(
value: _cellBloc,
child: BlocBuilder<BoardURLCellBloc, BoardURLCellState>(
buildWhen: (previous, current) => previous.content != current.content,
builder: (context, state) {
if (state.content.isEmpty) {
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:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'board_cell.dart';
import 'card_cell_builder.dart';
import 'card_container.dart';
typedef OnEndEditing = void Function(String rowId);
class BoardCard extends StatefulWidget {
final String gridId;
final String groupId;
final String fieldId;
final bool isEditing;
final CardDataController dataController;
final BoardCellBuilder cellBuilder;
final OnEndEditing onEditEditing;
final void Function(BuildContext) openCard;
const BoardCard({
required this.gridId,
required this.groupId,
required this.fieldId,
required this.isEditing,
required this.dataController,
required this.cellBuilder,
required this.onEditEditing,
required this.openCard,
Key? key,
}) : super(key: key);
@ -36,13 +37,16 @@ class BoardCard extends StatefulWidget {
class _BoardCardState extends State<BoardCard> {
late BoardCardBloc _cardBloc;
late EditableRowNotifier rowNotifier;
@override
void initState() {
rowNotifier = EditableRowNotifier();
_cardBloc = BoardCardBloc(
gridId: widget.gridId,
fieldId: widget.fieldId,
dataController: widget.dataController,
);
)..add(const BoardCardEvent.initial());
super.initState();
}
@ -51,16 +55,28 @@ class _BoardCardState extends State<BoardCard> {
return BlocProvider.value(
value: _cardBloc,
child: BlocBuilder<BoardCardBloc, BoardCardState>(
buildWhen: (previous, current) {
return previous.cells.length != current.cells.length;
},
builder: (context, state) {
return BoardCardContainer(
accessoryBuilder: (context) {
return [const _CardMoreOption()];
return [
_CardEditOption(
startEditing: () => rowNotifier.becomeFirstResponder(),
),
const _CardMoreOption(),
];
},
onTap: (context) {
widget.openCard(context);
},
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) {
return cellMap.values.map(
(cellId) {
final child = widget.cellBuilder.buildCell(cellId);
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 5),
List<Widget> _makeCells(
BuildContext context,
List<GridCellIdentifier> cells,
) {
final List<Widget> children = [];
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,
);
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
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
@ -96,3 +142,27 @@ class _CardMoreOption extends StatelessWidget with CardAccessory {
).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:flutter/material.dart';
import 'board_cell.dart';
import 'board_checkbox_cell.dart';
import 'board_date_cell.dart';
import 'board_number_cell.dart';
@ -19,7 +20,12 @@ class BoardCellBuilder {
BoardCellBuilder(this.delegate);
Widget buildCell(GridCellIdentifier cellId) {
Widget buildCell(
String groupId,
GridCellIdentifier cellId,
bool isEditing,
EditableCellNotifier cellNotifier,
) {
final cellControllerBuilder = GridCellControllerBuilder(
delegate: delegate,
cellId: cellId,
@ -30,36 +36,46 @@ class BoardCellBuilder {
switch (cellId.fieldType) {
case FieldType.Checkbox:
return BoardCheckboxCell(
groupId: groupId,
cellControllerBuilder: cellControllerBuilder,
key: key,
);
case FieldType.DateTime:
return BoardDateCell(
groupId: groupId,
cellControllerBuilder: cellControllerBuilder,
key: key,
);
case FieldType.SingleSelect:
return BoardSelectOptionCell(
groupId: groupId,
cellControllerBuilder: cellControllerBuilder,
key: key,
);
case FieldType.MultiSelect:
return BoardSelectOptionCell(
groupId: groupId,
cellControllerBuilder: cellControllerBuilder,
editableNotifier: cellNotifier,
key: key,
);
case FieldType.Number:
return BoardNumberCell(
groupId: groupId,
cellControllerBuilder: cellControllerBuilder,
key: key,
);
case FieldType.RichText:
return BoardTextCell(
groupId: groupId,
cellControllerBuilder: cellControllerBuilder,
isFocus: isEditing,
editableNotifier: cellNotifier,
key: key,
);
case FieldType.URL:
return BoardUrlCell(
groupId: groupId,
cellControllerBuilder: cellControllerBuilder,
key: key,
);

View File

@ -26,8 +26,8 @@ class BoardCardContainer extends StatelessWidget {
final accessories = accessoryBuilder!(context);
if (accessories.isNotEmpty) {
container = _CardEnterRegion(
child: container,
accessories: accessories,
child: container,
);
}
}
@ -69,25 +69,48 @@ class CardAccessoryContainer extends StatelessWidget {
style: HoverStyle(
hoverColor: theme.hover,
backgroundColor: theme.surface,
borderRadius: BorderRadius.zero,
),
builder: (_, onHover) => Container(
width: 26,
height: 26,
padding: const EdgeInsets.all(3),
builder: (_, onHover) => SizedBox(
width: 24,
height: 24,
child: accessory,
),
);
return GestureDetector(
child: hover,
behavior: HitTestBehavior.opaque,
onTap: () => accessory.onTap(context),
child: hover,
);
}).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 {
final Widget child;
final List<CardAccessory> accessories;
@ -102,8 +125,9 @@ class _CardEnterRegion extends StatelessWidget {
builder: (context, onEnter, _) {
List<Widget> children = [child];
if (onEnter) {
children.add(CardAccessoryContainer(accessories: accessories)
.positioned(right: 0));
children.add(CardAccessoryContainer(
accessories: accessories,
).positioned(right: 0));
}
return MouseRegion(
@ -116,7 +140,7 @@ class _CardEnterRegion extends StatelessWidget {
.onEnter = false,
child: IntrinsicHeight(
child: Stack(
alignment: AlignmentDirectional.center,
alignment: AlignmentDirectional.topEnd,
fit: StackFit.expand,
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,
outlineColor: Colors.white,
borderRadius: Corners.s8Border,
onPressed: onRestore,
child: FlowyText.medium(
LocaleKeys.deletePagePrompt_restore.tr(),
color: Colors.white,
fontSize: 14),
onPressed: onRestore),
fontSize: 14)),
const HSpace(20),
BaseStyledButton(
minWidth: 220,
@ -55,11 +55,11 @@ class DocumentBanner extends StatelessWidget {
downColor: theme.main1,
outlineColor: Colors.white,
borderRadius: Corners.s8Border,
onPressed: onDelete,
child: FlowyText.medium(
LocaleKeys.deletePagePrompt_deletePermanent.tr(),
color: Colors.white,
fontSize: 14),
onPressed: onDelete),
fontSize: 14)),
],
),
),

View File

@ -16,7 +16,10 @@ class EditorCheckboxBuilder extends QuillCheckboxBuilder {
EditorCheckboxBuilder(this.theme);
@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(
theme: theme,
isChecked: isChecked,
@ -37,10 +40,10 @@ class FlowyEditorCheckbox extends StatefulWidget {
}) : super(key: key);
@override
_FlowyEditorCheckboxState createState() => _FlowyEditorCheckboxState();
FlowyEditorCheckboxState createState() => FlowyEditorCheckboxState();
}
class _FlowyEditorCheckboxState extends State<FlowyEditorCheckbox> {
class FlowyEditorCheckboxState extends State<FlowyEditorCheckbox> {
late bool isChecked;
@override
@ -51,7 +54,9 @@ class _FlowyEditorCheckboxState extends State<FlowyEditorCheckbox> {
@override
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(
alignment: Alignment.centerLeft,
child: FlowyIconButton(

View File

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

View File

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

View File

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

View File

@ -21,10 +21,10 @@ class FlowyToggleStyleButton extends StatefulWidget {
}) : super(key: key);
@override
_ToggleStyleButtonState createState() => _ToggleStyleButtonState();
ToggleStyleButtonState createState() => ToggleStyleButtonState();
}
class _ToggleStyleButtonState extends State<FlowyToggleStyleButton> {
class ToggleStyleButtonState extends State<FlowyToggleStyleButton> {
bool? _isToggled;
Style get _selectionStyle => widget.controller.getSelectionStyle();
@override
@ -77,6 +77,8 @@ class _ToggleStyleButtonState extends State<FlowyToggleStyleButton> {
}
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(
color: Theme.of(context).canvasColor,
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;
@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();
bool _showLeftArrow = false;
bool _showRightArrow = false;
@ -196,7 +198,8 @@ class _ToolbarButtonListState extends State<ToolbarButtonList> with WidgetsBindi
return LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
List<Widget> children = [];
double width = (widget.buttons.length + 2) * defaultIconSize * kIconButtonFactor;
double width =
(widget.buttons.length + 2) * defaultIconSize * kIconButtonFactor;
final isFit = constraints.maxWidth > width;
if (!isFit) {
children.add(_buildLeftArrow());
@ -233,8 +236,10 @@ class _ToolbarButtonListState extends State<ToolbarButtonList> with WidgetsBindi
void _handleScroll() {
if (!mounted) return;
setState(() {
_showLeftArrow = _controller.position.minScrollExtent != _controller.position.pixels;
_showRightArrow = _controller.position.maxScrollExtent != _controller.position.pixels;
_showLeftArrow =
_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 {
final String rowId;
final String fieldId;
PublishNotifier<UpdateFieldNotifiedValue>? _updateCellNotifier = PublishNotifier();
PublishNotifier<UpdateFieldNotifiedValue>? _updateCellNotifier =
PublishNotifier();
GridNotificationListener? _listener;
CellListener({required this.rowId, required this.fieldId});
void start({required void Function(UpdateFieldNotifiedValue) 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) {

View File

@ -33,10 +33,17 @@ class GridCellCache {
required this.gridId,
});
void remove(String fieldId) {
void removeCellWithFieldId(String 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) {
var map = _cellDataByFieldId[key.fieldId];
if (map == null) {

View File

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

View File

@ -71,6 +71,6 @@ class GridCellIdentifier with _$GridCellIdentifier {
FieldType get fieldType => field.fieldType;
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
_cellListener?.start(onCellChanged: (result) {
result.fold(
(_) => _loadData(),
(_) {
_cellsCache.remove(_cacheKey);
_loadData();
},
(err) => Log.error(err),
);
});
@ -279,8 +282,8 @@ class IGridCellController<T, D> extends Equatable {
_loadDataOperation?.cancel();
_loadDataOperation = Timer(const Duration(milliseconds: 10), () {
_cellDataLoader.loadData().then((data) {
_cellDataNotifier?.value = 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 msg = LocaleKeys.grid_field_invalidTimeFormat.tr() + ". ";
String msg = "${LocaleKeys.grid_field_invalidTimeFormat.tr()}. ";
switch (state.dateTypeOptionPB.timeFormat) {
case TimeFormat.TwelveHour:
msg = msg + "e.g. 01: 00 AM";
msg = "${msg}e.g. 01: 00 AM";
break;
case TimeFormat.TwentyFourHour:
msg = msg + "e.g. 13: 00";
msg = "${msg}e.g. 13: 00";
break;
default:
break;

View File

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

View File

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

View File

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

View File

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

View File

@ -52,7 +52,8 @@ class GridRowCache {
//
notifier.onRowFieldsChanged(() => _rowChangeReasonNotifier
.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();
}
@ -209,7 +210,8 @@ class GridRowCache {
}
GridCellMap _makeGridCells(String rowId, RowPB? row) {
var cellDataMap = GridCellMap.new();
// ignore: prefer_collection_literals
var cellDataMap = GridCellMap();
for (final field in _fieldNotifier.fields) {
if (field.visibility) {
cellDataMap[field.id] = GridCellIdentifier(

View File

@ -190,12 +190,12 @@ class CellAccessoryContainer extends StatelessWidget {
),
);
return GestureDetector(
child: hover,
behavior: HitTestBehavior.opaque,
onTap: () => accessory.onTap(),
child: hover,
);
}).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) {
container = _GridCellEnterRegion(
child: container,
accessories: accessories,
child: container,
);
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -108,7 +108,7 @@ class SelectOptionTextField extends StatelessWidget {
child: SingleChildScrollView(
controller: sc,
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(
widget: OverlayContainer(
constraints: BoxConstraints.loose(const Size(300, 160)),
child: SizedBox(
width: 200,
child: Padding(padding: const EdgeInsets.all(6), child: editor),
),
constraints: BoxConstraints.loose(const Size(300, 160)),
),
identifier: URLCellEditor.identifier(),
anchorContext: context,

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/type_option/type_option_context.dart';
import 'package:app_flowy/startup/startup.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:flowy_infra/image.dart';
import 'package:flowy_infra/theme.dart';
@ -178,7 +180,10 @@ class CreateFieldButton extends StatelessWidget {
triggerActions: PopoverTriggerActionFlags.click,
direction: PopoverDirection.bottomWithRightAligned,
child: FlowyButton(
text: const FlowyText.medium('New column', fontSize: 12),
text: FlowyText.medium(
LocaleKeys.grid_field_newColumn.tr(),
fontSize: 12,
),
hoverColor: theme.shader6,
onTap: () {},
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 {
final _SelectNumberFormatCallback onSelected;
final SelectNumberFormatCallback onSelected;
final NumberFormat selectedFormat;
const NumberFormatList(
{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/prelude.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 {
final RowInfo rowInfo;
@ -122,10 +124,13 @@ class _InsertRowButton extends StatelessWidget {
Widget build(BuildContext context) {
final theme = context.watch<AppTheme>();
return FlowyIconButton(
tooltipText: LocaleKeys.tooltip_addNewRow.tr(),
hoverColor: theme.hover,
width: 20,
height: 30,
onPressed: () => context.read<RowBloc>().add(const RowEvent.createRow()),
onPressed: () => context.read<RowBloc>().add(
const RowEvent.createRow(),
),
iconPadding: const EdgeInsets.all(3),
icon: svgWidget("home/add"),
);
@ -139,6 +144,7 @@ class _DeleteRowButton extends StatelessWidget {
Widget build(BuildContext context) {
final theme = context.watch<AppTheme>();
return FlowyIconButton(
tooltipText: LocaleKeys.tooltip_openMenu.tr(),
hoverColor: theme.hover,
width: 20,
height: 30,
@ -184,7 +190,6 @@ class RowContent extends StatelessWidget {
return CellContainer(
width: cellId.field.width.toDouble(),
child: child,
rowStateNotifier:
Provider.of<RegionStateNotifier>(context, listen: false),
accessoryBuilder: (buildContext) {
@ -202,6 +207,7 @@ class RowContent extends StatelessWidget {
}
return accessories;
},
child: child,
);
},
).toList();

View File

@ -59,8 +59,8 @@ class GridRowActionSheet extends StatelessWidget {
}) {
FlowyOverlay.of(overlayContext).insertWithAnchor(
widget: OverlayContainer(
child: this,
constraints: BoxConstraints.loose(const Size(140, 200)),
child: this,
),
identifier: GridRowActionSheet.identifier(),
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/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/icon_button.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:easy_localization/easy_localization.dart';
import 'package:app_flowy/generated/locale_keys.g.dart';
@ -61,7 +63,12 @@ class _RowDetailPageState extends State<RowDetailPage> {
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 {
final String viewId;
final GridCellBuilder cellBuilder;
final ScrollController _scrollController;
_PropertyList({
required this.viewId,
required this.cellBuilder,
Key? key,
}) : _scrollController = ScrollController(),
@ -101,22 +110,65 @@ class _PropertyList extends StatelessWidget {
return BlocBuilder<RowDetailBloc, RowDetailState>(
buildWhen: (previous, current) => previous.gridCells != current.gridCells,
builder: (context, state) {
return ScrollbarListStack(
axis: Axis.vertical,
controller: _scrollController,
barSize: GridSize.scrollBarSize,
child: ListView.separated(
controller: _scrollController,
itemCount: state.gridCells.length,
itemBuilder: (BuildContext context, int index) {
return _RowDetailCell(
cellId: state.gridCells[index],
cellBuilder: cellBuilder,
);
},
separatorBuilder: (BuildContext context, int index) {
return const VSpace(2);
},
return Column(
children: [
Expanded(
child: ScrollbarListStack(
axis: Axis.vertical,
controller: _scrollController,
barSize: GridSize.scrollBarSize,
child: ListView.separated(
controller: _scrollController,
itemCount: state.gridCells.length,
itemBuilder: (BuildContext context, int index) {
return _RowDetailCell(
cellId: state.gridCells[index],
cellBuilder: cellBuilder,
);
},
separatorBuilder: (BuildContext context, int index) {
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,
onTap: () => cell.beginFocus.notify(),
child: AccessoryHover(
child: cell,
contentPadding:
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:appflowy_popover/popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/image.dart';
import 'package:flowy_infra/theme.dart';

View File

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

View File

@ -20,39 +20,35 @@ class InitAppWidgetTask extends LaunchTask {
final setting = await UserSettingsService().getAppearanceSettings();
final settingModel = AppearanceSettingModel(setting);
final app = ApplicationWidget(
child: widget,
settingModel: settingModel,
child: widget,
);
BlocOverrides.runZoned(
() {
runApp(
EasyLocalization(
supportedLocales: const [
// In alphabetical order
Locale('ca', 'ES'),
Locale('de', 'DE'),
Locale('en'),
Locale('es', 'VE'),
Locale('fr', 'FR'),
Locale('fr', 'CA'),
Locale('hu', 'HU'),
Locale('id', 'ID'),
Locale('it', 'IT'),
Locale('ja', 'JP'),
Locale('pl', 'PL'),
Locale('pt', 'BR'),
Locale('ru', 'RU'),
Locale('tr', 'TR'),
Locale('zh', 'CN'),
],
path: 'assets/translations',
fallbackLocale: const Locale('en'),
saveLocale: false,
child: app,
),
);
},
blocObserver: ApplicationBlocObserver(),
Bloc.observer = ApplicationBlocObserver();
runApp(
EasyLocalization(
supportedLocales: const [
// In alphabetical order
Locale('ca', 'ES'),
Locale('de', 'DE'),
Locale('en'),
Locale('es', 'VE'),
Locale('fr', 'FR'),
Locale('fr', 'CA'),
Locale('hu', 'HU'),
Locale('id', 'ID'),
Locale('it', 'IT'),
Locale('ja', 'JP'),
Locale('pl', 'PL'),
Locale('pt', 'BR'),
Locale('ru', 'RU'),
Locale('tr', 'TR'),
Locale('zh', 'CN'),
],
path: 'assets/translations',
fallbackLocale: const Locale('en'),
saveLocale: false,
child: app,
),
);
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(
context,
PageRoutes.fade(() => HomeScreen(profile, workspaceSetting), RouteDurations.slow.inMilliseconds * .001),
PageRoutes.fade(() => HomeScreen(profile, workspaceSetting),
RouteDurations.slow.inMilliseconds * .001),
);
}
}
class SplashRoute {
Future<void> pushWelcomeScreen(BuildContext context, UserProfilePB userProfile) async {
Future<void> pushWelcomeScreen(
BuildContext context, UserProfilePB userProfile) async {
final screen = WelcomeScreen(userProfile: userProfile);
final workspaceId = await Navigator.of(context).push(
PageRoutes.fade(
@ -46,20 +49,24 @@ class SplashRoute {
),
);
// ignore: use_build_context_synchronously
pushHomeScreen(context, userProfile, workspaceId);
}
void pushHomeScreen(BuildContext context, UserProfilePB userProfile, CurrentWorkspaceSettingPB workspaceSetting) {
void pushHomeScreen(BuildContext context, UserProfilePB userProfile,
CurrentWorkspaceSettingPB workspaceSetting) {
Navigator.push(
context,
PageRoutes.fade(() => HomeScreen(userProfile, workspaceSetting), RouteDurations.slow.inMilliseconds * .001),
PageRoutes.fade(() => HomeScreen(userProfile, workspaceSetting),
RouteDurations.slow.inMilliseconds * .001),
);
}
void pushSignInScreen(BuildContext context) {
Navigator.push(
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) {
final theme = context.watch<AppTheme>();
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(LocaleKeys.signIn_dontHaveAnAccount.tr(), style: TextStyle(color: theme.shader3, fontSize: 12)),
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) {
final theme = context.watch<AppTheme>();
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
LocaleKeys.signUp_alreadyHaveAnAccount.tr(),
@ -97,7 +98,6 @@ class SignUpPrompt extends StatelessWidget {
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 {
_DelimiterRun._(
class DelimiterRun {
DelimiterRun._(
{this.char,
this.length,
this.isLeftFlanking,
@ -420,8 +420,7 @@ class _DelimiterRun {
final bool? isFollowedByPunctuation;
// ignore: prefer_constructors_over_static_methods
static _DelimiterRun? tryParse(
InlineParser parser, int runStart, int runEnd) {
static DelimiterRun? tryParse(InlineParser parser, int runStart, int runEnd) {
bool leftFlanking,
rightFlanking,
precededByPunctuation,
@ -466,7 +465,7 @@ class _DelimiterRun {
return null;
}
return _DelimiterRun._(
return DelimiterRun._(
char: parser.charAt(runStart),
length: runEnd - runStart + 1,
isLeftFlanking: leftFlanking,
@ -516,7 +515,7 @@ class TagSyntax extends InlineSyntax {
return true;
}
final delimiterRun = _DelimiterRun.tryParse(parser, matchStart, matchEnd);
final delimiterRun = DelimiterRun.tryParse(parser, matchStart, matchEnd);
if (delimiterRun != null && delimiterRun.canOpen) {
parser.openTag(TagState(parser.pos, matchEnd + 1, this, delimiterRun));
return true;
@ -531,7 +530,7 @@ class TagSyntax extends InlineSyntax {
final matchStart = parser.pos;
final matchEnd = parser.pos + runLength - 1;
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) {
parser.addNode(Element('em', state.children));
@ -579,7 +578,7 @@ class StrikethroughSyntax extends TagSyntax {
final runLength = match.group(0)!.length;
final matchStart = parser.pos;
final matchEnd = parser.pos + runLength - 1;
final delimiterRun = _DelimiterRun.tryParse(parser, matchStart, matchEnd)!;
final delimiterRun = DelimiterRun.tryParse(parser, matchStart, matchEnd)!;
if (!delimiterRun.isRightFlanking!) {
return false;
}
@ -1170,7 +1169,7 @@ class TagState {
/// The children of this node. Will be `null` for text nodes.
final List<Node> children;
final _DelimiterRun? openingDelimiterRun;
final DelimiterRun? openingDelimiterRun;
/// Attempts to close this tag by matching the current text against its end
/// pattern.
@ -1193,7 +1192,7 @@ class TagState {
final closingMatchStart = parser.pos;
final closingMatchEnd = parser.pos + runLength - 1;
final closingDelimiterRun =
_DelimiterRun.tryParse(parser, closingMatchStart, closingMatchEnd);
DelimiterRun.tryParse(parser, closingMatchStart, closingMatchEnd);
if (closingDelimiterRun != null && closingDelimiterRun.canClose) {
// Emphasis rules #9 and #10:
final oneRunOpensAndCloses =

View File

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

View File

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