mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
Feat/view map database (#1885)
* refactor: rename structs * chore: read database id from view * chore: fix open database error because of create a database view for database id * chore: fix tests * chore: rename datbase id to view id in flutter * refactor: move grid and board to database view folder * refactor: rename functions * refactor: move calender to datbase view folder * refactor: rename app_flowy to appflowy_flutter * chore: reanming * chore: fix freeze gen * chore: remove todos * refactor: view process events * chore: add link database test * chore: just open view if there is opened database
This commit is contained in:
parent
6877607c5e
commit
61fd608200
@ -1,4 +1,4 @@
|
|||||||
frontend/app_flowy/
|
frontend/appflowy_flutter/
|
||||||
frontend/scripts/
|
frontend/scripts/
|
||||||
frontend/rust-lib/target
|
frontend/rust-lib/target
|
||||||
shared-lib/target/
|
shared-lib/target/
|
4
.github/workflows/appflowy_editor_test.yml
vendored
4
.github/workflows/appflowy_editor_test.yml
vendored
@ -11,7 +11,7 @@ on:
|
|||||||
- "main"
|
- "main"
|
||||||
- "release/*"
|
- "release/*"
|
||||||
paths:
|
paths:
|
||||||
- "frontend/app_flowy/packages/appflowy_editor/**"
|
- "frontend/appflowy_flutter/packages/appflowy_editor/**"
|
||||||
|
|
||||||
env:
|
env:
|
||||||
CARGO_TERM_COLOR: always
|
CARGO_TERM_COLOR: always
|
||||||
@ -35,7 +35,7 @@ jobs:
|
|||||||
cache: true
|
cache: true
|
||||||
|
|
||||||
- name: Run FlowyEditor tests
|
- name: Run FlowyEditor tests
|
||||||
working-directory: frontend/app_flowy/packages/appflowy_editor
|
working-directory: frontend/appflowy_flutter/packages/appflowy_editor
|
||||||
run: |
|
run: |
|
||||||
flutter pub get
|
flutter pub get
|
||||||
flutter format --set-exit-if-changed .
|
flutter format --set-exit-if-changed .
|
||||||
|
2
.github/workflows/flutter_ci.yaml
vendored
2
.github/workflows/flutter_ci.yaml
vendored
@ -98,7 +98,7 @@ jobs:
|
|||||||
cargo make --profile ${{ matrix.flutter_profile }} appflowy-dev
|
cargo make --profile ${{ matrix.flutter_profile }} appflowy-dev
|
||||||
|
|
||||||
- name: Flutter Analyzer
|
- name: Flutter Analyzer
|
||||||
working-directory: frontend/app_flowy
|
working-directory: frontend/appflowy_flutter
|
||||||
run: flutter analyze
|
run: flutter analyze
|
||||||
|
|
||||||
- name: Run Flutter unit tests
|
- name: Run Flutter unit tests
|
||||||
|
8
.github/workflows/integration_test.yml
vendored
8
.github/workflows/integration_test.yml
vendored
@ -6,14 +6,14 @@ on:
|
|||||||
- "main"
|
- "main"
|
||||||
- "release/*"
|
- "release/*"
|
||||||
paths:
|
paths:
|
||||||
- "frontend/app_flowy/**"
|
- "frontend/appflowy_flutter/**"
|
||||||
|
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- "main"
|
- "main"
|
||||||
- "release/*"
|
- "release/*"
|
||||||
paths:
|
paths:
|
||||||
- "frontend/app_flowy/**"
|
- "frontend/appflowy_flutter/**"
|
||||||
|
|
||||||
env:
|
env:
|
||||||
CARGO_TERM_COLOR: always
|
CARGO_TERM_COLOR: always
|
||||||
@ -100,14 +100,14 @@ jobs:
|
|||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
- name: Flutter Code Generation
|
- name: Flutter Code Generation
|
||||||
working-directory: frontend/app_flowy
|
working-directory: frontend/appflowy_flutter
|
||||||
run: |
|
run: |
|
||||||
flutter packages pub get
|
flutter packages pub get
|
||||||
flutter packages pub run easy_localization:generate -f keys -o locale_keys.g.dart -S assets/translations -s en.json
|
flutter packages pub run easy_localization:generate -f keys -o locale_keys.g.dart -S assets/translations -s en.json
|
||||||
flutter packages pub run build_runner build --delete-conflicting-outputs
|
flutter packages pub run build_runner build --delete-conflicting-outputs
|
||||||
|
|
||||||
- name: Run AppFlowy tests
|
- name: Run AppFlowy tests
|
||||||
working-directory: frontend/app_flowy
|
working-directory: frontend/appflowy_flutter
|
||||||
run: |
|
run: |
|
||||||
if [ "$RUNNER_OS" == "Linux" ]; then
|
if [ "$RUNNER_OS" == "Linux" ]; then
|
||||||
flutter test integration_test -d Linux --coverage
|
flutter test integration_test -d Linux --coverage
|
||||||
|
6
.github/workflows/release.yml
vendored
6
.github/workflows/release.yml
vendored
@ -39,7 +39,7 @@ jobs:
|
|||||||
name: ${{ matrix.job.target }} (${{ matrix.job.os }})
|
name: ${{ matrix.job.target }} (${{ matrix.job.os }})
|
||||||
needs: create-release
|
needs: create-release
|
||||||
env:
|
env:
|
||||||
WINDOWS_APP_RELEASE_PATH: frontend\app_flowy\product\${{ github.ref_name }}\windows
|
WINDOWS_APP_RELEASE_PATH: frontend\appflowy_flutter\product\${{ github.ref_name }}\windows
|
||||||
WINDOWS_ZIP_NAME: AppFlowy_${{ github.ref_name }}_windows-x86_64.zip
|
WINDOWS_ZIP_NAME: AppFlowy_${{ github.ref_name }}_windows-x86_64.zip
|
||||||
WINDOWS_INSTALLER_NAME: AppFlowy_${{ github.ref_name }}_windows-x86_64
|
WINDOWS_INSTALLER_NAME: AppFlowy_${{ github.ref_name }}_windows-x86_64
|
||||||
runs-on: ${{ matrix.job.os }}
|
runs-on: ${{ matrix.job.os }}
|
||||||
@ -129,7 +129,7 @@ jobs:
|
|||||||
runs-on: ${{ matrix.job.os }}
|
runs-on: ${{ matrix.job.os }}
|
||||||
needs: create-release
|
needs: create-release
|
||||||
env:
|
env:
|
||||||
MACOS_APP_RELEASE_PATH: frontend/app_flowy/product/${{ github.ref_name }}/macos/Release
|
MACOS_APP_RELEASE_PATH: frontend/appflowy_flutter/product/${{ github.ref_name }}/macos/Release
|
||||||
MACOS_X86_ZIP_NAME: AppFlowy_${{ github.ref_name }}_macos-x86_64.zip
|
MACOS_X86_ZIP_NAME: AppFlowy_${{ github.ref_name }}_macos-x86_64.zip
|
||||||
MACOS_DMG_NAME: AppFlowy_${{ github.ref_name }}_macos-x86_64
|
MACOS_DMG_NAME: AppFlowy_${{ github.ref_name }}_macos-x86_64
|
||||||
strategy:
|
strategy:
|
||||||
@ -217,7 +217,7 @@ jobs:
|
|||||||
runs-on: ${{ matrix.job.os }}
|
runs-on: ${{ matrix.job.os }}
|
||||||
needs: create-release
|
needs: create-release
|
||||||
env:
|
env:
|
||||||
LINUX_APP_RELEASE_PATH: frontend/app_flowy/product/${{ github.ref_name }}/linux/Release
|
LINUX_APP_RELEASE_PATH: frontend/appflowy_flutter/product/${{ github.ref_name }}/linux/Release
|
||||||
LINUX_ZIP_NAME: AppFlowy_${{ matrix.job.target }}_${{ matrix.job.os }}.tar.gz
|
LINUX_ZIP_NAME: AppFlowy_${{ matrix.job.target }}_${{ matrix.job.os }}.tar.gz
|
||||||
LINUX_PACKAGE_NAME: AppFlowy_${{ github.ref_name }}_${{ matrix.job.os }}.deb
|
LINUX_PACKAGE_NAME: AppFlowy_${{ github.ref_name }}_${{ matrix.job.os }}.deb
|
||||||
# PKG_CONFIG_SYSROOT_DIR: /
|
# PKG_CONFIG_SYSROOT_DIR: /
|
||||||
|
2
.github/workflows/translation_notify.yml
vendored
2
.github/workflows/translation_notify.yml
vendored
@ -3,7 +3,7 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches: [ main ]
|
branches: [ main ]
|
||||||
paths:
|
paths:
|
||||||
- "frontend/app_flowy/assets/translations/en.json"
|
- "frontend/appflowy_flutter/assets/translations/en.json"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
Discord-Notify:
|
Discord-Notify:
|
||||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -29,7 +29,7 @@ node_modules
|
|||||||
|
|
||||||
# Commit the highest level pubspec.lock, but ignore the others
|
# Commit the highest level pubspec.lock, but ignore the others
|
||||||
pubspec.lock
|
pubspec.lock
|
||||||
!frontend/app_flowy/pubspec.lock
|
!frontend/appflowy_flutter/pubspec.lock
|
||||||
|
|
||||||
# ignore tool used for commit linting
|
# ignore tool used for commit linting
|
||||||
.githooks/gitlint
|
.githooks/gitlint
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
<env name="flowy_tool" value="${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/scripts/flowy-tool/Cargo.toml" />
|
<env name="flowy_tool" value="${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/scripts/flowy-tool/Cargo.toml" />
|
||||||
<env name="rust_lib" value="${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/rust-lib/" />
|
<env name="rust_lib" value="${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/rust-lib/" />
|
||||||
<env name="shared_lib" value="${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/../shared_lib" />
|
<env name="shared_lib" value="${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/../shared_lib" />
|
||||||
<env name="flutter_lib" value="${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/app_flowy/packages" />
|
<env name="flutter_lib" value="${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/appflowy_flutter/packages" />
|
||||||
<env name="derive_meta" value="${shared_lib}/flowy-derive/src/derive_cache/derive_cache.rs" />
|
<env name="derive_meta" value="${shared_lib}/flowy-derive/src/derive_cache/derive_cache.rs" />
|
||||||
<env name="flutter_package_lib" value="${flutter_lib}/flowy_sdk/lib" />
|
<env name="flutter_package_lib" value="${flutter_lib}/flowy_sdk/lib" />
|
||||||
</envs>
|
</envs>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<component name="ProjectRunConfigurationManager">
|
<component name="ProjectRunConfigurationManager">
|
||||||
<configuration default="false" name="dart-event" type="CargoCommandRunConfiguration" factoryName="Cargo Command">
|
<configuration default="false" name="dart-event" type="CargoCommandRunConfiguration" factoryName="Cargo Command">
|
||||||
<option name="command" value="run --manifest-path $PROJECT_DIR$/scripts/flowy-tool/Cargo.toml -- dart-event --rust_source=$PROJECT_DIR$/rust-lib/ --output=$PROJECT_DIR$/app_flowy/packages/flowy_sdk/lib/dispatch/dart_event.dart" />
|
<option name="command" value="run --manifest-path $PROJECT_DIR$/scripts/flowy-tool/Cargo.toml -- dart-event --rust_source=$PROJECT_DIR$/rust-lib/ --output=$PROJECT_DIR$/appflowy_flutter/packages/flowy_sdk/lib/dispatch/dart_event.dart" />
|
||||||
<option name="workingDirectory" value="file://$PROJECT_DIR$" />
|
<option name="workingDirectory" value="file://$PROJECT_DIR$" />
|
||||||
<option name="channel" value="DEFAULT" />
|
<option name="channel" value="DEFAULT" />
|
||||||
<option name="allFeatures" value="false" />
|
<option name="allFeatures" value="false" />
|
||||||
|
12
frontend/.vscode/launch.json
vendored
12
frontend/.vscode/launch.json
vendored
@ -15,7 +15,7 @@
|
|||||||
"RUST_LOG": "trace",
|
"RUST_LOG": "trace",
|
||||||
// "RUST_LOG": "debug"
|
// "RUST_LOG": "debug"
|
||||||
},
|
},
|
||||||
"cwd": "${workspaceRoot}/app_flowy"
|
"cwd": "${workspaceRoot}/appflowy_flutter"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// This task only builds the Dart code of AppFlowy.
|
// This task only builds the Dart code of AppFlowy.
|
||||||
@ -26,7 +26,7 @@
|
|||||||
"env": {
|
"env": {
|
||||||
"RUST_LOG": "debug"
|
"RUST_LOG": "debug"
|
||||||
},
|
},
|
||||||
"cwd": "${workspaceRoot}/app_flowy"
|
"cwd": "${workspaceRoot}/appflowy_flutter"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// This task builds will:
|
// This task builds will:
|
||||||
@ -41,7 +41,7 @@
|
|||||||
"env": {
|
"env": {
|
||||||
"RUST_LOG": "trace"
|
"RUST_LOG": "trace"
|
||||||
},
|
},
|
||||||
"cwd": "${workspaceRoot}/app_flowy"
|
"cwd": "${workspaceRoot}/appflowy_flutter"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "AF-desktop: Debug Rust",
|
"name": "AF-desktop: Debug Rust",
|
||||||
@ -55,7 +55,7 @@
|
|||||||
// "program": "./lib/main.dart",
|
// "program": "./lib/main.dart",
|
||||||
// "type": "dart",
|
// "type": "dart",
|
||||||
// "flutterMode": "profile",
|
// "flutterMode": "profile",
|
||||||
// "cwd": "${workspaceRoot}/app_flowy"
|
// "cwd": "${workspaceRoot}/appflowy_flutter"
|
||||||
// },
|
// },
|
||||||
{
|
{
|
||||||
// This task builds the Rust and Dart code of AppFlowy for android.
|
// This task builds the Rust and Dart code of AppFlowy for android.
|
||||||
@ -67,7 +67,7 @@
|
|||||||
"env": {
|
"env": {
|
||||||
"RUST_LOG": "info"
|
"RUST_LOG": "info"
|
||||||
},
|
},
|
||||||
"cwd": "${workspaceRoot}/app_flowy"
|
"cwd": "${workspaceRoot}/appflowy_flutter"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// This task builds will:
|
// This task builds will:
|
||||||
@ -82,7 +82,7 @@
|
|||||||
"env": {
|
"env": {
|
||||||
"RUST_LOG": "info"
|
"RUST_LOG": "info"
|
||||||
},
|
},
|
||||||
"cwd": "${workspaceRoot}/app_flowy"
|
"cwd": "${workspaceRoot}/appflowy_flutter"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// https://tauri.app/v1/guides/debugging/vs-code
|
// https://tauri.app/v1/guides/debugging/vs-code
|
||||||
|
10
frontend/.vscode/tasks.json
vendored
10
frontend/.vscode/tasks.json
vendored
@ -96,7 +96,7 @@
|
|||||||
"type": "shell",
|
"type": "shell",
|
||||||
"command": "flutter clean",
|
"command": "flutter clean",
|
||||||
"options": {
|
"options": {
|
||||||
"cwd": "${workspaceFolder}/app_flowy"
|
"cwd": "${workspaceFolder}/appflowy_flutter"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -104,7 +104,7 @@
|
|||||||
"type": "shell",
|
"type": "shell",
|
||||||
"command": "flutter pub get",
|
"command": "flutter pub get",
|
||||||
"options": {
|
"options": {
|
||||||
"cwd": "${workspaceFolder}/app_flowy"
|
"cwd": "${workspaceFolder}/appflowy_flutter"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -112,7 +112,7 @@
|
|||||||
"type": "shell",
|
"type": "shell",
|
||||||
"command": "flutter packages pub get",
|
"command": "flutter packages pub get",
|
||||||
"options": {
|
"options": {
|
||||||
"cwd": "${workspaceFolder}/app_flowy"
|
"cwd": "${workspaceFolder}/appflowy_flutter"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -120,7 +120,7 @@
|
|||||||
"type": "shell",
|
"type": "shell",
|
||||||
"command": "flutter pub run build_runner build --delete-conflicting-outputs",
|
"command": "flutter pub run build_runner build --delete-conflicting-outputs",
|
||||||
"options": {
|
"options": {
|
||||||
"cwd": "${workspaceFolder}/app_flowy"
|
"cwd": "${workspaceFolder}/appflowy_flutter"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -163,7 +163,7 @@
|
|||||||
],
|
],
|
||||||
"group": "build",
|
"group": "build",
|
||||||
"problemMatcher": [],
|
"problemMatcher": [],
|
||||||
"detail": "app_flowy"
|
"detail": "appflowy_flutter"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "AF: Tauri UI Dev",
|
"label": "AF: Tauri UI Dev",
|
||||||
|
@ -43,7 +43,7 @@ PRODUCT_NAME = "AppFlowy"
|
|||||||
CRATE_TYPE = "staticlib"
|
CRATE_TYPE = "staticlib"
|
||||||
LIB_EXT = "a"
|
LIB_EXT = "a"
|
||||||
APP_ENVIRONMENT = "local"
|
APP_ENVIRONMENT = "local"
|
||||||
FLUTTER_FLOWY_SDK_PATH = "app_flowy/packages/appflowy_backend"
|
FLUTTER_FLOWY_SDK_PATH = "appflowy_flutter/packages/appflowy_backend"
|
||||||
TAURI_BACKEND_SERVICE_PATH = "appflowy_tauri/src/services/backend"
|
TAURI_BACKEND_SERVICE_PATH = "appflowy_tauri/src/services/backend"
|
||||||
# Test default config
|
# Test default config
|
||||||
TEST_CRATE_TYPE = "cdylib"
|
TEST_CRATE_TYPE = "cdylib"
|
||||||
|
@ -1,21 +0,0 @@
|
|||||||
# app_flowy
|
|
||||||
|
|
||||||
A new Flutter project.
|
|
||||||
|
|
||||||
## Getting Started
|
|
||||||
|
|
||||||
This project is a starting point for a Flutter application.
|
|
||||||
|
|
||||||
A few resources to get you started if this is your first Flutter project:
|
|
||||||
|
|
||||||
- [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab)
|
|
||||||
- [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook)
|
|
||||||
|
|
||||||
For help getting started with Flutter, view our
|
|
||||||
[online documentation](https://flutter.dev/docs), which offers tutorials,
|
|
||||||
samples, guidance on mobile development, and a full API reference.
|
|
||||||
|
|
||||||
|
|
||||||
## release check
|
|
||||||
1. [entitlements](https://flutter.dev/desktop#setting-up-entitlements)
|
|
||||||
2. [symbols stripped](https://flutter.dev/docs/development/platform-integration/c-interop)
|
|
@ -1,75 +0,0 @@
|
|||||||
def localProperties = new Properties()
|
|
||||||
def localPropertiesFile = rootProject.file('local.properties')
|
|
||||||
if (localPropertiesFile.exists()) {
|
|
||||||
localPropertiesFile.withReader('UTF-8') { reader ->
|
|
||||||
localProperties.load(reader)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def flutterRoot = localProperties.getProperty('flutter.sdk')
|
|
||||||
if (flutterRoot == null) {
|
|
||||||
throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
|
|
||||||
}
|
|
||||||
|
|
||||||
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
|
|
||||||
if (flutterVersionCode == null) {
|
|
||||||
flutterVersionCode = '1'
|
|
||||||
}
|
|
||||||
|
|
||||||
def flutterVersionName = localProperties.getProperty('flutter.versionName')
|
|
||||||
if (flutterVersionName == null) {
|
|
||||||
flutterVersionName = '1.0'
|
|
||||||
}
|
|
||||||
|
|
||||||
apply plugin: 'com.android.application'
|
|
||||||
apply plugin: 'kotlin-android'
|
|
||||||
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
|
|
||||||
|
|
||||||
android {
|
|
||||||
compileSdkVersion 31
|
|
||||||
ndkVersion "24.0.8215888"
|
|
||||||
|
|
||||||
compileOptions {
|
|
||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
|
||||||
targetCompatibility JavaVersion.VERSION_1_8
|
|
||||||
}
|
|
||||||
|
|
||||||
kotlinOptions {
|
|
||||||
jvmTarget = '1.8'
|
|
||||||
}
|
|
||||||
|
|
||||||
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 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
flutter {
|
|
||||||
source '../..'
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
|
||||||
implementation "com.android.support:multidex:2.0.1"
|
|
||||||
}
|
|
@ -1,7 +0,0 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
package="com.example.app_flowy">
|
|
||||||
<!-- Flutter needs it to communicate with the running application
|
|
||||||
to allow setting breakpoints, to provide hot reload, etc.
|
|
||||||
-->
|
|
||||||
<uses-permission android:name="android.permission.INTERNET"/>
|
|
||||||
</manifest>
|
|
@ -1,42 +0,0 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
package="com.example.app_flowy">
|
|
||||||
<application
|
|
||||||
android:label="app_flowy"
|
|
||||||
android:icon="@mipmap/ic_launcher"
|
|
||||||
android:name="${applicationName}">
|
|
||||||
<activity
|
|
||||||
android:name=".MainActivity"
|
|
||||||
android:launchMode="singleTop"
|
|
||||||
android:theme="@style/LaunchTheme"
|
|
||||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
|
||||||
android:hardwareAccelerated="true"
|
|
||||||
android:windowSoftInputMode="adjustResize">
|
|
||||||
<!-- Specifies an Android theme to apply to this Activity as soon as
|
|
||||||
the Android process has started. This theme is visible to the user
|
|
||||||
while the Flutter UI initializes. After that, this theme continues
|
|
||||||
to determine the Window background behind the Flutter UI. -->
|
|
||||||
<meta-data
|
|
||||||
android:name="io.flutter.embedding.android.NormalTheme"
|
|
||||||
android:resource="@style/NormalTheme"
|
|
||||||
/>
|
|
||||||
<!-- Displays an Android View that continues showing the launch screen
|
|
||||||
Drawable until Flutter paints its first frame, then this splash
|
|
||||||
screen fades out. A splash screen is useful to avoid any visual
|
|
||||||
gap between the end of Android's launch screen and the painting of
|
|
||||||
Flutter's first frame. -->
|
|
||||||
<meta-data
|
|
||||||
android:name="io.flutter.embedding.android.SplashScreenDrawable"
|
|
||||||
android:resource="@drawable/launch_background"
|
|
||||||
/>
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.MAIN"/>
|
|
||||||
<category android:name="android.intent.category.LAUNCHER"/>
|
|
||||||
</intent-filter>
|
|
||||||
</activity>
|
|
||||||
<!-- Don't delete the meta-data below.
|
|
||||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
|
||||||
<meta-data
|
|
||||||
android:name="flutterEmbedding"
|
|
||||||
android:value="2" />
|
|
||||||
</application>
|
|
||||||
</manifest>
|
|
@ -1,6 +0,0 @@
|
|||||||
package com.example.app_flowy
|
|
||||||
|
|
||||||
import io.flutter.embedding.android.FlutterActivity
|
|
||||||
|
|
||||||
class MainActivity: FlutterActivity() {
|
|
||||||
}
|
|
@ -1,7 +0,0 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
package="com.example.app_flowy">
|
|
||||||
<!-- Flutter needs it to communicate with the running application
|
|
||||||
to allow setting breakpoints, to provide hot reload, etc.
|
|
||||||
-->
|
|
||||||
<uses-permission android:name="android.permission.INTERNET"/>
|
|
||||||
</manifest>
|
|
@ -1,161 +0,0 @@
|
|||||||
import 'package:app_flowy/user/presentation/folder/folder_widget.dart';
|
|
||||||
import 'package:flowy_infra_ui/style_widget/text_field.dart';
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
|
||||||
import 'package:integration_test/integration_test.dart';
|
|
||||||
|
|
||||||
import 'util/mock/mock_file_picker.dart';
|
|
||||||
import 'util/util.dart';
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
|
||||||
|
|
||||||
group('customize the folder path', () {
|
|
||||||
const location = 'appflowy';
|
|
||||||
|
|
||||||
setUp(() async {
|
|
||||||
await TestFolder.cleanTestLocation(location);
|
|
||||||
await TestFolder.setTestLocation(location);
|
|
||||||
});
|
|
||||||
|
|
||||||
tearDown(() async {
|
|
||||||
await TestFolder.cleanTestLocation(location);
|
|
||||||
});
|
|
||||||
|
|
||||||
tearDownAll(() async {
|
|
||||||
await TestFolder.cleanTestLocation(null);
|
|
||||||
});
|
|
||||||
|
|
||||||
testWidgets(
|
|
||||||
'customize folder name and path when launching app in first time',
|
|
||||||
(tester) async {
|
|
||||||
const folderName = 'appflowy';
|
|
||||||
await TestFolder.cleanTestLocation(folderName);
|
|
||||||
|
|
||||||
await tester.initializeAppFlowy();
|
|
||||||
|
|
||||||
// Click create button
|
|
||||||
await tester.tapCreateButton();
|
|
||||||
|
|
||||||
// Set directory
|
|
||||||
final cfw = find.byType(CreateFolderWidget);
|
|
||||||
expect(cfw, findsOneWidget);
|
|
||||||
final state = tester.state(cfw) as CreateFolderWidgetState;
|
|
||||||
final dir = await TestFolder.testLocation(null);
|
|
||||||
state.directory = dir.path;
|
|
||||||
|
|
||||||
// input folder name
|
|
||||||
final ftf = find.byType(FlowyTextField);
|
|
||||||
expect(ftf, findsOneWidget);
|
|
||||||
await tester.enterText(ftf, 'appflowy');
|
|
||||||
|
|
||||||
// Click create button again
|
|
||||||
await tester.tapCreateButton();
|
|
||||||
|
|
||||||
await tester.expectToSeeWelcomePage();
|
|
||||||
|
|
||||||
await TestFolder.cleanTestLocation(folderName);
|
|
||||||
});
|
|
||||||
|
|
||||||
testWidgets('open a new folder when launching app in first time',
|
|
||||||
(tester) async {
|
|
||||||
const folderName = 'appflowy';
|
|
||||||
await TestFolder.cleanTestLocation(folderName);
|
|
||||||
await TestFolder.setTestLocation(folderName);
|
|
||||||
|
|
||||||
await tester.initializeAppFlowy();
|
|
||||||
|
|
||||||
// tap open button
|
|
||||||
await mockGetDirectoryPath(folderName);
|
|
||||||
await tester.tapOpenFolderButton();
|
|
||||||
|
|
||||||
await tester.wait(1000);
|
|
||||||
await tester.expectToSeeWelcomePage();
|
|
||||||
|
|
||||||
await TestFolder.cleanTestLocation(folderName);
|
|
||||||
});
|
|
||||||
|
|
||||||
testWidgets('switch to B from A, then switch to A again', (tester) async {
|
|
||||||
const String userA = 'userA';
|
|
||||||
const String userB = 'userB';
|
|
||||||
|
|
||||||
await TestFolder.cleanTestLocation(userA);
|
|
||||||
await TestFolder.setTestLocation(userA);
|
|
||||||
|
|
||||||
await tester.initializeAppFlowy();
|
|
||||||
|
|
||||||
await tester.tapGoButton();
|
|
||||||
await tester.expectToSeeWelcomePage();
|
|
||||||
|
|
||||||
// swith to user B
|
|
||||||
{
|
|
||||||
await tester.openSettings();
|
|
||||||
await tester.openSettingsPage(SettingsPage.user);
|
|
||||||
await tester.enterUserName(userA);
|
|
||||||
|
|
||||||
await tester.openSettingsPage(SettingsPage.files);
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
|
|
||||||
// mock the file_picker result
|
|
||||||
await mockGetDirectoryPath(userB);
|
|
||||||
await tester.tapCustomLocationButton();
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
await tester.expectToSeeWelcomePage();
|
|
||||||
}
|
|
||||||
|
|
||||||
// switch to the userA
|
|
||||||
{
|
|
||||||
await tester.openSettings();
|
|
||||||
await tester.openSettingsPage(SettingsPage.user);
|
|
||||||
await tester.enterUserName(userB);
|
|
||||||
|
|
||||||
await tester.openSettingsPage(SettingsPage.files);
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
|
|
||||||
// mock the file_picker result
|
|
||||||
await mockGetDirectoryPath(userA);
|
|
||||||
await tester.tapCustomLocationButton();
|
|
||||||
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
await tester.expectToSeeWelcomePage();
|
|
||||||
expect(find.textContaining(userA), findsOneWidget);
|
|
||||||
}
|
|
||||||
|
|
||||||
// swith to the userB again
|
|
||||||
{
|
|
||||||
await tester.openSettings();
|
|
||||||
await tester.openSettingsPage(SettingsPage.files);
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
|
|
||||||
// mock the file_picker result
|
|
||||||
await mockGetDirectoryPath(userB);
|
|
||||||
await tester.tapCustomLocationButton();
|
|
||||||
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
await tester.expectToSeeWelcomePage();
|
|
||||||
expect(find.textContaining(userB), findsOneWidget);
|
|
||||||
}
|
|
||||||
|
|
||||||
await TestFolder.cleanTestLocation(userA);
|
|
||||||
await TestFolder.cleanTestLocation(userB);
|
|
||||||
});
|
|
||||||
|
|
||||||
testWidgets('reset to default location', (tester) async {
|
|
||||||
await tester.initializeAppFlowy();
|
|
||||||
|
|
||||||
await tester.tapGoButton();
|
|
||||||
|
|
||||||
// home and readme document
|
|
||||||
await tester.expectToSeeWelcomePage();
|
|
||||||
|
|
||||||
// open settings and restore the location
|
|
||||||
await tester.openSettings();
|
|
||||||
await tester.openSettingsPage(SettingsPage.files);
|
|
||||||
await tester.restoreLocation();
|
|
||||||
|
|
||||||
expect(
|
|
||||||
await TestFolder.defaultDevelopmentLocation(),
|
|
||||||
await TestFolder.currentLocation(),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
@ -1,114 +0,0 @@
|
|||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:app_flowy/main.dart' as app;
|
|
||||||
import 'package:app_flowy/startup/tasks/prelude.dart';
|
|
||||||
import 'package:app_flowy/workspace/application/settings/settings_location_cubit.dart';
|
|
||||||
import 'package:flutter/gestures.dart';
|
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
|
||||||
import 'package:path_provider/path_provider.dart';
|
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
|
||||||
|
|
||||||
class TestFolder {
|
|
||||||
/// Location / Path
|
|
||||||
|
|
||||||
/// Set a given AppFlowy data storage location under test environment.
|
|
||||||
///
|
|
||||||
/// To pass null means clear the location.
|
|
||||||
///
|
|
||||||
/// The file_picker is a system component and can't be tapped, so using logic instead of tapping.
|
|
||||||
///
|
|
||||||
static Future<void> setTestLocation(String? name) async {
|
|
||||||
final location = await testLocation(name);
|
|
||||||
SharedPreferences.setMockInitialValues({
|
|
||||||
kSettingsLocationDefaultLocation: location.path,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Clean the location.
|
|
||||||
static Future<void> cleanTestLocation(String? name) async {
|
|
||||||
final dir = await testLocation(name);
|
|
||||||
await dir.delete(recursive: true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get current using location.
|
|
||||||
static Future<String> currentLocation() async {
|
|
||||||
final prefs = await SharedPreferences.getInstance();
|
|
||||||
return prefs.getString(kSettingsLocationDefaultLocation)!;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get default location under development environment.
|
|
||||||
static Future<String> defaultDevelopmentLocation() async {
|
|
||||||
final dir = await appFlowyDocumentDirectory();
|
|
||||||
return dir.path;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get default location under test environment.
|
|
||||||
static Future<Directory> testLocation(String? name) async {
|
|
||||||
final dir = await getApplicationDocumentsDirectory();
|
|
||||||
var path = '${dir.path}/flowy_test';
|
|
||||||
if (name != null) {
|
|
||||||
path += '/$name';
|
|
||||||
}
|
|
||||||
return Directory(path).create(recursive: true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension AppFlowyTestBase on WidgetTester {
|
|
||||||
Future<void> initializeAppFlowy() async {
|
|
||||||
const MethodChannel('hotkey_manager')
|
|
||||||
.setMockMethodCallHandler((MethodCall methodCall) async {
|
|
||||||
if (methodCall.method == 'unregisterAll') {
|
|
||||||
// do nothing
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
await app.main();
|
|
||||||
await wait(3000);
|
|
||||||
await pumpAndSettle(const Duration(seconds: 2));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> tapButton(
|
|
||||||
Finder finder, {
|
|
||||||
int? pointer,
|
|
||||||
int buttons = kPrimaryButton,
|
|
||||||
bool warnIfMissed = true,
|
|
||||||
int milliseconds = 500,
|
|
||||||
}) async {
|
|
||||||
await tap(finder);
|
|
||||||
await pumpAndSettle(Duration(milliseconds: milliseconds));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> tapButtonWithName(
|
|
||||||
String tr, {
|
|
||||||
int milliseconds = 500,
|
|
||||||
}) async {
|
|
||||||
final button = find.text(tr);
|
|
||||||
await tapButton(
|
|
||||||
button,
|
|
||||||
milliseconds: milliseconds,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> tapButtonWithTooltip(
|
|
||||||
String tr, {
|
|
||||||
int milliseconds = 500,
|
|
||||||
}) async {
|
|
||||||
final button = find.byTooltip(tr);
|
|
||||||
await tapButton(
|
|
||||||
button,
|
|
||||||
milliseconds: milliseconds,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> wait(int milliseconds) async {
|
|
||||||
await pumpAndSettle(Duration(milliseconds: milliseconds));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,23 +0,0 @@
|
|||||||
import 'package:app_flowy/generated/locale_keys.g.dart';
|
|
||||||
import 'package:app_flowy/workspace/presentation/home/home_stack.dart';
|
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
|
||||||
|
|
||||||
import 'base.dart';
|
|
||||||
|
|
||||||
extension AppFlowyLaunch on WidgetTester {
|
|
||||||
Future<void> tapGoButton() async {
|
|
||||||
await tapButtonWithName(LocaleKeys.letsGoButtonText.tr());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> tapCreateButton() async {
|
|
||||||
await tapButtonWithName(LocaleKeys.settings_files_create.tr());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> expectToSeeWelcomePage() async {
|
|
||||||
expect(find.byType(HomeStack), findsOneWidget);
|
|
||||||
expect(find.textContaining('Read me'), findsNWidgets(2));
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,29 +0,0 @@
|
|||||||
import 'package:app_flowy/startup/startup.dart';
|
|
||||||
import 'package:app_flowy/util/file_picker/file_picker_impl.dart';
|
|
||||||
import 'package:app_flowy/util/file_picker/file_picker_service.dart';
|
|
||||||
|
|
||||||
import '../util.dart';
|
|
||||||
|
|
||||||
class MockFilePicker extends FilePicker {
|
|
||||||
MockFilePicker({
|
|
||||||
required this.mockPath,
|
|
||||||
});
|
|
||||||
|
|
||||||
final String mockPath;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<String?> getDirectoryPath({String? title}) {
|
|
||||||
return Future.value(mockPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> mockGetDirectoryPath(String? name) async {
|
|
||||||
final dir = await TestFolder.testLocation(name);
|
|
||||||
getIt.unregister<FilePickerService>();
|
|
||||||
getIt.registerFactory<FilePickerService>(
|
|
||||||
() => MockFilePicker(
|
|
||||||
mockPath: dir.path,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
@ -1,84 +0,0 @@
|
|||||||
import 'package:app_flowy/generated/locale_keys.g.dart';
|
|
||||||
import 'package:app_flowy/workspace/presentation/settings/settings_dialog.dart';
|
|
||||||
import 'package:app_flowy/workspace/presentation/settings/widgets/settings_user_view.dart';
|
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
|
||||||
|
|
||||||
import 'base.dart';
|
|
||||||
|
|
||||||
enum SettingsPage {
|
|
||||||
appearance,
|
|
||||||
language,
|
|
||||||
files,
|
|
||||||
user,
|
|
||||||
}
|
|
||||||
|
|
||||||
extension on SettingsPage {
|
|
||||||
String get name {
|
|
||||||
switch (this) {
|
|
||||||
case SettingsPage.appearance:
|
|
||||||
return LocaleKeys.settings_menu_appearance.tr();
|
|
||||||
case SettingsPage.language:
|
|
||||||
return LocaleKeys.settings_menu_language.tr();
|
|
||||||
case SettingsPage.files:
|
|
||||||
return LocaleKeys.settings_menu_files.tr();
|
|
||||||
case SettingsPage.user:
|
|
||||||
return LocaleKeys.settings_menu_user.tr();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension AppFlowySettings on WidgetTester {
|
|
||||||
/// Open settings page
|
|
||||||
Future<void> openSettings() async {
|
|
||||||
final settingsButton = find.byTooltip(LocaleKeys.settings_menu_open.tr());
|
|
||||||
expect(settingsButton, findsOneWidget);
|
|
||||||
await tapButton(settingsButton);
|
|
||||||
final settingsDialog = find.byType(SettingsDialog);
|
|
||||||
expect(settingsDialog, findsOneWidget);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Open the page taht insides the settings page
|
|
||||||
Future<void> openSettingsPage(SettingsPage page) async {
|
|
||||||
final button = find.text(page.name, findRichText: true);
|
|
||||||
expect(button, findsOneWidget);
|
|
||||||
await tapButton(button);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Restore the AppFlowy data storage location
|
|
||||||
Future<void> restoreLocation() async {
|
|
||||||
final buton =
|
|
||||||
find.byTooltip(LocaleKeys.settings_files_restoreLocation.tr());
|
|
||||||
expect(buton, findsOneWidget);
|
|
||||||
await tapButton(buton);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> tapOpenFolderButton() async {
|
|
||||||
final buton = find.text(LocaleKeys.settings_files_open.tr());
|
|
||||||
expect(buton, findsOneWidget);
|
|
||||||
await tapButton(buton);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> tapCustomLocationButton() async {
|
|
||||||
final buton =
|
|
||||||
find.byTooltip(LocaleKeys.settings_files_customizeLocation.tr());
|
|
||||||
expect(buton, findsOneWidget);
|
|
||||||
await tapButton(buton);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Enter user name
|
|
||||||
Future<void> enterUserName(String name) async {
|
|
||||||
final uni = find.byType(UserNameInput);
|
|
||||||
expect(uni, findsOneWidget);
|
|
||||||
await tap(uni);
|
|
||||||
await enterText(uni, name);
|
|
||||||
await wait(300); //
|
|
||||||
await testTextInput.receiveAction(TextInputAction.done);
|
|
||||||
await pumpAndSettle();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,51 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
||||||
<plist version="1.0">
|
|
||||||
<dict>
|
|
||||||
<key>CFBundleDevelopmentRegion</key>
|
|
||||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
|
||||||
<key>CFBundleExecutable</key>
|
|
||||||
<string>$(EXECUTABLE_NAME)</string>
|
|
||||||
<key>CFBundleIdentifier</key>
|
|
||||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
|
||||||
<key>CFBundleInfoDictionaryVersion</key>
|
|
||||||
<string>6.0</string>
|
|
||||||
<key>CFBundleName</key>
|
|
||||||
<string>app_flowy</string>
|
|
||||||
<key>CFBundlePackageType</key>
|
|
||||||
<string>APPL</string>
|
|
||||||
<key>CFBundleShortVersionString</key>
|
|
||||||
<string>$(FLUTTER_BUILD_NAME)</string>
|
|
||||||
<key>CFBundleSignature</key>
|
|
||||||
<string>????</string>
|
|
||||||
<key>CFBundleVersion</key>
|
|
||||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
|
||||||
<key>LSRequiresIPhoneOS</key>
|
|
||||||
<true/>
|
|
||||||
<key>UILaunchStoryboardName</key>
|
|
||||||
<string>LaunchScreen</string>
|
|
||||||
<key>UIMainStoryboardFile</key>
|
|
||||||
<string>Main</string>
|
|
||||||
<key>UISupportedInterfaceOrientations</key>
|
|
||||||
<array>
|
|
||||||
<string>UIInterfaceOrientationPortrait</string>
|
|
||||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
|
||||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
|
||||||
</array>
|
|
||||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
|
||||||
<array>
|
|
||||||
<string>UIInterfaceOrientationPortrait</string>
|
|
||||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
|
||||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
|
||||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
|
||||||
</array>
|
|
||||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
|
||||||
<false/>
|
|
||||||
<key>CFBundleLocalizations</key>
|
|
||||||
<array>
|
|
||||||
<string>en</string>
|
|
||||||
</array>
|
|
||||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
|
||||||
<true/>
|
|
||||||
</dict>
|
|
||||||
</plist>
|
|
@ -1,71 +0,0 @@
|
|||||||
import 'package:app_flowy/workspace/presentation/home/home_stack.dart';
|
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
|
||||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:app_flowy/generated/locale_keys.g.dart';
|
|
||||||
import 'package:app_flowy/startup/plugin/plugin.dart';
|
|
||||||
|
|
||||||
class BlankPluginBuilder extends PluginBuilder {
|
|
||||||
@override
|
|
||||||
Plugin build(dynamic data) {
|
|
||||||
return BlankPagePlugin();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get menuName => "Blank";
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get menuIcon => "";
|
|
||||||
|
|
||||||
@override
|
|
||||||
PluginType get pluginType => PluginType.blank;
|
|
||||||
}
|
|
||||||
|
|
||||||
class BlankPluginConfig implements PluginConfig {
|
|
||||||
@override
|
|
||||||
bool get creatable => false;
|
|
||||||
}
|
|
||||||
|
|
||||||
class BlankPagePlugin extends Plugin {
|
|
||||||
@override
|
|
||||||
PluginDisplay get display => BlankPagePluginDisplay();
|
|
||||||
|
|
||||||
@override
|
|
||||||
PluginId get id => "BlankStack";
|
|
||||||
|
|
||||||
@override
|
|
||||||
PluginType get ty => PluginType.blank;
|
|
||||||
}
|
|
||||||
|
|
||||||
class BlankPagePluginDisplay extends PluginDisplay with NavigationItem {
|
|
||||||
@override
|
|
||||||
Widget get leftBarItem => FlowyText.medium(LocaleKeys.blankPageTitle.tr());
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget buildWidget(PluginContext context) => const BlankPage();
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<NavigationItem> get navigationItems => [this];
|
|
||||||
}
|
|
||||||
|
|
||||||
class BlankPage extends StatefulWidget {
|
|
||||||
const BlankPage({Key? key}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<BlankPage> createState() => _BlankPageState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _BlankPageState extends State<BlankPage> {
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return SizedBox.expand(
|
|
||||||
child: Container(
|
|
||||||
color: Theme.of(context).colorScheme.surface,
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(10),
|
|
||||||
child: Container(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,512 +0,0 @@
|
|||||||
import 'dart:async';
|
|
||||||
import 'dart:collection';
|
|
||||||
|
|
||||||
import 'package:app_flowy/plugins/grid/application/field/field_controller.dart';
|
|
||||||
import 'package:app_flowy/plugins/grid/application/row/row_cache.dart';
|
|
||||||
import 'package:app_flowy/plugins/grid/application/row/row_service.dart';
|
|
||||||
import 'package:appflowy_board/appflowy_board.dart';
|
|
||||||
import 'package:dartz/dartz.dart';
|
|
||||||
import 'package:equatable/equatable.dart';
|
|
||||||
import 'package:appflowy_backend/log.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-database/protobuf.dart';
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
|
||||||
|
|
||||||
import 'board_data_controller.dart';
|
|
||||||
import 'group_controller.dart';
|
|
||||||
|
|
||||||
part 'board_bloc.freezed.dart';
|
|
||||||
|
|
||||||
class BoardBloc extends Bloc<BoardEvent, BoardState> {
|
|
||||||
final BoardDataController _gridDataController;
|
|
||||||
late final AppFlowyBoardController boardController;
|
|
||||||
final MoveRowFFIService _rowService;
|
|
||||||
final LinkedHashMap<String, GroupController> groupControllers =
|
|
||||||
LinkedHashMap();
|
|
||||||
|
|
||||||
GridFieldController get fieldController =>
|
|
||||||
_gridDataController.fieldController;
|
|
||||||
String get databaseId => _gridDataController.viewId;
|
|
||||||
|
|
||||||
BoardBloc({required ViewPB view})
|
|
||||||
: _rowService = MoveRowFFIService(viewId: view.id),
|
|
||||||
_gridDataController = BoardDataController(view: view),
|
|
||||||
super(BoardState.initial(view.id)) {
|
|
||||||
boardController = AppFlowyBoardController(
|
|
||||||
onMoveGroup: (
|
|
||||||
fromGroupId,
|
|
||||||
fromIndex,
|
|
||||||
toGroupId,
|
|
||||||
toIndex,
|
|
||||||
) {
|
|
||||||
_moveGroup(fromGroupId, toGroupId);
|
|
||||||
},
|
|
||||||
onMoveGroupItem: (
|
|
||||||
groupId,
|
|
||||||
fromIndex,
|
|
||||||
toIndex,
|
|
||||||
) {
|
|
||||||
final fromRow = groupControllers[groupId]?.rowAtIndex(fromIndex);
|
|
||||||
final toRow = groupControllers[groupId]?.rowAtIndex(toIndex);
|
|
||||||
_moveRow(fromRow, groupId, toRow);
|
|
||||||
},
|
|
||||||
onMoveGroupItemToGroup: (
|
|
||||||
fromGroupId,
|
|
||||||
fromIndex,
|
|
||||||
toGroupId,
|
|
||||||
toIndex,
|
|
||||||
) {
|
|
||||||
final fromRow = groupControllers[fromGroupId]?.rowAtIndex(fromIndex);
|
|
||||||
final toRow = groupControllers[toGroupId]?.rowAtIndex(toIndex);
|
|
||||||
_moveRow(fromRow, toGroupId, toRow);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
on<BoardEvent>(
|
|
||||||
(event, emit) async {
|
|
||||||
await event.when(
|
|
||||||
initial: () async {
|
|
||||||
_startListening();
|
|
||||||
await _openGrid(emit);
|
|
||||||
},
|
|
||||||
createBottomRow: (groupId) async {
|
|
||||||
final startRowId = groupControllers[groupId]?.lastRow()?.id;
|
|
||||||
final result = await _gridDataController.createBoardCard(
|
|
||||||
groupId,
|
|
||||||
startRowId: startRowId,
|
|
||||||
);
|
|
||||||
result.fold(
|
|
||||||
(_) {},
|
|
||||||
(err) => Log.error(err),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
createHeaderRow: (String groupId) async {
|
|
||||||
final result = await _gridDataController.createBoardCard(groupId);
|
|
||||||
result.fold(
|
|
||||||
(_) {},
|
|
||||||
(err) => Log.error(err),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
didCreateRow: (group, row, int? index) {
|
|
||||||
emit(state.copyWith(
|
|
||||||
editingRow: Some(BoardEditingRow(
|
|
||||||
group: group,
|
|
||||||
row: row,
|
|
||||||
index: index,
|
|
||||||
)),
|
|
||||||
));
|
|
||||||
_groupItemStartEditing(group, row, true);
|
|
||||||
},
|
|
||||||
startEditingRow: (group, row) {
|
|
||||||
emit(state.copyWith(
|
|
||||||
editingRow: Some(BoardEditingRow(
|
|
||||||
group: group,
|
|
||||||
row: row,
|
|
||||||
index: null,
|
|
||||||
)),
|
|
||||||
));
|
|
||||||
_groupItemStartEditing(group, row, true);
|
|
||||||
},
|
|
||||||
endEditingRow: (rowId) {
|
|
||||||
state.editingRow.fold(() => null, (editingRow) {
|
|
||||||
assert(editingRow.row.id == rowId);
|
|
||||||
_groupItemStartEditing(editingRow.group, editingRow.row, false);
|
|
||||||
emit(state.copyWith(editingRow: none()));
|
|
||||||
});
|
|
||||||
},
|
|
||||||
didReceiveGridUpdate: (DatabasePB grid) {
|
|
||||||
emit(state.copyWith(grid: Some(grid)));
|
|
||||||
},
|
|
||||||
didReceiveError: (FlowyError error) {
|
|
||||||
emit(state.copyWith(noneOrError: some(error)));
|
|
||||||
},
|
|
||||||
didReceiveGroups: (List<GroupPB> groups) {
|
|
||||||
emit(
|
|
||||||
state.copyWith(
|
|
||||||
groupIds: groups.map((group) => group.groupId).toList(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _groupItemStartEditing(GroupPB group, RowPB row, bool isEdit) {
|
|
||||||
final fieldInfo = fieldController.getField(group.fieldId);
|
|
||||||
if (fieldInfo == null) {
|
|
||||||
Log.warn("fieldInfo should not be null");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
boardController.enableGroupDragging(!isEdit);
|
|
||||||
// boardController.updateGroupItem(
|
|
||||||
// group.groupId,
|
|
||||||
// GroupItem(
|
|
||||||
// row: row,
|
|
||||||
// fieldInfo: fieldInfo,
|
|
||||||
// isDraggable: !isEdit,
|
|
||||||
// ),
|
|
||||||
// );
|
|
||||||
}
|
|
||||||
|
|
||||||
void _moveRow(RowPB? fromRow, String columnId, RowPB? toRow) {
|
|
||||||
if (fromRow != null) {
|
|
||||||
_rowService
|
|
||||||
.moveGroupRow(
|
|
||||||
fromRowId: fromRow.id,
|
|
||||||
toGroupId: columnId,
|
|
||||||
toRowId: toRow?.id,
|
|
||||||
)
|
|
||||||
.then((result) {
|
|
||||||
result.fold((l) => null, (r) => add(BoardEvent.didReceiveError(r)));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _moveGroup(String fromGroupId, String toGroupId) {
|
|
||||||
_rowService
|
|
||||||
.moveGroup(
|
|
||||||
fromGroupId: fromGroupId,
|
|
||||||
toGroupId: toGroupId,
|
|
||||||
)
|
|
||||||
.then((result) {
|
|
||||||
result.fold((l) => null, (r) => add(BoardEvent.didReceiveError(r)));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> close() async {
|
|
||||||
await _gridDataController.dispose();
|
|
||||||
for (final controller in groupControllers.values) {
|
|
||||||
controller.dispose();
|
|
||||||
}
|
|
||||||
return super.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
void initializeGroups(List<GroupPB> groups) {
|
|
||||||
for (var controller in groupControllers.values) {
|
|
||||||
controller.dispose();
|
|
||||||
}
|
|
||||||
groupControllers.clear();
|
|
||||||
boardController.clear();
|
|
||||||
|
|
||||||
boardController.addGroups(groups
|
|
||||||
.where((group) => fieldController.getField(group.fieldId) != null)
|
|
||||||
.map((group) => initializeGroupData(group))
|
|
||||||
.toList());
|
|
||||||
|
|
||||||
for (final group in groups) {
|
|
||||||
final controller = initializeGroupController(group);
|
|
||||||
groupControllers[controller.group.groupId] = (controller);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
GridRowCache? getRowCache(String blockId) {
|
|
||||||
return _gridDataController.rowCache;
|
|
||||||
}
|
|
||||||
|
|
||||||
void _startListening() {
|
|
||||||
_gridDataController.addListener(
|
|
||||||
onGridChanged: (grid) {
|
|
||||||
if (!isClosed) {
|
|
||||||
add(BoardEvent.didReceiveGridUpdate(grid));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
didLoadGroups: (groups) {
|
|
||||||
if (isClosed) return;
|
|
||||||
initializeGroups(groups);
|
|
||||||
add(BoardEvent.didReceiveGroups(groups));
|
|
||||||
},
|
|
||||||
onDeletedGroup: (groupIds) {
|
|
||||||
if (isClosed) return;
|
|
||||||
boardController.removeGroups(groupIds);
|
|
||||||
},
|
|
||||||
onInsertedGroup: (insertedGroup) {
|
|
||||||
if (isClosed) return;
|
|
||||||
final group = insertedGroup.group;
|
|
||||||
final newGroup = initializeGroupData(group);
|
|
||||||
final controller = initializeGroupController(group);
|
|
||||||
groupControllers[controller.group.groupId] = (controller);
|
|
||||||
boardController.addGroup(newGroup);
|
|
||||||
},
|
|
||||||
onUpdatedGroup: (updatedGroups) {
|
|
||||||
if (isClosed) return;
|
|
||||||
for (final group in updatedGroups) {
|
|
||||||
final columnController =
|
|
||||||
boardController.getGroupController(group.groupId);
|
|
||||||
columnController?.updateGroupName(group.desc);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onError: (err) {
|
|
||||||
Log.error(err);
|
|
||||||
},
|
|
||||||
onResetGroups: (groups) {
|
|
||||||
if (isClosed) return;
|
|
||||||
|
|
||||||
initializeGroups(groups);
|
|
||||||
add(BoardEvent.didReceiveGroups(groups));
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
List<AppFlowyGroupItem> _buildGroupItems(GroupPB group) {
|
|
||||||
final items = group.rows.map((row) {
|
|
||||||
final fieldInfo = fieldController.getField(group.fieldId);
|
|
||||||
return GroupItem(
|
|
||||||
row: row,
|
|
||||||
fieldInfo: fieldInfo!,
|
|
||||||
);
|
|
||||||
}).toList();
|
|
||||||
|
|
||||||
return <AppFlowyGroupItem>[...items];
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _openGrid(Emitter<BoardState> emit) async {
|
|
||||||
final result = await _gridDataController.openGrid();
|
|
||||||
result.fold(
|
|
||||||
(grid) => emit(
|
|
||||||
state.copyWith(loadingState: GridLoadingState.finish(left(unit))),
|
|
||||||
),
|
|
||||||
(err) => emit(
|
|
||||||
state.copyWith(loadingState: GridLoadingState.finish(right(err))),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
GroupController initializeGroupController(GroupPB group) {
|
|
||||||
final delegate = GroupControllerDelegateImpl(
|
|
||||||
controller: boardController,
|
|
||||||
fieldController: fieldController,
|
|
||||||
onNewColumnItem: (groupId, row, index) {
|
|
||||||
add(BoardEvent.didCreateRow(group, row, index));
|
|
||||||
},
|
|
||||||
);
|
|
||||||
final controller = GroupController(
|
|
||||||
databaseId: state.databaseId,
|
|
||||||
group: group,
|
|
||||||
delegate: delegate,
|
|
||||||
);
|
|
||||||
controller.startListening();
|
|
||||||
return controller;
|
|
||||||
}
|
|
||||||
|
|
||||||
AppFlowyGroupData initializeGroupData(GroupPB group) {
|
|
||||||
return AppFlowyGroupData(
|
|
||||||
id: group.groupId,
|
|
||||||
name: group.desc,
|
|
||||||
items: _buildGroupItems(group),
|
|
||||||
customData: GroupData(
|
|
||||||
group: group,
|
|
||||||
fieldInfo: fieldController.getField(group.fieldId)!,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@freezed
|
|
||||||
class BoardEvent with _$BoardEvent {
|
|
||||||
const factory BoardEvent.initial() = _InitialBoard;
|
|
||||||
const factory BoardEvent.createBottomRow(String groupId) = _CreateBottomRow;
|
|
||||||
const factory BoardEvent.createHeaderRow(String groupId) = _CreateHeaderRow;
|
|
||||||
const factory BoardEvent.didCreateRow(
|
|
||||||
GroupPB group,
|
|
||||||
RowPB row,
|
|
||||||
int? index,
|
|
||||||
) = _DidCreateRow;
|
|
||||||
const factory BoardEvent.startEditingRow(
|
|
||||||
GroupPB group,
|
|
||||||
RowPB row,
|
|
||||||
) = _StartEditRow;
|
|
||||||
const factory BoardEvent.endEditingRow(String rowId) = _EndEditRow;
|
|
||||||
const factory BoardEvent.didReceiveError(FlowyError error) = _DidReceiveError;
|
|
||||||
const factory BoardEvent.didReceiveGridUpdate(
|
|
||||||
DatabasePB grid,
|
|
||||||
) = _DidReceiveGridUpdate;
|
|
||||||
const factory BoardEvent.didReceiveGroups(List<GroupPB> groups) =
|
|
||||||
_DidReceiveGroups;
|
|
||||||
}
|
|
||||||
|
|
||||||
@freezed
|
|
||||||
class BoardState with _$BoardState {
|
|
||||||
const factory BoardState({
|
|
||||||
required String databaseId,
|
|
||||||
required Option<DatabasePB> grid,
|
|
||||||
required List<String> groupIds,
|
|
||||||
required Option<BoardEditingRow> editingRow,
|
|
||||||
required GridLoadingState loadingState,
|
|
||||||
required Option<FlowyError> noneOrError,
|
|
||||||
}) = _BoardState;
|
|
||||||
|
|
||||||
factory BoardState.initial(String databaseId) => BoardState(
|
|
||||||
grid: none(),
|
|
||||||
databaseId: databaseId,
|
|
||||||
groupIds: [],
|
|
||||||
editingRow: none(),
|
|
||||||
noneOrError: none(),
|
|
||||||
loadingState: const _Loading(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@freezed
|
|
||||||
class GridLoadingState with _$GridLoadingState {
|
|
||||||
const factory GridLoadingState.loading() = _Loading;
|
|
||||||
const factory GridLoadingState.finish(
|
|
||||||
Either<Unit, FlowyError> successOrFail) = _Finish;
|
|
||||||
}
|
|
||||||
|
|
||||||
class GridFieldEquatable extends Equatable {
|
|
||||||
final UnmodifiableListView<FieldPB> _fields;
|
|
||||||
const GridFieldEquatable(
|
|
||||||
UnmodifiableListView<FieldPB> fields,
|
|
||||||
) : _fields = fields;
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<Object?> get props {
|
|
||||||
if (_fields.isEmpty) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
_fields.length,
|
|
||||||
_fields
|
|
||||||
.map((field) => field.width)
|
|
||||||
.reduce((value, element) => value + element),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
UnmodifiableListView<FieldPB> get value => UnmodifiableListView(_fields);
|
|
||||||
}
|
|
||||||
|
|
||||||
class GroupItem extends AppFlowyGroupItem {
|
|
||||||
final RowPB row;
|
|
||||||
final FieldInfo fieldInfo;
|
|
||||||
|
|
||||||
GroupItem({
|
|
||||||
required this.row,
|
|
||||||
required this.fieldInfo,
|
|
||||||
bool draggable = true,
|
|
||||||
}) {
|
|
||||||
super.draggable = draggable;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get id => row.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
class GroupControllerDelegateImpl extends GroupControllerDelegate {
|
|
||||||
final GridFieldController fieldController;
|
|
||||||
final AppFlowyBoardController controller;
|
|
||||||
final void Function(String, RowPB, int?) onNewColumnItem;
|
|
||||||
|
|
||||||
GroupControllerDelegateImpl({
|
|
||||||
required this.controller,
|
|
||||||
required this.fieldController,
|
|
||||||
required this.onNewColumnItem,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
void insertRow(GroupPB group, RowPB row, int? index) {
|
|
||||||
final fieldInfo = fieldController.getField(group.fieldId);
|
|
||||||
if (fieldInfo == null) {
|
|
||||||
Log.warn("fieldInfo should not be null");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (index != null) {
|
|
||||||
final item = GroupItem(
|
|
||||||
row: row,
|
|
||||||
fieldInfo: fieldInfo,
|
|
||||||
);
|
|
||||||
controller.insertGroupItem(group.groupId, index, item);
|
|
||||||
} else {
|
|
||||||
final item = GroupItem(
|
|
||||||
row: row,
|
|
||||||
fieldInfo: fieldInfo,
|
|
||||||
);
|
|
||||||
controller.addGroupItem(group.groupId, item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void removeRow(GroupPB group, String rowId) {
|
|
||||||
controller.removeGroupItem(group.groupId, rowId);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void updateRow(GroupPB group, RowPB row) {
|
|
||||||
final fieldInfo = fieldController.getField(group.fieldId);
|
|
||||||
if (fieldInfo == null) {
|
|
||||||
Log.warn("fieldInfo should not be null");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
controller.updateGroupItem(
|
|
||||||
group.groupId,
|
|
||||||
GroupItem(
|
|
||||||
row: row,
|
|
||||||
fieldInfo: fieldInfo,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void addNewRow(GroupPB group, RowPB row, int? index) {
|
|
||||||
final fieldInfo = fieldController.getField(group.fieldId);
|
|
||||||
if (fieldInfo == null) {
|
|
||||||
Log.warn("fieldInfo should not be null");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final item = GroupItem(
|
|
||||||
row: row,
|
|
||||||
fieldInfo: fieldInfo,
|
|
||||||
draggable: false,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (index != null) {
|
|
||||||
controller.insertGroupItem(group.groupId, index, item);
|
|
||||||
} else {
|
|
||||||
controller.addGroupItem(group.groupId, item);
|
|
||||||
}
|
|
||||||
onNewColumnItem(group.groupId, row, index);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class BoardEditingRow {
|
|
||||||
GroupPB group;
|
|
||||||
RowPB row;
|
|
||||||
int? index;
|
|
||||||
|
|
||||||
BoardEditingRow({
|
|
||||||
required this.group,
|
|
||||||
required this.row,
|
|
||||||
required this.index,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
class GroupData {
|
|
||||||
final GroupPB group;
|
|
||||||
final FieldInfo fieldInfo;
|
|
||||||
GroupData({
|
|
||||||
required this.group,
|
|
||||||
required this.fieldInfo,
|
|
||||||
});
|
|
||||||
|
|
||||||
CheckboxGroup? asCheckboxGroup() {
|
|
||||||
if (fieldType != FieldType.Checkbox) return null;
|
|
||||||
return CheckboxGroup(group);
|
|
||||||
}
|
|
||||||
|
|
||||||
FieldType get fieldType => fieldInfo.fieldType;
|
|
||||||
}
|
|
||||||
|
|
||||||
class CheckboxGroup {
|
|
||||||
final GroupPB group;
|
|
||||||
|
|
||||||
CheckboxGroup(this.group);
|
|
||||||
|
|
||||||
// Hardcode value: "Yes" that equal to the value defined in Rust
|
|
||||||
// pub const CHECK: &str = "Yes";
|
|
||||||
bool get isCheck => group.groupId == "Yes";
|
|
||||||
}
|
|
@ -1,151 +0,0 @@
|
|||||||
import 'dart:collection';
|
|
||||||
|
|
||||||
import 'package:app_flowy/plugins/grid/application/view/grid_view_cache.dart';
|
|
||||||
import 'package:app_flowy/plugins/grid/application/field/field_controller.dart';
|
|
||||||
import 'package:app_flowy/plugins/grid/application/grid_service.dart';
|
|
||||||
import 'package:app_flowy/plugins/grid/application/row/row_cache.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
|
||||||
import 'dart:async';
|
|
||||||
import 'package:dartz/dartz.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-database/protobuf.dart';
|
|
||||||
|
|
||||||
import 'board_listener.dart';
|
|
||||||
|
|
||||||
typedef OnFieldsChanged = void Function(UnmodifiableListView<FieldInfo>);
|
|
||||||
typedef OnGridChanged = void Function(DatabasePB);
|
|
||||||
typedef DidLoadGroups = void Function(List<GroupPB>);
|
|
||||||
typedef OnUpdatedGroup = void Function(List<GroupPB>);
|
|
||||||
typedef OnDeletedGroup = void Function(List<String>);
|
|
||||||
typedef OnInsertedGroup = void Function(InsertedGroupPB);
|
|
||||||
typedef OnResetGroups = void Function(List<GroupPB>);
|
|
||||||
|
|
||||||
typedef OnRowsChanged = void Function(
|
|
||||||
List<RowInfo>,
|
|
||||||
RowsChangedReason,
|
|
||||||
);
|
|
||||||
typedef OnError = void Function(FlowyError);
|
|
||||||
|
|
||||||
class BoardDataController {
|
|
||||||
final String viewId;
|
|
||||||
final DatabaseFFIService _databaseFFIService;
|
|
||||||
final GridFieldController fieldController;
|
|
||||||
final BoardListener _listener;
|
|
||||||
late DatabaseViewCache _viewCache;
|
|
||||||
|
|
||||||
OnFieldsChanged? _onFieldsChanged;
|
|
||||||
OnGridChanged? _onGridChanged;
|
|
||||||
DidLoadGroups? _didLoadGroup;
|
|
||||||
OnRowsChanged? _onRowsChanged;
|
|
||||||
OnError? _onError;
|
|
||||||
|
|
||||||
List<RowInfo> get rowInfos => _viewCache.rowInfos;
|
|
||||||
GridRowCache get rowCache => _viewCache.rowCache;
|
|
||||||
|
|
||||||
BoardDataController({required ViewPB view})
|
|
||||||
: viewId = view.id,
|
|
||||||
_listener = BoardListener(view.id),
|
|
||||||
_databaseFFIService = DatabaseFFIService(viewId: view.id),
|
|
||||||
fieldController = GridFieldController(databaseId: view.id) {
|
|
||||||
//
|
|
||||||
_viewCache = DatabaseViewCache(
|
|
||||||
databaseId: view.id,
|
|
||||||
fieldController: fieldController,
|
|
||||||
);
|
|
||||||
_viewCache.addListener(onRowsChanged: (reason) {
|
|
||||||
_onRowsChanged?.call(rowInfos, reason);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void addListener({
|
|
||||||
required OnGridChanged onGridChanged,
|
|
||||||
OnFieldsChanged? onFieldsChanged,
|
|
||||||
required DidLoadGroups didLoadGroups,
|
|
||||||
OnRowsChanged? onRowsChanged,
|
|
||||||
required OnUpdatedGroup onUpdatedGroup,
|
|
||||||
required OnDeletedGroup onDeletedGroup,
|
|
||||||
required OnInsertedGroup onInsertedGroup,
|
|
||||||
required OnResetGroups onResetGroups,
|
|
||||||
required OnError? onError,
|
|
||||||
}) {
|
|
||||||
_onGridChanged = onGridChanged;
|
|
||||||
_onFieldsChanged = onFieldsChanged;
|
|
||||||
_didLoadGroup = didLoadGroups;
|
|
||||||
_onRowsChanged = onRowsChanged;
|
|
||||||
_onError = onError;
|
|
||||||
|
|
||||||
fieldController.addListener(onFields: (fields) {
|
|
||||||
_onFieldsChanged?.call(UnmodifiableListView(fields));
|
|
||||||
});
|
|
||||||
|
|
||||||
_listener.start(
|
|
||||||
onBoardChanged: (result) {
|
|
||||||
result.fold(
|
|
||||||
(changeset) {
|
|
||||||
if (changeset.updateGroups.isNotEmpty) {
|
|
||||||
onUpdatedGroup.call(changeset.updateGroups);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (changeset.deletedGroups.isNotEmpty) {
|
|
||||||
onDeletedGroup.call(changeset.deletedGroups);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (final insertedGroup in changeset.insertedGroups) {
|
|
||||||
onInsertedGroup.call(insertedGroup);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
(e) => _onError?.call(e),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
onGroupByNewField: (result) {
|
|
||||||
result.fold(
|
|
||||||
(groups) => onResetGroups(groups),
|
|
||||||
(e) => _onError?.call(e),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<Either<Unit, FlowyError>> openGrid() async {
|
|
||||||
final result = await _databaseFFIService.openGrid();
|
|
||||||
return result.fold(
|
|
||||||
(grid) async {
|
|
||||||
_onGridChanged?.call(grid);
|
|
||||||
return fieldController.loadFields(fieldIds: grid.fields).then((result) {
|
|
||||||
return result.fold(
|
|
||||||
(l) => Future(() async {
|
|
||||||
await _loadGroups();
|
|
||||||
_viewCache.rowCache.initializeRows(grid.rows);
|
|
||||||
return left(l);
|
|
||||||
}),
|
|
||||||
(err) => right(err),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
(err) => right(err),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<Either<RowPB, FlowyError>> createBoardCard(String groupId,
|
|
||||||
{String? startRowId}) {
|
|
||||||
return _databaseFFIService.createBoardCard(groupId, startRowId);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> dispose() async {
|
|
||||||
await _viewCache.dispose();
|
|
||||||
await _databaseFFIService.closeGrid();
|
|
||||||
await fieldController.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _loadGroups() async {
|
|
||||||
final result = await _databaseFFIService.loadGroups();
|
|
||||||
return Future(
|
|
||||||
() => result.fold(
|
|
||||||
(groups) {
|
|
||||||
_didLoadGroup?.call(groups.items);
|
|
||||||
},
|
|
||||||
(err) => _onError?.call(err),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,66 +0,0 @@
|
|||||||
import 'dart:typed_data';
|
|
||||||
|
|
||||||
import 'package:app_flowy/core/grid_notification.dart';
|
|
||||||
import 'package:flowy_infra/notifier.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-database/notification.pb.dart';
|
|
||||||
import 'package:dartz/dartz.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-database/group.pb.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-database/group_changeset.pb.dart';
|
|
||||||
|
|
||||||
typedef GroupUpdateValue = Either<GroupChangesetPB, FlowyError>;
|
|
||||||
typedef GroupByNewFieldValue = Either<List<GroupPB>, FlowyError>;
|
|
||||||
|
|
||||||
class BoardListener {
|
|
||||||
final String viewId;
|
|
||||||
PublishNotifier<GroupUpdateValue>? _groupUpdateNotifier = PublishNotifier();
|
|
||||||
PublishNotifier<GroupByNewFieldValue>? _groupByNewFieldNotifier =
|
|
||||||
PublishNotifier();
|
|
||||||
DatabaseNotificationListener? _listener;
|
|
||||||
BoardListener(this.viewId);
|
|
||||||
|
|
||||||
void start({
|
|
||||||
required void Function(GroupUpdateValue) onBoardChanged,
|
|
||||||
required void Function(GroupByNewFieldValue) onGroupByNewField,
|
|
||||||
}) {
|
|
||||||
_groupUpdateNotifier?.addPublishListener(onBoardChanged);
|
|
||||||
_groupByNewFieldNotifier?.addPublishListener(onGroupByNewField);
|
|
||||||
_listener = DatabaseNotificationListener(
|
|
||||||
objectId: viewId,
|
|
||||||
handler: _handler,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _handler(
|
|
||||||
DatabaseNotification ty,
|
|
||||||
Either<Uint8List, FlowyError> result,
|
|
||||||
) {
|
|
||||||
switch (ty) {
|
|
||||||
case DatabaseNotification.DidUpdateGroups:
|
|
||||||
result.fold(
|
|
||||||
(payload) => _groupUpdateNotifier?.value =
|
|
||||||
left(GroupChangesetPB.fromBuffer(payload)),
|
|
||||||
(error) => _groupUpdateNotifier?.value = right(error),
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case DatabaseNotification.DidGroupByField:
|
|
||||||
result.fold(
|
|
||||||
(payload) => _groupByNewFieldNotifier?.value =
|
|
||||||
left(GroupChangesetPB.fromBuffer(payload).initialGroups),
|
|
||||||
(error) => _groupByNewFieldNotifier?.value = right(error),
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> stop() async {
|
|
||||||
await _listener?.stop();
|
|
||||||
_groupUpdateNotifier?.dispose();
|
|
||||||
_groupUpdateNotifier = null;
|
|
||||||
|
|
||||||
_groupByNewFieldNotifier?.dispose();
|
|
||||||
_groupByNewFieldNotifier = null;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,75 +0,0 @@
|
|||||||
import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart';
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
|
||||||
import 'dart:async';
|
|
||||||
|
|
||||||
part 'board_checkbox_cell_bloc.freezed.dart';
|
|
||||||
|
|
||||||
class BoardCheckboxCellBloc
|
|
||||||
extends Bloc<BoardCheckboxCellEvent, BoardCheckboxCellState> {
|
|
||||||
final GridCheckboxCellController cellController;
|
|
||||||
void Function()? _onCellChangedFn;
|
|
||||||
BoardCheckboxCellBloc({
|
|
||||||
required this.cellController,
|
|
||||||
}) : super(BoardCheckboxCellState.initial(cellController)) {
|
|
||||||
on<BoardCheckboxCellEvent>(
|
|
||||||
(event, emit) async {
|
|
||||||
await event.when(
|
|
||||||
initial: () async {
|
|
||||||
_startListening();
|
|
||||||
},
|
|
||||||
didReceiveCellUpdate: (cellData) {
|
|
||||||
emit(state.copyWith(isSelected: _isSelected(cellData)));
|
|
||||||
},
|
|
||||||
select: () async {
|
|
||||||
cellController.saveCellData(!state.isSelected ? "Yes" : "No");
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> close() async {
|
|
||||||
if (_onCellChangedFn != null) {
|
|
||||||
cellController.removeListener(_onCellChangedFn!);
|
|
||||||
_onCellChangedFn = null;
|
|
||||||
}
|
|
||||||
await cellController.dispose();
|
|
||||||
return super.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _startListening() {
|
|
||||||
_onCellChangedFn = cellController.startListening(
|
|
||||||
onCellChanged: ((cellContent) {
|
|
||||||
if (!isClosed) {
|
|
||||||
add(BoardCheckboxCellEvent.didReceiveCellUpdate(cellContent ?? ""));
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@freezed
|
|
||||||
class BoardCheckboxCellEvent with _$BoardCheckboxCellEvent {
|
|
||||||
const factory BoardCheckboxCellEvent.initial() = _InitialCell;
|
|
||||||
const factory BoardCheckboxCellEvent.select() = _Selected;
|
|
||||||
const factory BoardCheckboxCellEvent.didReceiveCellUpdate(
|
|
||||||
String cellContent) = _DidReceiveCellUpdate;
|
|
||||||
}
|
|
||||||
|
|
||||||
@freezed
|
|
||||||
class BoardCheckboxCellState with _$BoardCheckboxCellState {
|
|
||||||
const factory BoardCheckboxCellState({
|
|
||||||
required bool isSelected,
|
|
||||||
}) = _CheckboxCellState;
|
|
||||||
|
|
||||||
factory BoardCheckboxCellState.initial(GridTextCellController context) {
|
|
||||||
return BoardCheckboxCellState(
|
|
||||||
isSelected: _isSelected(context.getCellData()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool _isSelected(String? cellData) {
|
|
||||||
return cellData == "Yes";
|
|
||||||
}
|
|
@ -1,81 +0,0 @@
|
|||||||
import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart';
|
|
||||||
import 'package:app_flowy/plugins/grid/application/field/field_controller.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-database/date_type_option_entities.pb.dart';
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
|
||||||
import 'dart:async';
|
|
||||||
part 'board_date_cell_bloc.freezed.dart';
|
|
||||||
|
|
||||||
class BoardDateCellBloc extends Bloc<BoardDateCellEvent, BoardDateCellState> {
|
|
||||||
final GridDateCellController cellController;
|
|
||||||
void Function()? _onCellChangedFn;
|
|
||||||
|
|
||||||
BoardDateCellBloc({required this.cellController})
|
|
||||||
: super(BoardDateCellState.initial(cellController)) {
|
|
||||||
on<BoardDateCellEvent>(
|
|
||||||
(event, emit) async {
|
|
||||||
event.when(
|
|
||||||
initial: () => _startListening(),
|
|
||||||
didReceiveCellUpdate: (DateCellDataPB? cellData) {
|
|
||||||
emit(state.copyWith(
|
|
||||||
data: cellData, dateStr: _dateStrFromCellData(cellData)));
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> close() async {
|
|
||||||
if (_onCellChangedFn != null) {
|
|
||||||
cellController.removeListener(_onCellChangedFn!);
|
|
||||||
_onCellChangedFn = null;
|
|
||||||
}
|
|
||||||
await cellController.dispose();
|
|
||||||
return super.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _startListening() {
|
|
||||||
_onCellChangedFn = cellController.startListening(
|
|
||||||
onCellChanged: ((data) {
|
|
||||||
if (!isClosed) {
|
|
||||||
add(BoardDateCellEvent.didReceiveCellUpdate(data));
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@freezed
|
|
||||||
class BoardDateCellEvent with _$BoardDateCellEvent {
|
|
||||||
const factory BoardDateCellEvent.initial() = _InitialCell;
|
|
||||||
const factory BoardDateCellEvent.didReceiveCellUpdate(DateCellDataPB? data) =
|
|
||||||
_DidReceiveCellUpdate;
|
|
||||||
}
|
|
||||||
|
|
||||||
@freezed
|
|
||||||
class BoardDateCellState with _$BoardDateCellState {
|
|
||||||
const factory BoardDateCellState({
|
|
||||||
required DateCellDataPB? data,
|
|
||||||
required String dateStr,
|
|
||||||
required FieldInfo fieldInfo,
|
|
||||||
}) = _BoardDateCellState;
|
|
||||||
|
|
||||||
factory BoardDateCellState.initial(GridDateCellController context) {
|
|
||||||
final cellData = context.getCellData();
|
|
||||||
|
|
||||||
return BoardDateCellState(
|
|
||||||
fieldInfo: context.fieldInfo,
|
|
||||||
data: cellData,
|
|
||||||
dateStr: _dateStrFromCellData(cellData),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String _dateStrFromCellData(DateCellDataPB? cellData) {
|
|
||||||
String dateStr = "";
|
|
||||||
if (cellData != null) {
|
|
||||||
dateStr = "${cellData.date} ${cellData.time}";
|
|
||||||
}
|
|
||||||
return dateStr;
|
|
||||||
}
|
|
@ -1,67 +0,0 @@
|
|||||||
import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart';
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
|
||||||
import 'dart:async';
|
|
||||||
|
|
||||||
part 'board_number_cell_bloc.freezed.dart';
|
|
||||||
|
|
||||||
class BoardNumberCellBloc
|
|
||||||
extends Bloc<BoardNumberCellEvent, BoardNumberCellState> {
|
|
||||||
final GridNumberCellController cellController;
|
|
||||||
void Function()? _onCellChangedFn;
|
|
||||||
BoardNumberCellBloc({
|
|
||||||
required this.cellController,
|
|
||||||
}) : super(BoardNumberCellState.initial(cellController)) {
|
|
||||||
on<BoardNumberCellEvent>(
|
|
||||||
(event, emit) async {
|
|
||||||
await event.when(
|
|
||||||
initial: () async {
|
|
||||||
_startListening();
|
|
||||||
},
|
|
||||||
didReceiveCellUpdate: (content) {
|
|
||||||
emit(state.copyWith(content: content));
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> close() async {
|
|
||||||
if (_onCellChangedFn != null) {
|
|
||||||
cellController.removeListener(_onCellChangedFn!);
|
|
||||||
_onCellChangedFn = null;
|
|
||||||
}
|
|
||||||
await cellController.dispose();
|
|
||||||
return super.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _startListening() {
|
|
||||||
_onCellChangedFn = cellController.startListening(
|
|
||||||
onCellChanged: ((cellContent) {
|
|
||||||
if (!isClosed) {
|
|
||||||
add(BoardNumberCellEvent.didReceiveCellUpdate(cellContent ?? ""));
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@freezed
|
|
||||||
class BoardNumberCellEvent with _$BoardNumberCellEvent {
|
|
||||||
const factory BoardNumberCellEvent.initial() = _InitialCell;
|
|
||||||
const factory BoardNumberCellEvent.didReceiveCellUpdate(String cellContent) =
|
|
||||||
_DidReceiveCellUpdate;
|
|
||||||
}
|
|
||||||
|
|
||||||
@freezed
|
|
||||||
class BoardNumberCellState with _$BoardNumberCellState {
|
|
||||||
const factory BoardNumberCellState({
|
|
||||||
required String content,
|
|
||||||
}) = _BoardNumberCellState;
|
|
||||||
|
|
||||||
factory BoardNumberCellState.initial(GridTextCellController context) =>
|
|
||||||
BoardNumberCellState(
|
|
||||||
content: context.getCellData() ?? "",
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,75 +0,0 @@
|
|||||||
import 'dart:async';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-database/select_type_option.pb.dart';
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
|
||||||
import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart';
|
|
||||||
|
|
||||||
part 'board_select_option_cell_bloc.freezed.dart';
|
|
||||||
|
|
||||||
class BoardSelectOptionCellBloc
|
|
||||||
extends Bloc<BoardSelectOptionCellEvent, BoardSelectOptionCellState> {
|
|
||||||
final GridSelectOptionCellController cellController;
|
|
||||||
void Function()? _onCellChangedFn;
|
|
||||||
|
|
||||||
BoardSelectOptionCellBloc({
|
|
||||||
required this.cellController,
|
|
||||||
}) : super(BoardSelectOptionCellState.initial(cellController)) {
|
|
||||||
on<BoardSelectOptionCellEvent>(
|
|
||||||
(event, emit) async {
|
|
||||||
await event.when(
|
|
||||||
initial: () async {
|
|
||||||
_startListening();
|
|
||||||
},
|
|
||||||
didReceiveOptions: (List<SelectOptionPB> selectedOptions) {
|
|
||||||
emit(state.copyWith(selectedOptions: selectedOptions));
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> close() async {
|
|
||||||
if (_onCellChangedFn != null) {
|
|
||||||
cellController.removeListener(_onCellChangedFn!);
|
|
||||||
_onCellChangedFn = null;
|
|
||||||
}
|
|
||||||
await cellController.dispose();
|
|
||||||
return super.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _startListening() {
|
|
||||||
_onCellChangedFn = cellController.startListening(
|
|
||||||
onCellChanged: ((selectOptionContext) {
|
|
||||||
if (!isClosed) {
|
|
||||||
add(BoardSelectOptionCellEvent.didReceiveOptions(
|
|
||||||
selectOptionContext?.selectOptions ?? [],
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@freezed
|
|
||||||
class BoardSelectOptionCellEvent with _$BoardSelectOptionCellEvent {
|
|
||||||
const factory BoardSelectOptionCellEvent.initial() = _InitialCell;
|
|
||||||
const factory BoardSelectOptionCellEvent.didReceiveOptions(
|
|
||||||
List<SelectOptionPB> selectedOptions,
|
|
||||||
) = _DidReceiveOptions;
|
|
||||||
}
|
|
||||||
|
|
||||||
@freezed
|
|
||||||
class BoardSelectOptionCellState with _$BoardSelectOptionCellState {
|
|
||||||
const factory BoardSelectOptionCellState({
|
|
||||||
required List<SelectOptionPB> selectedOptions,
|
|
||||||
}) = _BoardSelectOptionCellState;
|
|
||||||
|
|
||||||
factory BoardSelectOptionCellState.initial(
|
|
||||||
GridSelectOptionCellController context) {
|
|
||||||
final data = context.getCellData();
|
|
||||||
return BoardSelectOptionCellState(
|
|
||||||
selectedOptions: data?.selectOptions ?? [],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,79 +0,0 @@
|
|||||||
import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart';
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
|
||||||
import 'dart:async';
|
|
||||||
|
|
||||||
part 'board_text_cell_bloc.freezed.dart';
|
|
||||||
|
|
||||||
class BoardTextCellBloc extends Bloc<BoardTextCellEvent, BoardTextCellState> {
|
|
||||||
final GridTextCellController cellController;
|
|
||||||
void Function()? _onCellChangedFn;
|
|
||||||
BoardTextCellBloc({
|
|
||||||
required this.cellController,
|
|
||||||
}) : super(BoardTextCellState.initial(cellController)) {
|
|
||||||
on<BoardTextCellEvent>(
|
|
||||||
(event, emit) async {
|
|
||||||
await event.when(
|
|
||||||
initial: () async {
|
|
||||||
_startListening();
|
|
||||||
},
|
|
||||||
didReceiveCellUpdate: (content) {
|
|
||||||
emit(state.copyWith(content: content));
|
|
||||||
},
|
|
||||||
updateText: (text) {
|
|
||||||
if (text != state.content) {
|
|
||||||
cellController.saveCellData(text);
|
|
||||||
emit(state.copyWith(content: text));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
enableEdit: (bool enabled) {
|
|
||||||
emit(state.copyWith(enableEdit: enabled));
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> close() async {
|
|
||||||
if (_onCellChangedFn != null) {
|
|
||||||
cellController.removeListener(_onCellChangedFn!);
|
|
||||||
_onCellChangedFn = null;
|
|
||||||
}
|
|
||||||
await cellController.dispose();
|
|
||||||
return super.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _startListening() {
|
|
||||||
_onCellChangedFn = cellController.startListening(
|
|
||||||
onCellChanged: ((cellContent) {
|
|
||||||
if (!isClosed) {
|
|
||||||
add(BoardTextCellEvent.didReceiveCellUpdate(cellContent ?? ""));
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@freezed
|
|
||||||
class BoardTextCellEvent with _$BoardTextCellEvent {
|
|
||||||
const factory BoardTextCellEvent.initial() = _InitialCell;
|
|
||||||
const factory BoardTextCellEvent.updateText(String text) = _UpdateContent;
|
|
||||||
const factory BoardTextCellEvent.enableEdit(bool enabled) = _EnableEdit;
|
|
||||||
const factory BoardTextCellEvent.didReceiveCellUpdate(String cellContent) =
|
|
||||||
_DidReceiveCellUpdate;
|
|
||||||
}
|
|
||||||
|
|
||||||
@freezed
|
|
||||||
class BoardTextCellState with _$BoardTextCellState {
|
|
||||||
const factory BoardTextCellState({
|
|
||||||
required String content,
|
|
||||||
required bool enableEdit,
|
|
||||||
}) = _BoardTextCellState;
|
|
||||||
|
|
||||||
factory BoardTextCellState.initial(GridTextCellController context) =>
|
|
||||||
BoardTextCellState(
|
|
||||||
content: context.getCellData() ?? "",
|
|
||||||
enableEdit: false,
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,78 +0,0 @@
|
|||||||
import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-database/url_type_option_entities.pb.dart';
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
|
||||||
import 'dart:async';
|
|
||||||
|
|
||||||
part 'board_url_cell_bloc.freezed.dart';
|
|
||||||
|
|
||||||
class BoardURLCellBloc extends Bloc<BoardURLCellEvent, BoardURLCellState> {
|
|
||||||
final GridURLCellController cellController;
|
|
||||||
void Function()? _onCellChangedFn;
|
|
||||||
BoardURLCellBloc({
|
|
||||||
required this.cellController,
|
|
||||||
}) : super(BoardURLCellState.initial(cellController)) {
|
|
||||||
on<BoardURLCellEvent>(
|
|
||||||
(event, emit) async {
|
|
||||||
event.when(
|
|
||||||
initial: () {
|
|
||||||
_startListening();
|
|
||||||
},
|
|
||||||
didReceiveCellUpdate: (cellData) {
|
|
||||||
emit(state.copyWith(
|
|
||||||
content: cellData?.content ?? "",
|
|
||||||
url: cellData?.url ?? "",
|
|
||||||
));
|
|
||||||
},
|
|
||||||
updateURL: (String url) {
|
|
||||||
cellController.saveCellData(url, deduplicate: true);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> close() async {
|
|
||||||
if (_onCellChangedFn != null) {
|
|
||||||
cellController.removeListener(_onCellChangedFn!);
|
|
||||||
_onCellChangedFn = null;
|
|
||||||
}
|
|
||||||
await cellController.dispose();
|
|
||||||
return super.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _startListening() {
|
|
||||||
_onCellChangedFn = cellController.startListening(
|
|
||||||
onCellChanged: ((cellData) {
|
|
||||||
if (!isClosed) {
|
|
||||||
add(BoardURLCellEvent.didReceiveCellUpdate(cellData));
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@freezed
|
|
||||||
class BoardURLCellEvent with _$BoardURLCellEvent {
|
|
||||||
const factory BoardURLCellEvent.initial() = _InitialCell;
|
|
||||||
const factory BoardURLCellEvent.updateURL(String url) = _UpdateURL;
|
|
||||||
const factory BoardURLCellEvent.didReceiveCellUpdate(URLCellDataPB? cell) =
|
|
||||||
_DidReceiveCellUpdate;
|
|
||||||
}
|
|
||||||
|
|
||||||
@freezed
|
|
||||||
class BoardURLCellState with _$BoardURLCellState {
|
|
||||||
const factory BoardURLCellState({
|
|
||||||
required String content,
|
|
||||||
required String url,
|
|
||||||
}) = _BoardURLCellState;
|
|
||||||
|
|
||||||
factory BoardURLCellState.initial(GridURLCellController context) {
|
|
||||||
final cellData = context.getCellData();
|
|
||||||
return BoardURLCellState(
|
|
||||||
content: cellData?.content ?? "",
|
|
||||||
url: cellData?.url ?? "",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,140 +0,0 @@
|
|||||||
import 'dart:collection';
|
|
||||||
import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart';
|
|
||||||
import 'package:app_flowy/plugins/grid/application/row/row_cache.dart';
|
|
||||||
import 'package:app_flowy/plugins/grid/application/row/row_service.dart';
|
|
||||||
import 'package:equatable/equatable.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-database/row_entities.pb.dart';
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
|
||||||
import 'dart:async';
|
|
||||||
import 'card_data_controller.dart';
|
|
||||||
|
|
||||||
part 'card_bloc.freezed.dart';
|
|
||||||
|
|
||||||
class BoardCardBloc extends Bloc<BoardCardEvent, BoardCardState> {
|
|
||||||
final String groupFieldId;
|
|
||||||
final RowFFIService _rowService;
|
|
||||||
final CardDataController _dataController;
|
|
||||||
|
|
||||||
BoardCardBloc({
|
|
||||||
required this.groupFieldId,
|
|
||||||
required String viewId,
|
|
||||||
required CardDataController dataController,
|
|
||||||
required bool isEditing,
|
|
||||||
}) : _rowService = RowFFIService(
|
|
||||||
databaseId: viewId,
|
|
||||||
),
|
|
||||||
_dataController = dataController,
|
|
||||||
super(
|
|
||||||
BoardCardState.initial(
|
|
||||||
dataController.rowPB,
|
|
||||||
_makeCells(groupFieldId, dataController.loadData()),
|
|
||||||
isEditing,
|
|
||||||
),
|
|
||||||
) {
|
|
||||||
on<BoardCardEvent>(
|
|
||||||
(event, emit) async {
|
|
||||||
await event.when(
|
|
||||||
initial: () async {
|
|
||||||
await _startListening();
|
|
||||||
},
|
|
||||||
didReceiveCells: (cells, reason) async {
|
|
||||||
emit(state.copyWith(
|
|
||||||
cells: cells,
|
|
||||||
changeReason: reason,
|
|
||||||
));
|
|
||||||
},
|
|
||||||
setIsEditing: (bool isEditing) {
|
|
||||||
emit(state.copyWith(isEditing: isEditing));
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> close() async {
|
|
||||||
_dataController.dispose();
|
|
||||||
return super.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
RowInfo rowInfo() {
|
|
||||||
return RowInfo(
|
|
||||||
databaseId: _rowService.databaseId,
|
|
||||||
fields: UnmodifiableListView(
|
|
||||||
state.cells.map((cell) => cell.identifier.fieldInfo).toList(),
|
|
||||||
),
|
|
||||||
rowPB: state.rowPB,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _startListening() async {
|
|
||||||
_dataController.addListener(
|
|
||||||
onRowChanged: (cellMap, reason) {
|
|
||||||
if (!isClosed) {
|
|
||||||
final cells = _makeCells(groupFieldId, cellMap);
|
|
||||||
add(BoardCardEvent.didReceiveCells(cells, reason));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
List<BoardCellEquatable> _makeCells(
|
|
||||||
String groupFieldId, GridCellMap originalCellMap) {
|
|
||||||
List<BoardCellEquatable> cells = [];
|
|
||||||
for (final entry in originalCellMap.entries) {
|
|
||||||
// Filter out the cell if it's fieldId equal to the groupFieldId
|
|
||||||
if (entry.value.fieldId != groupFieldId) {
|
|
||||||
cells.add(BoardCellEquatable(entry.value));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return cells;
|
|
||||||
}
|
|
||||||
|
|
||||||
@freezed
|
|
||||||
class BoardCardEvent with _$BoardCardEvent {
|
|
||||||
const factory BoardCardEvent.initial() = _InitialRow;
|
|
||||||
const factory BoardCardEvent.setIsEditing(bool isEditing) = _IsEditing;
|
|
||||||
const factory BoardCardEvent.didReceiveCells(
|
|
||||||
List<BoardCellEquatable> cells,
|
|
||||||
RowsChangedReason reason,
|
|
||||||
) = _DidReceiveCells;
|
|
||||||
}
|
|
||||||
|
|
||||||
@freezed
|
|
||||||
class BoardCardState with _$BoardCardState {
|
|
||||||
const factory BoardCardState({
|
|
||||||
required RowPB rowPB,
|
|
||||||
required List<BoardCellEquatable> cells,
|
|
||||||
required bool isEditing,
|
|
||||||
RowsChangedReason? changeReason,
|
|
||||||
}) = _BoardCardState;
|
|
||||||
|
|
||||||
factory BoardCardState.initial(
|
|
||||||
RowPB rowPB,
|
|
||||||
List<BoardCellEquatable> cells,
|
|
||||||
bool isEditing,
|
|
||||||
) =>
|
|
||||||
BoardCardState(
|
|
||||||
rowPB: rowPB,
|
|
||||||
cells: cells,
|
|
||||||
isEditing: isEditing,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
class BoardCellEquatable extends Equatable {
|
|
||||||
final GridCellIdentifier identifier;
|
|
||||||
|
|
||||||
const BoardCellEquatable(this.identifier);
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<Object?> get props {
|
|
||||||
return [
|
|
||||||
identifier.fieldInfo.id,
|
|
||||||
identifier.fieldInfo.fieldType,
|
|
||||||
identifier.fieldInfo.visibility,
|
|
||||||
identifier.fieldInfo.width,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,49 +0,0 @@
|
|||||||
import 'package:app_flowy/plugins/board/presentation/card/card_cell_builder.dart';
|
|
||||||
import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart';
|
|
||||||
import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_field_notifier.dart';
|
|
||||||
import 'package:app_flowy/plugins/grid/application/field/field_controller.dart';
|
|
||||||
import 'package:app_flowy/plugins/grid/application/row/row_cache.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-database/row_entities.pb.dart';
|
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
|
|
||||||
typedef OnCardChanged = void Function(GridCellMap, RowsChangedReason);
|
|
||||||
|
|
||||||
class CardDataController extends BoardCellBuilderDelegate {
|
|
||||||
final RowPB rowPB;
|
|
||||||
final GridFieldController _fieldController;
|
|
||||||
final GridRowCache _rowCache;
|
|
||||||
final List<VoidCallback> _onCardChangedListeners = [];
|
|
||||||
|
|
||||||
CardDataController({
|
|
||||||
required this.rowPB,
|
|
||||||
required GridFieldController fieldController,
|
|
||||||
required GridRowCache rowCache,
|
|
||||||
}) : _fieldController = fieldController,
|
|
||||||
_rowCache = rowCache;
|
|
||||||
|
|
||||||
GridCellMap loadData() {
|
|
||||||
return _rowCache.loadGridCells(rowPB.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
void addListener({OnCardChanged? onRowChanged}) {
|
|
||||||
_onCardChangedListeners.add(_rowCache.addListener(
|
|
||||||
rowId: rowPB.id,
|
|
||||||
onCellUpdated: onRowChanged,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
void dispose() {
|
|
||||||
for (final fn in _onCardChangedListeners) {
|
|
||||||
_rowCache.removeRowListener(fn);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
GridCellFieldNotifier buildFieldNotifier() {
|
|
||||||
return GridCellFieldNotifier(
|
|
||||||
notifier: GridCellFieldNotifierImpl(_fieldController));
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
GridCellCache get cellCache => _rowCache.cellCache;
|
|
||||||
}
|
|
@ -1,107 +0,0 @@
|
|||||||
import 'package:appflowy_backend/log.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-database/protobuf.dart';
|
|
||||||
import 'group_listener.dart';
|
|
||||||
|
|
||||||
typedef OnGroupError = void Function(FlowyError);
|
|
||||||
|
|
||||||
abstract class GroupControllerDelegate {
|
|
||||||
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 {
|
|
||||||
final GroupPB group;
|
|
||||||
final GroupListener _listener;
|
|
||||||
final GroupControllerDelegate delegate;
|
|
||||||
|
|
||||||
GroupController({
|
|
||||||
required String databaseId,
|
|
||||||
required this.group,
|
|
||||||
required this.delegate,
|
|
||||||
}) : _listener = GroupListener(group);
|
|
||||||
|
|
||||||
RowPB? rowAtIndex(int index) {
|
|
||||||
if (index < group.rows.length) {
|
|
||||||
return group.rows[index];
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
RowPB? lastRow() {
|
|
||||||
if (group.rows.isEmpty) return null;
|
|
||||||
return group.rows.last;
|
|
||||||
}
|
|
||||||
|
|
||||||
void startListening() {
|
|
||||||
_listener.start(onGroupChanged: (result) {
|
|
||||||
result.fold(
|
|
||||||
(GroupRowsNotificationPB 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);
|
|
||||||
} else {
|
|
||||||
group.rows.add(insertedRow.row);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (insertedRow.isNew) {
|
|
||||||
delegate.addNewRow(group, insertedRow.row, index);
|
|
||||||
} else {
|
|
||||||
delegate.insertRow(group, insertedRow.row, index);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (final updatedRow in changeset.updatedRows) {
|
|
||||||
final index = group.rows.indexWhere(
|
|
||||||
(rowPB) => rowPB.id == updatedRow.id,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (index != -1) {
|
|
||||||
group.rows[index] = updatedRow;
|
|
||||||
}
|
|
||||||
|
|
||||||
delegate.updateRow(group, updatedRow);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
(err) => Log.error(err),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,51 +0,0 @@
|
|||||||
import 'dart:typed_data';
|
|
||||||
|
|
||||||
import 'package:app_flowy/core/grid_notification.dart';
|
|
||||||
import 'package:flowy_infra/notifier.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-database/notification.pb.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-database/group.pb.dart';
|
|
||||||
import 'package:dartz/dartz.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-database/group_changeset.pb.dart';
|
|
||||||
|
|
||||||
typedef UpdateGroupNotifiedValue = Either<GroupRowsNotificationPB, FlowyError>;
|
|
||||||
|
|
||||||
class GroupListener {
|
|
||||||
final GroupPB group;
|
|
||||||
PublishNotifier<UpdateGroupNotifiedValue>? _groupNotifier = PublishNotifier();
|
|
||||||
DatabaseNotificationListener? _listener;
|
|
||||||
GroupListener(this.group);
|
|
||||||
|
|
||||||
void start({
|
|
||||||
required void Function(UpdateGroupNotifiedValue) onGroupChanged,
|
|
||||||
}) {
|
|
||||||
_groupNotifier?.addPublishListener(onGroupChanged);
|
|
||||||
_listener = DatabaseNotificationListener(
|
|
||||||
objectId: group.groupId,
|
|
||||||
handler: _handler,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _handler(
|
|
||||||
DatabaseNotification ty,
|
|
||||||
Either<Uint8List, FlowyError> result,
|
|
||||||
) {
|
|
||||||
switch (ty) {
|
|
||||||
case DatabaseNotification.DidUpdateGroupRow:
|
|
||||||
result.fold(
|
|
||||||
(payload) => _groupNotifier?.value =
|
|
||||||
left(GroupRowsNotificationPB.fromBuffer(payload)),
|
|
||||||
(error) => _groupNotifier?.value = right(error),
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> stop() async {
|
|
||||||
await _listener?.stop();
|
|
||||||
_groupNotifier?.dispose();
|
|
||||||
_groupNotifier = null;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,47 +0,0 @@
|
|||||||
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 databaseId;
|
|
||||||
BoardSettingBloc({required this.databaseId})
|
|
||||||
: 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,
|
|
||||||
groups,
|
|
||||||
}
|
|
@ -1,88 +0,0 @@
|
|||||||
import 'package:app_flowy/generated/locale_keys.g.dart';
|
|
||||||
import 'package:app_flowy/plugins/util.dart';
|
|
||||||
import 'package:app_flowy/startup/plugin/plugin.dart';
|
|
||||||
import 'package:app_flowy/workspace/presentation/home/home_stack.dart';
|
|
||||||
import 'package:app_flowy/workspace/presentation/widgets/left_bar_item.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import 'presentation/board_page.dart';
|
|
||||||
|
|
||||||
class BoardPluginBuilder implements PluginBuilder {
|
|
||||||
@override
|
|
||||||
Plugin build(dynamic data) {
|
|
||||||
if (data is ViewPB) {
|
|
||||||
return BoardPlugin(pluginType: pluginType, view: data);
|
|
||||||
} else {
|
|
||||||
throw FlowyPluginException.invalidData;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get menuName => LocaleKeys.board_menuName.tr();
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get menuIcon => "editor/board";
|
|
||||||
|
|
||||||
@override
|
|
||||||
PluginType get pluginType => PluginType.board;
|
|
||||||
|
|
||||||
@override
|
|
||||||
ViewDataFormatPB get dataFormatType => ViewDataFormatPB.DatabaseFormat;
|
|
||||||
|
|
||||||
@override
|
|
||||||
ViewLayoutTypePB? get layoutType => ViewLayoutTypePB.Board;
|
|
||||||
}
|
|
||||||
|
|
||||||
class BoardPluginConfig implements PluginConfig {
|
|
||||||
@override
|
|
||||||
bool get creatable => true;
|
|
||||||
}
|
|
||||||
|
|
||||||
class BoardPlugin extends Plugin {
|
|
||||||
@override
|
|
||||||
final ViewPluginNotifier notifier;
|
|
||||||
final PluginType _pluginType;
|
|
||||||
|
|
||||||
BoardPlugin({
|
|
||||||
required ViewPB view,
|
|
||||||
required PluginType pluginType,
|
|
||||||
}) : _pluginType = pluginType,
|
|
||||||
notifier = ViewPluginNotifier(view: view);
|
|
||||||
|
|
||||||
@override
|
|
||||||
PluginDisplay get display => GridPluginDisplay(notifier: notifier);
|
|
||||||
|
|
||||||
@override
|
|
||||||
PluginId get id => notifier.view.id;
|
|
||||||
|
|
||||||
@override
|
|
||||||
PluginType get ty => _pluginType;
|
|
||||||
}
|
|
||||||
|
|
||||||
class GridPluginDisplay extends PluginDisplay {
|
|
||||||
final ViewPluginNotifier notifier;
|
|
||||||
GridPluginDisplay({required this.notifier, Key? key});
|
|
||||||
|
|
||||||
ViewPB get view => notifier.view;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget get leftBarItem => ViewLeftBarItem(view: view);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget buildWidget(PluginContext context) {
|
|
||||||
notifier.isDeleted.addListener(() {
|
|
||||||
notifier.isDeleted.value.fold(() => null, (deletedView) {
|
|
||||||
if (deletedView.hasIndex()) {
|
|
||||||
context.onDeleted(view, deletedView.index);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return BoardPage(key: ValueKey(view.id), view: view);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<NavigationItem> get navigationItems => [this];
|
|
||||||
}
|
|
@ -1,388 +0,0 @@
|
|||||||
// ignore_for_file: unused_field
|
|
||||||
|
|
||||||
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/field/field_controller.dart';
|
|
||||||
import 'package:app_flowy/plugins/grid/application/row/row_cache.dart';
|
|
||||||
import 'package:app_flowy/plugins/grid/application/row/row_data_controller.dart';
|
|
||||||
import 'package:app_flowy/plugins/grid/presentation/widgets/cell/cell_builder.dart';
|
|
||||||
import 'package:app_flowy/plugins/grid/presentation/widgets/row/row_detail.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-database/field_entities.pb.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-database/row_entities.pb.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_ui/flowy_infra_ui_web.dart';
|
|
||||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
|
||||||
import 'package:flowy_infra_ui/widget/error_page.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.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 {
|
|
||||||
BoardPage({
|
|
||||||
required this.view,
|
|
||||||
Key? key,
|
|
||||||
this.onEditStateChanged,
|
|
||||||
}) : super(key: ValueKey(view.id));
|
|
||||||
|
|
||||||
final ViewPB view;
|
|
||||||
|
|
||||||
/// Called when edit state changed
|
|
||||||
final VoidCallback? onEditStateChanged;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return BlocProvider(
|
|
||||||
create: (context) =>
|
|
||||||
BoardBloc(view: view)..add(const BoardEvent.initial()),
|
|
||||||
child: BlocBuilder<BoardBloc, BoardState>(
|
|
||||||
buildWhen: (p, c) => p.loadingState != c.loadingState,
|
|
||||||
builder: (context, state) {
|
|
||||||
return state.loadingState.map(
|
|
||||||
loading: (_) =>
|
|
||||||
const Center(child: CircularProgressIndicator.adaptive()),
|
|
||||||
finish: (result) {
|
|
||||||
return result.successOrFail.fold(
|
|
||||||
(_) => BoardContent(
|
|
||||||
onEditStateChanged: onEditStateChanged,
|
|
||||||
),
|
|
||||||
(err) => FlowyErrorPage(err.toString()),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class BoardContent extends StatefulWidget {
|
|
||||||
const BoardContent({
|
|
||||||
Key? key,
|
|
||||||
this.onEditStateChanged,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
final VoidCallback? onEditStateChanged;
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<BoardContent> createState() => _BoardContentState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _BoardContentState extends State<BoardContent> {
|
|
||||||
late AppFlowyBoardScrollController scrollManager;
|
|
||||||
|
|
||||||
final config = AppFlowyBoardConfig(
|
|
||||||
groupBackgroundColor: HexColor.fromHex('#F7F8FC'),
|
|
||||||
);
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
scrollManager = AppFlowyBoardScrollController();
|
|
||||||
super.initState();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return BlocListener<BoardBloc, BoardState>(
|
|
||||||
listener: (context, state) {
|
|
||||||
_handleEditStateChanged(state, context);
|
|
||||||
widget.onEditStateChanged?.call();
|
|
||||||
},
|
|
||||||
child: BlocBuilder<BoardBloc, BoardState>(
|
|
||||||
buildWhen: (previous, current) => previous.groupIds != current.groupIds,
|
|
||||||
builder: (context, state) {
|
|
||||||
final column = Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [const _ToolbarBlocAdaptor(), _buildBoard(context)],
|
|
||||||
);
|
|
||||||
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
|
||||||
child: column,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildBoard(BuildContext context) {
|
|
||||||
return Expanded(
|
|
||||||
child: AppFlowyBoard(
|
|
||||||
boardScrollController: scrollManager,
|
|
||||||
scrollController: ScrollController(),
|
|
||||||
controller: context.read<BoardBloc>().boardController,
|
|
||||||
headerBuilder: _buildHeader,
|
|
||||||
footerBuilder: _buildFooter,
|
|
||||||
cardBuilder: (_, column, columnItem) => _buildCard(
|
|
||||||
context,
|
|
||||||
column,
|
|
||||||
columnItem,
|
|
||||||
),
|
|
||||||
groupConstraints: const BoxConstraints.tightFor(width: 300),
|
|
||||||
config: AppFlowyBoardConfig(
|
|
||||||
groupBackgroundColor: Theme.of(context).colorScheme.surfaceVariant,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _handleEditStateChanged(BoardState state, BuildContext context) {
|
|
||||||
state.editingRow.fold(
|
|
||||||
() => null,
|
|
||||||
(editingRow) {
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
||||||
if (editingRow.index != null) {
|
|
||||||
} else {
|
|
||||||
scrollManager.scrollToBottom(editingRow.group.groupId);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildHeader(
|
|
||||||
BuildContext context,
|
|
||||||
AppFlowyGroupData groupData,
|
|
||||||
) {
|
|
||||||
final boardCustomData = groupData.customData as GroupData;
|
|
||||||
return AppFlowyGroupHeader(
|
|
||||||
title: Flexible(
|
|
||||||
fit: FlexFit.tight,
|
|
||||||
child: FlowyText.medium(
|
|
||||||
groupData.headerData.groupName,
|
|
||||||
fontSize: 14,
|
|
||||||
overflow: TextOverflow.clip,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
icon: _buildHeaderIcon(boardCustomData),
|
|
||||||
addIcon: SizedBox(
|
|
||||||
height: 20,
|
|
||||||
width: 20,
|
|
||||||
child: svgWidget(
|
|
||||||
"home/add",
|
|
||||||
color: Theme.of(context).colorScheme.onSurface,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
onAddButtonClick: () {
|
|
||||||
context.read<BoardBloc>().add(
|
|
||||||
BoardEvent.createHeaderRow(groupData.id),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
height: 50,
|
|
||||||
margin: config.headerPadding,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildFooter(BuildContext context, AppFlowyGroupData columnData) {
|
|
||||||
// final boardCustomData = columnData.customData as BoardCustomData;
|
|
||||||
// final group = boardCustomData.group;
|
|
||||||
|
|
||||||
return AppFlowyGroupFooter(
|
|
||||||
icon: SizedBox(
|
|
||||||
height: 20,
|
|
||||||
width: 20,
|
|
||||||
child: svgWidget(
|
|
||||||
"home/add",
|
|
||||||
color: Theme.of(context).colorScheme.onSurface,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
title: FlowyText.medium(
|
|
||||||
LocaleKeys.board_column_create_new_card.tr(),
|
|
||||||
fontSize: 14,
|
|
||||||
),
|
|
||||||
height: 50,
|
|
||||||
margin: config.footerPadding,
|
|
||||||
onAddButtonClick: () {
|
|
||||||
context.read<BoardBloc>().add(
|
|
||||||
BoardEvent.createBottomRow(columnData.id),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildCard(
|
|
||||||
BuildContext context,
|
|
||||||
AppFlowyGroupData afGroupData,
|
|
||||||
AppFlowyGroupItem afGroupItem,
|
|
||||||
) {
|
|
||||||
final groupItem = afGroupItem as GroupItem;
|
|
||||||
final groupData = afGroupData.customData as GroupData;
|
|
||||||
final rowPB = groupItem.row;
|
|
||||||
final rowCache = context.read<BoardBloc>().getRowCache(rowPB.blockId);
|
|
||||||
|
|
||||||
/// Return placeholder widget if the rowCache is null.
|
|
||||||
if (rowCache == null) return SizedBox(key: ObjectKey(groupItem));
|
|
||||||
|
|
||||||
final fieldController = context.read<BoardBloc>().fieldController;
|
|
||||||
final databaseId = context.read<BoardBloc>().databaseId;
|
|
||||||
final cardController = CardDataController(
|
|
||||||
fieldController: fieldController,
|
|
||||||
rowCache: rowCache,
|
|
||||||
rowPB: rowPB,
|
|
||||||
);
|
|
||||||
|
|
||||||
final cellBuilder = BoardCellBuilder(cardController);
|
|
||||||
bool isEditing = false;
|
|
||||||
context.read<BoardBloc>().state.editingRow.fold(
|
|
||||||
() => null,
|
|
||||||
(editingRow) {
|
|
||||||
isEditing = editingRow.row.id == groupItem.row.id;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
final groupItemId = groupItem.row.id + groupData.group.groupId;
|
|
||||||
return AppFlowyGroupCard(
|
|
||||||
key: ValueKey(groupItemId),
|
|
||||||
margin: config.cardPadding,
|
|
||||||
decoration: _makeBoxDecoration(context),
|
|
||||||
child: BoardCard(
|
|
||||||
viewId: databaseId,
|
|
||||||
groupId: groupData.group.groupId,
|
|
||||||
fieldId: groupItem.fieldInfo.id,
|
|
||||||
isEditing: isEditing,
|
|
||||||
cellBuilder: cellBuilder,
|
|
||||||
dataController: cardController,
|
|
||||||
openCard: (context) => _openCard(
|
|
||||||
databaseId,
|
|
||||||
fieldController,
|
|
||||||
rowPB,
|
|
||||||
rowCache,
|
|
||||||
context,
|
|
||||||
),
|
|
||||||
onStartEditing: () {
|
|
||||||
context.read<BoardBloc>().add(
|
|
||||||
BoardEvent.startEditingRow(
|
|
||||||
groupData.group,
|
|
||||||
groupItem.row,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
onEndEditing: () {
|
|
||||||
context
|
|
||||||
.read<BoardBloc>()
|
|
||||||
.add(BoardEvent.endEditingRow(groupItem.row.id));
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
BoxDecoration _makeBoxDecoration(BuildContext context) {
|
|
||||||
final borderSide = BorderSide(
|
|
||||||
color: Theme.of(context).dividerColor,
|
|
||||||
width: 1.0,
|
|
||||||
);
|
|
||||||
return BoxDecoration(
|
|
||||||
color: Theme.of(context).colorScheme.surface,
|
|
||||||
border: Border.fromBorderSide(borderSide),
|
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(6)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _openCard(
|
|
||||||
String databaseId,
|
|
||||||
GridFieldController fieldController,
|
|
||||||
RowPB rowPB,
|
|
||||||
GridRowCache rowCache,
|
|
||||||
BuildContext context,
|
|
||||||
) {
|
|
||||||
final rowInfo = RowInfo(
|
|
||||||
databaseId: databaseId,
|
|
||||||
fields: UnmodifiableListView(fieldController.fieldInfos),
|
|
||||||
rowPB: rowPB,
|
|
||||||
);
|
|
||||||
|
|
||||||
final dataController = RowDataController(
|
|
||||||
rowInfo: rowInfo,
|
|
||||||
fieldController: fieldController,
|
|
||||||
rowCache: rowCache,
|
|
||||||
);
|
|
||||||
|
|
||||||
FlowyOverlay.show(
|
|
||||||
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.databaseId,
|
|
||||||
fieldController: bloc.fieldController,
|
|
||||||
);
|
|
||||||
|
|
||||||
return BoardToolbar(toolbarContext: toolbarContext);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension HexColor on Color {
|
|
||||||
static Color fromHex(String hexString) {
|
|
||||||
final buffer = StringBuffer();
|
|
||||||
if (hexString.length == 6 || hexString.length == 7) buffer.write('ff');
|
|
||||||
buffer.write(hexString.replaceFirst('#', ''));
|
|
||||||
return Color(int.parse(buffer.toString(), radix: 16));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget? _buildHeaderIcon(GroupData customData) {
|
|
||||||
Widget? widget;
|
|
||||||
switch (customData.fieldType) {
|
|
||||||
case FieldType.Checkbox:
|
|
||||||
final group = customData.asCheckboxGroup()!;
|
|
||||||
if (group.isCheck) {
|
|
||||||
widget = svgWidget('editor/editor_check');
|
|
||||||
} else {
|
|
||||||
widget = svgWidget('editor/editor_uncheck');
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case FieldType.DateTime:
|
|
||||||
break;
|
|
||||||
case FieldType.MultiSelect:
|
|
||||||
break;
|
|
||||||
case FieldType.Number:
|
|
||||||
break;
|
|
||||||
case FieldType.RichText:
|
|
||||||
break;
|
|
||||||
case FieldType.SingleSelect:
|
|
||||||
break;
|
|
||||||
case FieldType.URL:
|
|
||||||
break;
|
|
||||||
case FieldType.Checklist:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (widget != null) {
|
|
||||||
widget = SizedBox(
|
|
||||||
width: 20,
|
|
||||||
height: 20,
|
|
||||||
child: widget,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return widget;
|
|
||||||
}
|
|
@ -1,97 +0,0 @@
|
|||||||
import 'package:app_flowy/plugins/grid/application/prelude.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
abstract class FocusableBoardCell {
|
|
||||||
set becomeFocus(bool isFocus);
|
|
||||||
}
|
|
||||||
|
|
||||||
class EditableCellNotifier {
|
|
||||||
final ValueNotifier<bool> isCellEditing;
|
|
||||||
|
|
||||||
EditableCellNotifier({bool isEditing = false})
|
|
||||||
: isCellEditing = ValueNotifier(isEditing);
|
|
||||||
|
|
||||||
void dispose() {
|
|
||||||
isCellEditing.dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class EditableRowNotifier {
|
|
||||||
final Map<EditableCellId, EditableCellNotifier> _cells = {};
|
|
||||||
final ValueNotifier<bool> isEditing;
|
|
||||||
|
|
||||||
EditableRowNotifier({required bool isEditing})
|
|
||||||
: isEditing = ValueNotifier(isEditing);
|
|
||||||
|
|
||||||
void bindCell(
|
|
||||||
GridCellIdentifier cellIdentifier,
|
|
||||||
EditableCellNotifier notifier,
|
|
||||||
) {
|
|
||||||
assert(
|
|
||||||
_cells.values.isEmpty,
|
|
||||||
'Only one cell can receive the notification',
|
|
||||||
);
|
|
||||||
final id = EditableCellId.from(cellIdentifier);
|
|
||||||
_cells[id]?.dispose();
|
|
||||||
|
|
||||||
notifier.isCellEditing.addListener(() {
|
|
||||||
isEditing.value = notifier.isCellEditing.value;
|
|
||||||
});
|
|
||||||
|
|
||||||
_cells[EditableCellId.from(cellIdentifier)] = notifier;
|
|
||||||
}
|
|
||||||
|
|
||||||
void becomeFirstResponder() {
|
|
||||||
if (_cells.values.isEmpty) return;
|
|
||||||
assert(
|
|
||||||
_cells.values.length == 1,
|
|
||||||
'Only one cell can receive the notification',
|
|
||||||
);
|
|
||||||
_cells.values.first.isCellEditing.value = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void resignFirstResponder() {
|
|
||||||
if (_cells.values.isEmpty) return;
|
|
||||||
assert(
|
|
||||||
_cells.values.length == 1,
|
|
||||||
'Only one cell can receive the notification',
|
|
||||||
);
|
|
||||||
_cells.values.first.isCellEditing.value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
void unbind() {
|
|
||||||
for (final notifier in _cells.values) {
|
|
||||||
notifier.dispose();
|
|
||||||
}
|
|
||||||
_cells.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
void dispose() {
|
|
||||||
for (final notifier in _cells.values) {
|
|
||||||
notifier.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
_cells.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract class EditableCell {
|
|
||||||
// Each cell notifier will be bind to the [EditableRowNotifier], which enable
|
|
||||||
// the row notifier receive its cells event. For example: begin editing the
|
|
||||||
// cell or end editing the cell.
|
|
||||||
//
|
|
||||||
EditableCellNotifier? get editableNotifier;
|
|
||||||
}
|
|
||||||
|
|
||||||
class EditableCellId {
|
|
||||||
String fieldId;
|
|
||||||
String rowId;
|
|
||||||
|
|
||||||
EditableCellId(this.rowId, this.fieldId);
|
|
||||||
|
|
||||||
factory EditableCellId.from(GridCellIdentifier cellIdentifier) =>
|
|
||||||
EditableCellId(
|
|
||||||
cellIdentifier.rowId,
|
|
||||||
cellIdentifier.fieldId,
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,66 +0,0 @@
|
|||||||
import 'package:app_flowy/plugins/board/application/card/board_checkbox_cell_bloc.dart';
|
|
||||||
import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart';
|
|
||||||
import 'package:flowy_infra/image.dart';
|
|
||||||
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
||||||
|
|
||||||
class BoardCheckboxCell extends StatefulWidget {
|
|
||||||
final String groupId;
|
|
||||||
final GridCellControllerBuilder cellControllerBuilder;
|
|
||||||
|
|
||||||
const BoardCheckboxCell({
|
|
||||||
required this.groupId,
|
|
||||||
required this.cellControllerBuilder,
|
|
||||||
Key? key,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<BoardCheckboxCell> createState() => _BoardCheckboxCellState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _BoardCheckboxCellState extends State<BoardCheckboxCell> {
|
|
||||||
late BoardCheckboxCellBloc _cellBloc;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
final cellController =
|
|
||||||
widget.cellControllerBuilder.build() as GridCheckboxCellController;
|
|
||||||
_cellBloc = BoardCheckboxCellBloc(cellController: cellController);
|
|
||||||
_cellBloc.add(const BoardCheckboxCellEvent.initial());
|
|
||||||
super.initState();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return BlocProvider.value(
|
|
||||||
value: _cellBloc,
|
|
||||||
child: BlocBuilder<BoardCheckboxCellBloc, BoardCheckboxCellState>(
|
|
||||||
buildWhen: (previous, current) =>
|
|
||||||
previous.isSelected != current.isSelected,
|
|
||||||
builder: (context, state) {
|
|
||||||
final icon = state.isSelected
|
|
||||||
? svgWidget('editor/editor_check')
|
|
||||||
: svgWidget('editor/editor_uncheck');
|
|
||||||
return Align(
|
|
||||||
alignment: Alignment.centerLeft,
|
|
||||||
child: FlowyIconButton(
|
|
||||||
iconPadding: EdgeInsets.zero,
|
|
||||||
icon: icon,
|
|
||||||
width: 20,
|
|
||||||
onPressed: () => context
|
|
||||||
.read<BoardCheckboxCellBloc>()
|
|
||||||
.add(const BoardCheckboxCellEvent.select()),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> dispose() async {
|
|
||||||
_cellBloc.close();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,38 +0,0 @@
|
|||||||
import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart';
|
|
||||||
import 'package:app_flowy/plugins/grid/application/cell/checklist_cell_bloc.dart';
|
|
||||||
import 'package:app_flowy/plugins/grid/presentation/widgets/cell/checklist_cell/checklist_progress_bar.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
||||||
|
|
||||||
class BoardChecklistCell extends StatefulWidget {
|
|
||||||
final GridCellControllerBuilder cellControllerBuilder;
|
|
||||||
const BoardChecklistCell({required this.cellControllerBuilder, Key? key})
|
|
||||||
: super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<BoardChecklistCell> createState() => _BoardChecklistCellState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _BoardChecklistCellState extends State<BoardChecklistCell> {
|
|
||||||
late ChecklistCellBloc _cellBloc;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
final cellController =
|
|
||||||
widget.cellControllerBuilder.build() as GridChecklistCellController;
|
|
||||||
_cellBloc = ChecklistCellBloc(cellController: cellController);
|
|
||||||
_cellBloc.add(const ChecklistCellEvent.initial());
|
|
||||||
super.initState();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return BlocProvider.value(
|
|
||||||
value: _cellBloc,
|
|
||||||
child: BlocBuilder<ChecklistCellBloc, ChecklistCellState>(
|
|
||||||
builder: (context, state) =>
|
|
||||||
ChecklistProgressBar(percent: state.percent),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,70 +0,0 @@
|
|||||||
import 'package:app_flowy/plugins/board/application/card/board_date_cell_bloc.dart';
|
|
||||||
import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart';
|
|
||||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
||||||
|
|
||||||
import 'define.dart';
|
|
||||||
|
|
||||||
class BoardDateCell extends StatefulWidget {
|
|
||||||
final String groupId;
|
|
||||||
final GridCellControllerBuilder cellControllerBuilder;
|
|
||||||
|
|
||||||
const BoardDateCell({
|
|
||||||
required this.groupId,
|
|
||||||
required this.cellControllerBuilder,
|
|
||||||
Key? key,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<BoardDateCell> createState() => _BoardDateCellState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _BoardDateCellState extends State<BoardDateCell> {
|
|
||||||
late BoardDateCellBloc _cellBloc;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
final cellController =
|
|
||||||
widget.cellControllerBuilder.build() as GridDateCellController;
|
|
||||||
|
|
||||||
_cellBloc = BoardDateCellBloc(cellController: cellController)
|
|
||||||
..add(const BoardDateCellEvent.initial());
|
|
||||||
super.initState();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return BlocProvider.value(
|
|
||||||
value: _cellBloc,
|
|
||||||
child: BlocBuilder<BoardDateCellBloc, BoardDateCellState>(
|
|
||||||
buildWhen: (previous, current) => previous.dateStr != current.dateStr,
|
|
||||||
builder: (context, state) {
|
|
||||||
if (state.dateStr.isEmpty) {
|
|
||||||
return const SizedBox();
|
|
||||||
} else {
|
|
||||||
return Align(
|
|
||||||
alignment: Alignment.centerLeft,
|
|
||||||
child: Padding(
|
|
||||||
padding: EdgeInsets.symmetric(
|
|
||||||
vertical: BoardSizes.cardCellVPadding,
|
|
||||||
),
|
|
||||||
child: FlowyText.regular(
|
|
||||||
state.dateStr,
|
|
||||||
fontSize: 13,
|
|
||||||
color: Theme.of(context).hintColor,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> dispose() async {
|
|
||||||
_cellBloc.close();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,68 +0,0 @@
|
|||||||
import 'package:app_flowy/plugins/board/application/card/board_number_cell_bloc.dart';
|
|
||||||
import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart';
|
|
||||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
||||||
import 'define.dart';
|
|
||||||
|
|
||||||
class BoardNumberCell extends StatefulWidget {
|
|
||||||
final String groupId;
|
|
||||||
final GridCellControllerBuilder cellControllerBuilder;
|
|
||||||
|
|
||||||
const BoardNumberCell({
|
|
||||||
required this.groupId,
|
|
||||||
required this.cellControllerBuilder,
|
|
||||||
Key? key,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<BoardNumberCell> createState() => _BoardNumberCellState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _BoardNumberCellState extends State<BoardNumberCell> {
|
|
||||||
late BoardNumberCellBloc _cellBloc;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
final cellController =
|
|
||||||
widget.cellControllerBuilder.build() as GridNumberCellController;
|
|
||||||
|
|
||||||
_cellBloc = BoardNumberCellBloc(cellController: cellController)
|
|
||||||
..add(const BoardNumberCellEvent.initial());
|
|
||||||
super.initState();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return BlocProvider.value(
|
|
||||||
value: _cellBloc,
|
|
||||||
child: BlocBuilder<BoardNumberCellBloc, BoardNumberCellState>(
|
|
||||||
buildWhen: (previous, current) => previous.content != current.content,
|
|
||||||
builder: (context, state) {
|
|
||||||
if (state.content.isEmpty) {
|
|
||||||
return const SizedBox();
|
|
||||||
} else {
|
|
||||||
return Align(
|
|
||||||
alignment: Alignment.centerLeft,
|
|
||||||
child: Padding(
|
|
||||||
padding: EdgeInsets.symmetric(
|
|
||||||
vertical: BoardSizes.cardCellVPadding,
|
|
||||||
),
|
|
||||||
child: FlowyText.medium(
|
|
||||||
state.content,
|
|
||||||
fontSize: 14,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> dispose() async {
|
|
||||||
_cellBloc.close();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,110 +0,0 @@
|
|||||||
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:appflowy_popover/appflowy_popover.dart';
|
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<BoardSelectOptionCell> createState() => _BoardSelectOptionCellState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _BoardSelectOptionCellState extends State<BoardSelectOptionCell> {
|
|
||||||
late BoardSelectOptionCellBloc _cellBloc;
|
|
||||||
late PopoverController _popover;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
_popover = PopoverController();
|
|
||||||
final cellController =
|
|
||||||
widget.cellControllerBuilder.build() as GridSelectOptionCellController;
|
|
||||||
_cellBloc = BoardSelectOptionCellBloc(cellController: cellController)
|
|
||||||
..add(const BoardSelectOptionCellEvent.initial());
|
|
||||||
super.initState();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return BlocProvider.value(
|
|
||||||
value: _cellBloc,
|
|
||||||
child: BlocBuilder<BoardSelectOptionCellBloc, BoardSelectOptionCellState>(
|
|
||||||
buildWhen: (previous, current) {
|
|
||||||
return previous.selectedOptions != current.selectedOptions;
|
|
||||||
}, builder: (context, state) {
|
|
||||||
// Returns SizedBox if the content of the cell is empty
|
|
||||||
if (_isEmpty(state)) return const SizedBox();
|
|
||||||
|
|
||||||
final children = state.selectedOptions.map(
|
|
||||||
(option) {
|
|
||||||
final tag = SelectOptionTag.fromOption(
|
|
||||||
context: context,
|
|
||||||
option: option,
|
|
||||||
onSelected: () => _popover.show(),
|
|
||||||
);
|
|
||||||
return _wrapPopover(tag);
|
|
||||||
},
|
|
||||||
).toList();
|
|
||||||
|
|
||||||
return IntrinsicHeight(
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 6),
|
|
||||||
child: SizedBox.expand(
|
|
||||||
child: Wrap(spacing: 4, runSpacing: 2, children: children),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool _isEmpty(BoardSelectOptionCellState state) {
|
|
||||||
// The cell should hide if the option id is equal to the groupId.
|
|
||||||
final isInGroup = state.selectedOptions
|
|
||||||
.where((element) => element.id == widget.groupId)
|
|
||||||
.isNotEmpty;
|
|
||||||
return isInGroup || state.selectedOptions.isEmpty;
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _wrapPopover(Widget child) {
|
|
||||||
final constraints = BoxConstraints.loose(Size(
|
|
||||||
SelectOptionCellEditor.editorPanelWidth,
|
|
||||||
300,
|
|
||||||
));
|
|
||||||
return AppFlowyPopover(
|
|
||||||
controller: _popover,
|
|
||||||
constraints: constraints,
|
|
||||||
direction: PopoverDirection.bottomWithLeftAligned,
|
|
||||||
popupBuilder: (BuildContext context) {
|
|
||||||
return SelectOptionCellEditor(
|
|
||||||
cellController: widget.cellControllerBuilder.build()
|
|
||||||
as GridSelectOptionCellController,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
onClose: () {},
|
|
||||||
child: child,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> dispose() async {
|
|
||||||
_cellBloc.close();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,167 +0,0 @@
|
|||||||
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/size.dart';
|
|
||||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
||||||
import 'package:textstyle_extensions/textstyle_extensions.dart';
|
|
||||||
import 'board_cell.dart';
|
|
||||||
import 'define.dart';
|
|
||||||
|
|
||||||
class BoardTextCell extends StatefulWidget with EditableCell {
|
|
||||||
final String groupId;
|
|
||||||
@override
|
|
||||||
final EditableCellNotifier? editableNotifier;
|
|
||||||
final GridCellControllerBuilder cellControllerBuilder;
|
|
||||||
|
|
||||||
const BoardTextCell({
|
|
||||||
required this.groupId,
|
|
||||||
required this.cellControllerBuilder,
|
|
||||||
this.editableNotifier,
|
|
||||||
Key? key,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<BoardTextCell> createState() => _BoardTextCellState();
|
|
||||||
}
|
|
||||||
|
|
||||||
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 GridTextCellController;
|
|
||||||
_cellBloc = BoardTextCellBloc(cellController: cellController)
|
|
||||||
..add(const BoardTextCellEvent.initial());
|
|
||||||
_controller = TextEditingController(text: _cellBloc.state.content);
|
|
||||||
focusWhenInit = widget.editableNotifier?.isCellEditing.value ?? false;
|
|
||||||
if (focusWhenInit) {
|
|
||||||
focusNode.requestFocus();
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the focusNode lost its focus, the widget's editableNotifier will
|
|
||||||
// set to false, which will cause the [EditableRowNotifier] to receive
|
|
||||||
// end edit event.
|
|
||||||
focusNode.addListener(() {
|
|
||||||
if (!focusNode.hasFocus) {
|
|
||||||
focusWhenInit = false;
|
|
||||||
widget.editableNotifier?.isCellEditing.value = false;
|
|
||||||
_cellBloc.add(const BoardTextCellEvent.enableEdit(false));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
_bindEditableNotifier();
|
|
||||||
super.initState();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _bindEditableNotifier() {
|
|
||||||
widget.editableNotifier?.isCellEditing.addListener(() {
|
|
||||||
if (!mounted) return;
|
|
||||||
|
|
||||||
final isEditing = widget.editableNotifier?.isCellEditing.value ?? false;
|
|
||||||
if (isEditing) {
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
||||||
focusNode.requestFocus();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
_cellBloc.add(BoardTextCellEvent.enableEdit(isEditing));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void didUpdateWidget(covariant BoardTextCell oldWidget) {
|
|
||||||
_bindEditableNotifier();
|
|
||||||
super.didUpdateWidget(oldWidget);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return BlocProvider.value(
|
|
||||||
value: _cellBloc,
|
|
||||||
child: BlocListener<BoardTextCellBloc, BoardTextCellState>(
|
|
||||||
listener: (context, state) {
|
|
||||||
if (_controller.text != state.content) {
|
|
||||||
_controller.text = state.content;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
child: BlocBuilder<BoardTextCellBloc, BoardTextCellState>(
|
|
||||||
buildWhen: (previous, current) {
|
|
||||||
if (previous.content != current.content &&
|
|
||||||
_controller.text == current.content &&
|
|
||||||
current.enableEdit) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return previous != current;
|
|
||||||
},
|
|
||||||
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,
|
|
||||||
maxLines: null, // Enable multiple lines
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildTextField() {
|
|
||||||
return IntrinsicHeight(
|
|
||||||
child: TextField(
|
|
||||||
controller: _controller,
|
|
||||||
focusNode: focusNode,
|
|
||||||
onChanged: (value) => focusChanged(),
|
|
||||||
onEditingComplete: () => focusNode.unfocus(),
|
|
||||||
maxLines: null,
|
|
||||||
style: Theme.of(context).textTheme.bodyMedium!.size(FontSizes.s14),
|
|
||||||
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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,77 +0,0 @@
|
|||||||
import 'package:app_flowy/plugins/board/application/card/board_url_cell_bloc.dart';
|
|
||||||
import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart';
|
|
||||||
import 'package:flowy_infra/size.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
||||||
import 'package:textstyle_extensions/textstyle_extensions.dart';
|
|
||||||
|
|
||||||
import 'define.dart';
|
|
||||||
|
|
||||||
class BoardUrlCell extends StatefulWidget {
|
|
||||||
final String groupId;
|
|
||||||
final GridCellControllerBuilder cellControllerBuilder;
|
|
||||||
|
|
||||||
const BoardUrlCell({
|
|
||||||
required this.groupId,
|
|
||||||
required this.cellControllerBuilder,
|
|
||||||
Key? key,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<BoardUrlCell> createState() => _BoardUrlCellState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _BoardUrlCellState extends State<BoardUrlCell> {
|
|
||||||
late BoardURLCellBloc _cellBloc;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
final cellController =
|
|
||||||
widget.cellControllerBuilder.build() as GridURLCellController;
|
|
||||||
_cellBloc = BoardURLCellBloc(cellController: cellController);
|
|
||||||
_cellBloc.add(const BoardURLCellEvent.initial());
|
|
||||||
super.initState();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
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();
|
|
||||||
} else {
|
|
||||||
return Align(
|
|
||||||
alignment: Alignment.centerLeft,
|
|
||||||
child: Padding(
|
|
||||||
padding: EdgeInsets.symmetric(
|
|
||||||
vertical: BoardSizes.cardCellVPadding,
|
|
||||||
),
|
|
||||||
child: RichText(
|
|
||||||
textAlign: TextAlign.left,
|
|
||||||
text: TextSpan(
|
|
||||||
text: state.content,
|
|
||||||
style: Theme.of(context)
|
|
||||||
.textTheme
|
|
||||||
.bodyMedium!
|
|
||||||
.size(FontSizes.s14)
|
|
||||||
.textColor(Theme.of(context).colorScheme.primary)
|
|
||||||
.underline,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> dispose() async {
|
|
||||||
_cellBloc.close();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,257 +0,0 @@
|
|||||||
import 'package:app_flowy/plugins/board/application/card/card_bloc.dart';
|
|
||||||
import 'package:app_flowy/plugins/board/application/card/card_data_controller.dart';
|
|
||||||
import 'package:app_flowy/plugins/grid/presentation/widgets/row/row_action_sheet.dart';
|
|
||||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
|
||||||
import 'package:flowy_infra/image.dart';
|
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
||||||
import 'board_cell.dart';
|
|
||||||
import 'card_cell_builder.dart';
|
|
||||||
import 'container/accessory.dart';
|
|
||||||
import 'container/card_container.dart';
|
|
||||||
|
|
||||||
class BoardCard extends StatefulWidget {
|
|
||||||
final String viewId;
|
|
||||||
final String groupId;
|
|
||||||
final String fieldId;
|
|
||||||
final bool isEditing;
|
|
||||||
final CardDataController dataController;
|
|
||||||
final BoardCellBuilder cellBuilder;
|
|
||||||
final void Function(BuildContext) openCard;
|
|
||||||
final VoidCallback onStartEditing;
|
|
||||||
final VoidCallback onEndEditing;
|
|
||||||
|
|
||||||
const BoardCard({
|
|
||||||
required this.viewId,
|
|
||||||
required this.groupId,
|
|
||||||
required this.fieldId,
|
|
||||||
required this.isEditing,
|
|
||||||
required this.dataController,
|
|
||||||
required this.cellBuilder,
|
|
||||||
required this.openCard,
|
|
||||||
required this.onStartEditing,
|
|
||||||
required this.onEndEditing,
|
|
||||||
Key? key,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<BoardCard> createState() => _BoardCardState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _BoardCardState extends State<BoardCard> {
|
|
||||||
late BoardCardBloc _cardBloc;
|
|
||||||
late EditableRowNotifier rowNotifier;
|
|
||||||
late PopoverController popoverController;
|
|
||||||
AccessoryType? accessoryType;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
rowNotifier = EditableRowNotifier(isEditing: widget.isEditing);
|
|
||||||
_cardBloc = BoardCardBloc(
|
|
||||||
viewId: widget.viewId,
|
|
||||||
groupFieldId: widget.fieldId,
|
|
||||||
dataController: widget.dataController,
|
|
||||||
isEditing: widget.isEditing,
|
|
||||||
)..add(const BoardCardEvent.initial());
|
|
||||||
|
|
||||||
rowNotifier.isEditing.addListener(() {
|
|
||||||
if (!mounted) return;
|
|
||||||
_cardBloc.add(BoardCardEvent.setIsEditing(rowNotifier.isEditing.value));
|
|
||||||
|
|
||||||
if (rowNotifier.isEditing.value) {
|
|
||||||
widget.onStartEditing();
|
|
||||||
} else {
|
|
||||||
widget.onEndEditing();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
popoverController = PopoverController();
|
|
||||||
super.initState();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return BlocProvider.value(
|
|
||||||
value: _cardBloc,
|
|
||||||
child: BlocBuilder<BoardCardBloc, BoardCardState>(
|
|
||||||
buildWhen: (previous, current) {
|
|
||||||
// Rebuild when:
|
|
||||||
// 1.If the length of the cells is not the same
|
|
||||||
// 2.isEditing changed
|
|
||||||
if (previous.cells.length != current.cells.length ||
|
|
||||||
previous.isEditing != current.isEditing) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3.Compare the content of the cells. The cells consists of
|
|
||||||
// list of [BoardCellEquatable] that extends the [Equatable].
|
|
||||||
return !listEquals(previous.cells, current.cells);
|
|
||||||
},
|
|
||||||
builder: (context, state) {
|
|
||||||
return AppFlowyPopover(
|
|
||||||
controller: popoverController,
|
|
||||||
triggerActions: PopoverTriggerFlags.none,
|
|
||||||
constraints: BoxConstraints.loose(const Size(140, 200)),
|
|
||||||
margin: const EdgeInsets.all(6),
|
|
||||||
direction: PopoverDirection.rightWithCenterAligned,
|
|
||||||
popupBuilder: (popoverContext) => _handlePopoverBuilder(
|
|
||||||
context,
|
|
||||||
popoverContext,
|
|
||||||
),
|
|
||||||
child: BoardCardContainer(
|
|
||||||
buildAccessoryWhen: () => state.isEditing == false,
|
|
||||||
accessoryBuilder: (context) {
|
|
||||||
return [
|
|
||||||
_CardEditOption(rowNotifier: rowNotifier),
|
|
||||||
_CardMoreOption(),
|
|
||||||
];
|
|
||||||
},
|
|
||||||
openAccessory: _handleOpenAccessory,
|
|
||||||
openCard: (context) => widget.openCard(context),
|
|
||||||
child: _CellColumn(
|
|
||||||
groupId: widget.groupId,
|
|
||||||
rowNotifier: rowNotifier,
|
|
||||||
cellBuilder: widget.cellBuilder,
|
|
||||||
cells: state.cells,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _handleOpenAccessory(AccessoryType newAccessoryType) {
|
|
||||||
accessoryType = newAccessoryType;
|
|
||||||
switch (newAccessoryType) {
|
|
||||||
case AccessoryType.edit:
|
|
||||||
break;
|
|
||||||
case AccessoryType.more:
|
|
||||||
popoverController.show();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _handlePopoverBuilder(
|
|
||||||
BuildContext context,
|
|
||||||
BuildContext popoverContext,
|
|
||||||
) {
|
|
||||||
switch (accessoryType!) {
|
|
||||||
case AccessoryType.edit:
|
|
||||||
throw UnimplementedError();
|
|
||||||
case AccessoryType.more:
|
|
||||||
return GridRowActionSheet(
|
|
||||||
rowData: context.read<BoardCardBloc>().rowInfo(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> dispose() async {
|
|
||||||
rowNotifier.dispose();
|
|
||||||
_cardBloc.close();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _CellColumn extends StatelessWidget {
|
|
||||||
final String groupId;
|
|
||||||
final BoardCellBuilder cellBuilder;
|
|
||||||
final EditableRowNotifier rowNotifier;
|
|
||||||
final List<BoardCellEquatable> cells;
|
|
||||||
const _CellColumn({
|
|
||||||
required this.groupId,
|
|
||||||
required this.rowNotifier,
|
|
||||||
required this.cellBuilder,
|
|
||||||
required this.cells,
|
|
||||||
Key? key,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: _makeCells(context, cells),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
List<Widget> _makeCells(
|
|
||||||
BuildContext context,
|
|
||||||
List<BoardCellEquatable> cells,
|
|
||||||
) {
|
|
||||||
final List<Widget> children = [];
|
|
||||||
// Remove all the cell listeners.
|
|
||||||
rowNotifier.unbind();
|
|
||||||
|
|
||||||
cells.asMap().forEach(
|
|
||||||
(int index, BoardCellEquatable cell) {
|
|
||||||
final isEditing = index == 0 ? rowNotifier.isEditing.value : false;
|
|
||||||
final cellNotifier = EditableCellNotifier(isEditing: isEditing);
|
|
||||||
|
|
||||||
if (index == 0) {
|
|
||||||
// Only use the first cell to receive user's input when click the edit
|
|
||||||
// button
|
|
||||||
rowNotifier.bindCell(cell.identifier, cellNotifier);
|
|
||||||
}
|
|
||||||
|
|
||||||
final child = Padding(
|
|
||||||
key: cell.identifier.key(),
|
|
||||||
padding: const EdgeInsets.only(left: 4, right: 4),
|
|
||||||
child: cellBuilder.buildCell(
|
|
||||||
groupId,
|
|
||||||
cell.identifier,
|
|
||||||
cellNotifier,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
children.add(child);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
return children;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _CardMoreOption extends StatelessWidget with CardAccessory {
|
|
||||||
_CardMoreOption({Key? key}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.all(3.0),
|
|
||||||
child: svgWidget(
|
|
||||||
'grid/details',
|
|
||||||
color: Theme.of(context).colorScheme.onSurface,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
AccessoryType get type => AccessoryType.more;
|
|
||||||
}
|
|
||||||
|
|
||||||
class _CardEditOption extends StatelessWidget with CardAccessory {
|
|
||||||
final EditableRowNotifier rowNotifier;
|
|
||||||
const _CardEditOption({
|
|
||||||
required this.rowNotifier,
|
|
||||||
Key? key,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.all(3.0),
|
|
||||||
child: svgWidget(
|
|
||||||
'editor/edit',
|
|
||||||
color: Theme.of(context).colorScheme.onSurface,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void onTap(BuildContext context) => rowNotifier.becomeFirstResponder();
|
|
||||||
|
|
||||||
@override
|
|
||||||
AccessoryType get type => AccessoryType.edit;
|
|
||||||
}
|
|
@ -1,89 +0,0 @@
|
|||||||
import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-database/field_entities.pb.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import 'board_cell.dart';
|
|
||||||
import 'board_checkbox_cell.dart';
|
|
||||||
import 'board_checklist_cell.dart';
|
|
||||||
import 'board_date_cell.dart';
|
|
||||||
import 'board_number_cell.dart';
|
|
||||||
import 'board_select_option_cell.dart';
|
|
||||||
import 'board_text_cell.dart';
|
|
||||||
import 'board_url_cell.dart';
|
|
||||||
|
|
||||||
abstract class BoardCellBuilderDelegate
|
|
||||||
extends GridCellControllerBuilderDelegate {
|
|
||||||
GridCellCache get cellCache;
|
|
||||||
}
|
|
||||||
|
|
||||||
class BoardCellBuilder {
|
|
||||||
final BoardCellBuilderDelegate delegate;
|
|
||||||
|
|
||||||
BoardCellBuilder(this.delegate);
|
|
||||||
|
|
||||||
Widget buildCell(
|
|
||||||
String groupId,
|
|
||||||
GridCellIdentifier cellId,
|
|
||||||
EditableCellNotifier cellNotifier,
|
|
||||||
) {
|
|
||||||
final cellControllerBuilder = GridCellControllerBuilder(
|
|
||||||
delegate: delegate,
|
|
||||||
cellId: cellId,
|
|
||||||
cellCache: delegate.cellCache,
|
|
||||||
);
|
|
||||||
|
|
||||||
final key = cellId.key();
|
|
||||||
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.Checklist:
|
|
||||||
return BoardChecklistCell(
|
|
||||||
cellControllerBuilder: cellControllerBuilder,
|
|
||||||
key: key,
|
|
||||||
);
|
|
||||||
case FieldType.Number:
|
|
||||||
return BoardNumberCell(
|
|
||||||
groupId: groupId,
|
|
||||||
cellControllerBuilder: cellControllerBuilder,
|
|
||||||
key: key,
|
|
||||||
);
|
|
||||||
case FieldType.RichText:
|
|
||||||
return BoardTextCell(
|
|
||||||
groupId: groupId,
|
|
||||||
cellControllerBuilder: cellControllerBuilder,
|
|
||||||
editableNotifier: cellNotifier,
|
|
||||||
key: key,
|
|
||||||
);
|
|
||||||
case FieldType.URL:
|
|
||||||
return BoardUrlCell(
|
|
||||||
groupId: groupId,
|
|
||||||
cellControllerBuilder: cellControllerBuilder,
|
|
||||||
key: key,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
throw UnimplementedError;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,187 +0,0 @@
|
|||||||
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_controller.dart';
|
|
||||||
import 'package:app_flowy/plugins/grid/presentation/layout/sizes.dart';
|
|
||||||
import 'package:app_flowy/plugins/grid/presentation/widgets/toolbar/grid_group.dart';
|
|
||||||
import 'package:app_flowy/plugins/grid/presentation/widgets/toolbar/grid_property.dart';
|
|
||||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
|
||||||
import 'package:flowy_infra/image.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 'package:styled_widget/styled_widget.dart';
|
|
||||||
|
|
||||||
import 'board_toolbar.dart';
|
|
||||||
|
|
||||||
class BoardSettingContext {
|
|
||||||
final String viewId;
|
|
||||||
final GridFieldController fieldController;
|
|
||||||
BoardSettingContext({
|
|
||||||
required this.viewId,
|
|
||||||
required this.fieldController,
|
|
||||||
});
|
|
||||||
|
|
||||||
factory BoardSettingContext.from(BoardToolbarContext toolbarContext) =>
|
|
||||||
BoardSettingContext(
|
|
||||||
viewId: toolbarContext.viewId,
|
|
||||||
fieldController: toolbarContext.fieldController,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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(databaseId: settingContext.viewId),
|
|
||||||
child: BlocListener<BoardSettingBloc, BoardSettingState>(
|
|
||||||
listenWhen: (previous, current) =>
|
|
||||||
previous.selectedAction != current.selectedAction,
|
|
||||||
listener: (context, state) {
|
|
||||||
state.selectedAction.foldLeft(null, (_, action) {
|
|
||||||
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];
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _SettingItem extends StatelessWidget {
|
|
||||||
final BoardSettingAction action;
|
|
||||||
|
|
||||||
const _SettingItem({
|
|
||||||
required this.action,
|
|
||||||
Key? key,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
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()),
|
|
||||||
onTap: () {
|
|
||||||
context
|
|
||||||
.read<BoardSettingBloc>()
|
|
||||||
.add(BoardSettingEvent.performAction(action));
|
|
||||||
},
|
|
||||||
leftIcon: svgWidget(
|
|
||||||
action.iconName(),
|
|
||||||
color: Theme.of(context).colorScheme.onSurface,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension _GridSettingExtension on BoardSettingAction {
|
|
||||||
String iconName() {
|
|
||||||
switch (this) {
|
|
||||||
case BoardSettingAction.properties:
|
|
||||||
return 'grid/setting/properties';
|
|
||||||
case BoardSettingAction.groups:
|
|
||||||
return 'grid/setting/group';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String title() {
|
|
||||||
switch (this) {
|
|
||||||
case BoardSettingAction.properties:
|
|
||||||
return LocaleKeys.grid_settings_Properties.tr();
|
|
||||||
case BoardSettingAction.groups:
|
|
||||||
return LocaleKeys.grid_settings_group.tr();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class BoardSettingListPopover extends StatefulWidget {
|
|
||||||
final PopoverController popoverController;
|
|
||||||
final BoardSettingContext settingContext;
|
|
||||||
|
|
||||||
const BoardSettingListPopover({
|
|
||||||
Key? key,
|
|
||||||
required this.popoverController,
|
|
||||||
required this.settingContext,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<StatefulWidget> createState() => _BoardSettingListPopoverState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _BoardSettingListPopoverState extends State<BoardSettingListPopover> {
|
|
||||||
BoardSettingAction? _action;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
if (_action != null) {
|
|
||||||
switch (_action!) {
|
|
||||||
case BoardSettingAction.groups:
|
|
||||||
return GridGroupList(
|
|
||||||
viewId: widget.settingContext.viewId,
|
|
||||||
fieldController: widget.settingContext.fieldController,
|
|
||||||
onDismissed: () {
|
|
||||||
widget.popoverController.close();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
case BoardSettingAction.properties:
|
|
||||||
return GridPropertyList(
|
|
||||||
databaseId: widget.settingContext.viewId,
|
|
||||||
fieldController: widget.settingContext.fieldController,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return BoardSettingList(
|
|
||||||
settingContext: widget.settingContext,
|
|
||||||
onAction: (action, settingContext) {
|
|
||||||
setState(() => _action = action);
|
|
||||||
},
|
|
||||||
).padding(all: 6.0);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,89 +0,0 @@
|
|||||||
import 'package:app_flowy/plugins/grid/application/field/field_controller.dart';
|
|
||||||
import 'package:app_flowy/plugins/grid/presentation/layout/sizes.dart';
|
|
||||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
|
||||||
import 'package:flowy_infra/theme_extension.dart';
|
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
|
||||||
import 'package:flowy_infra_ui/style_widget/button.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import '../../../../generated/locale_keys.g.dart';
|
|
||||||
import 'board_setting.dart';
|
|
||||||
|
|
||||||
class BoardToolbarContext {
|
|
||||||
final String viewId;
|
|
||||||
final GridFieldController fieldController;
|
|
||||||
|
|
||||||
BoardToolbarContext({
|
|
||||||
required this.viewId,
|
|
||||||
required this.fieldController,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
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: [
|
|
||||||
const Spacer(),
|
|
||||||
_SettingButton(
|
|
||||||
settingContext: BoardSettingContext.from(toolbarContext),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _SettingButton extends StatefulWidget {
|
|
||||||
final BoardSettingContext settingContext;
|
|
||||||
const _SettingButton({required this.settingContext, Key? key})
|
|
||||||
: super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<_SettingButton> createState() => _SettingButtonState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _SettingButtonState extends State<_SettingButton> {
|
|
||||||
late PopoverController popoverController;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
popoverController = PopoverController();
|
|
||||||
super.initState();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return AppFlowyPopover(
|
|
||||||
controller: popoverController,
|
|
||||||
direction: PopoverDirection.leftWithTopAligned,
|
|
||||||
triggerActions: PopoverTriggerFlags.none,
|
|
||||||
constraints: BoxConstraints.loose(const Size(260, 400)),
|
|
||||||
margin: EdgeInsets.zero,
|
|
||||||
child: FlowyTextButton(
|
|
||||||
LocaleKeys.settings_title.tr(),
|
|
||||||
fillColor: Colors.transparent,
|
|
||||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
|
||||||
padding: GridSize.typeOptionContentInsets,
|
|
||||||
onPressed: () {
|
|
||||||
popoverController.show();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
popupBuilder: (BuildContext popoverContext) {
|
|
||||||
return BoardSettingListPopover(
|
|
||||||
settingContext: widget.settingContext,
|
|
||||||
popoverController: popoverController,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,16 +0,0 @@
|
|||||||
// import 'package:flutter_test/flutter_test.dart';
|
|
||||||
// import 'package:integration_test/integration_test.dart';
|
|
||||||
// import 'package:app_flowy/main.dart' as app;
|
|
||||||
|
|
||||||
// void main() {
|
|
||||||
// IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
|
||||||
|
|
||||||
// group('end-to-end test', () {
|
|
||||||
// testWidgets('tap on the floating action button, verify counter',
|
|
||||||
// (tester) async {
|
|
||||||
// app.main();
|
|
||||||
|
|
||||||
// await tester.pumpAndSettle();
|
|
||||||
// });
|
|
||||||
// });
|
|
||||||
// }
|
|
@ -1,159 +0,0 @@
|
|||||||
import 'package:app_flowy/plugins/grid/application/field/field_controller.dart';
|
|
||||||
import 'package:app_flowy/plugins/grid/application/row/row_cache.dart';
|
|
||||||
import 'package:appflowy_backend/log.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-error/protobuf.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-database/protobuf.dart';
|
|
||||||
import 'package:calendar_view/calendar_view.dart';
|
|
||||||
import 'package:dartz/dartz.dart';
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
|
||||||
|
|
||||||
import 'calendar_data_controller.dart';
|
|
||||||
|
|
||||||
part 'calendar_bloc.freezed.dart';
|
|
||||||
|
|
||||||
class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
|
|
||||||
final CalendarDataController _databaseDataController;
|
|
||||||
final EventController calendarEventsController = EventController();
|
|
||||||
|
|
||||||
GridFieldController get fieldController =>
|
|
||||||
_databaseDataController.fieldController;
|
|
||||||
String get databaseId => _databaseDataController.databaseId;
|
|
||||||
|
|
||||||
CalendarBloc({required ViewPB view})
|
|
||||||
: _databaseDataController = CalendarDataController(view: view),
|
|
||||||
super(CalendarState.initial(view.id)) {
|
|
||||||
on<CalendarEvent>(
|
|
||||||
(event, emit) async {
|
|
||||||
await event.when(
|
|
||||||
initial: () async {
|
|
||||||
_startListening();
|
|
||||||
await _openDatabase(emit);
|
|
||||||
},
|
|
||||||
didReceiveCalendarSettings: (CalendarSettingsPB settings) {
|
|
||||||
emit(state.copyWith(settings: Some(settings)));
|
|
||||||
},
|
|
||||||
didReceiveDatabaseUpdate: (DatabasePB database) {
|
|
||||||
emit(state.copyWith(database: Some(database)));
|
|
||||||
},
|
|
||||||
didReceiveError: (FlowyError error) {
|
|
||||||
emit(state.copyWith(noneOrError: Some(error)));
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _openDatabase(Emitter<CalendarState> emit) async {
|
|
||||||
final result = await _databaseDataController.openDatabase();
|
|
||||||
result.fold(
|
|
||||||
(database) => emit(
|
|
||||||
state.copyWith(loadingState: DatabaseLoadingState.finish(left(unit))),
|
|
||||||
),
|
|
||||||
(err) => emit(
|
|
||||||
state.copyWith(loadingState: DatabaseLoadingState.finish(right(err))),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
GridRowCache? getRowCache(String blockId) {
|
|
||||||
return _databaseDataController.rowCache;
|
|
||||||
}
|
|
||||||
|
|
||||||
void _startListening() {
|
|
||||||
_databaseDataController.addListener(
|
|
||||||
onDatabaseChanged: (database) {
|
|
||||||
if (!isClosed) return;
|
|
||||||
|
|
||||||
add(CalendarEvent.didReceiveDatabaseUpdate(database));
|
|
||||||
},
|
|
||||||
onSettingsChanged: (CalendarSettingsPB settings) {
|
|
||||||
if (isClosed) return;
|
|
||||||
add(CalendarEvent.didReceiveCalendarSettings(settings));
|
|
||||||
},
|
|
||||||
onArrangeWithNewField: (field) {
|
|
||||||
if (isClosed) return;
|
|
||||||
_initializeEvents(field);
|
|
||||||
// add(CalendarEvent.)
|
|
||||||
},
|
|
||||||
onError: (err) {
|
|
||||||
Log.error(err);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _initializeEvents(FieldPB dateField) {
|
|
||||||
calendarEventsController.removeWhere((element) => true);
|
|
||||||
|
|
||||||
const events = <CalendarEventData<CalendarData>>[];
|
|
||||||
|
|
||||||
// final List<CalendarEventData<CalendarData>> events = rows.map((row) {
|
|
||||||
// final event = CalendarEventData(
|
|
||||||
// title: "",
|
|
||||||
// date: row -> dateField -> value,
|
|
||||||
// event: row,
|
|
||||||
// );
|
|
||||||
|
|
||||||
// return event;
|
|
||||||
// }).toList();
|
|
||||||
|
|
||||||
calendarEventsController.addAll(events);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@freezed
|
|
||||||
class CalendarEvent with _$CalendarEvent {
|
|
||||||
const factory CalendarEvent.initial() = _InitialCalendar;
|
|
||||||
const factory CalendarEvent.didReceiveCalendarSettings(
|
|
||||||
CalendarSettingsPB settings) = _DidReceiveCalendarSettings;
|
|
||||||
const factory CalendarEvent.didReceiveError(FlowyError error) =
|
|
||||||
_DidReceiveError;
|
|
||||||
const factory CalendarEvent.didReceiveDatabaseUpdate(DatabasePB database) =
|
|
||||||
_DidReceiveDatabaseUpdate;
|
|
||||||
}
|
|
||||||
|
|
||||||
@freezed
|
|
||||||
class CalendarState with _$CalendarState {
|
|
||||||
const factory CalendarState({
|
|
||||||
required String databaseId,
|
|
||||||
required Option<DatabasePB> database,
|
|
||||||
required Option<FieldPB> dateField,
|
|
||||||
required Option<List<RowInfo>> unscheduledRows,
|
|
||||||
required Option<CalendarSettingsPB> settings,
|
|
||||||
required DatabaseLoadingState loadingState,
|
|
||||||
required Option<FlowyError> noneOrError,
|
|
||||||
}) = _CalendarState;
|
|
||||||
|
|
||||||
factory CalendarState.initial(String databaseId) => CalendarState(
|
|
||||||
database: none(),
|
|
||||||
databaseId: databaseId,
|
|
||||||
dateField: none(),
|
|
||||||
unscheduledRows: none(),
|
|
||||||
settings: none(),
|
|
||||||
noneOrError: none(),
|
|
||||||
loadingState: const _Loading(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@freezed
|
|
||||||
class DatabaseLoadingState with _$DatabaseLoadingState {
|
|
||||||
const factory DatabaseLoadingState.loading() = _Loading;
|
|
||||||
const factory DatabaseLoadingState.finish(
|
|
||||||
Either<Unit, FlowyError> successOrFail) = _Finish;
|
|
||||||
}
|
|
||||||
|
|
||||||
class CalendarEditingRow {
|
|
||||||
RowPB row;
|
|
||||||
int? index;
|
|
||||||
|
|
||||||
CalendarEditingRow({
|
|
||||||
required this.row,
|
|
||||||
required this.index,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
class CalendarData {
|
|
||||||
final RowInfo rowInfo;
|
|
||||||
CalendarData(this.rowInfo);
|
|
||||||
}
|
|
@ -1,115 +0,0 @@
|
|||||||
import 'dart:async';
|
|
||||||
import 'dart:collection';
|
|
||||||
|
|
||||||
import 'package:app_flowy/plugins/grid/application/view/grid_view_cache.dart';
|
|
||||||
import 'package:app_flowy/plugins/grid/application/field/field_controller.dart';
|
|
||||||
import 'package:app_flowy/plugins/grid/application/grid_service.dart';
|
|
||||||
import 'package:app_flowy/plugins/grid/application/row/row_cache.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-database/protobuf.dart';
|
|
||||||
import 'package:dartz/dartz.dart';
|
|
||||||
|
|
||||||
import 'calendar_listener.dart';
|
|
||||||
|
|
||||||
typedef OnFieldsChanged = void Function(UnmodifiableListView<FieldInfo>);
|
|
||||||
typedef OnDatabaseChanged = void Function(DatabasePB);
|
|
||||||
typedef OnSettingsChanged = void Function(CalendarSettingsPB);
|
|
||||||
typedef OnArrangeWithNewField = void Function(FieldPB);
|
|
||||||
|
|
||||||
typedef OnRowsChanged = void Function(List<RowInfo>, RowsChangedReason);
|
|
||||||
typedef OnError = void Function(FlowyError);
|
|
||||||
|
|
||||||
class CalendarDataController {
|
|
||||||
final String databaseId;
|
|
||||||
final DatabaseFFIService _databaseFFIService;
|
|
||||||
final GridFieldController fieldController;
|
|
||||||
final CalendarListener _listener;
|
|
||||||
late DatabaseViewCache _viewCache;
|
|
||||||
|
|
||||||
OnFieldsChanged? _onFieldsChanged;
|
|
||||||
OnDatabaseChanged? _onDatabaseChanged;
|
|
||||||
OnRowsChanged? _onRowsChanged;
|
|
||||||
OnSettingsChanged? _onSettingsChanged;
|
|
||||||
OnArrangeWithNewField? _onArrangeWithNewField;
|
|
||||||
OnError? _onError;
|
|
||||||
|
|
||||||
List<RowInfo> get rowInfos => _viewCache.rowInfos;
|
|
||||||
GridRowCache get rowCache => _viewCache.rowCache;
|
|
||||||
|
|
||||||
CalendarDataController({required ViewPB view})
|
|
||||||
: databaseId = view.id,
|
|
||||||
_listener = CalendarListener(view.id),
|
|
||||||
_databaseFFIService = DatabaseFFIService(viewId: view.id),
|
|
||||||
fieldController = GridFieldController(databaseId: view.id) {
|
|
||||||
_viewCache = DatabaseViewCache(
|
|
||||||
databaseId: view.id,
|
|
||||||
fieldController: fieldController,
|
|
||||||
);
|
|
||||||
_viewCache.addListener(onRowsChanged: (reason) {
|
|
||||||
_onRowsChanged?.call(rowInfos, reason);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void addListener({
|
|
||||||
required OnDatabaseChanged onDatabaseChanged,
|
|
||||||
OnFieldsChanged? onFieldsChanged,
|
|
||||||
OnRowsChanged? onRowsChanged,
|
|
||||||
required OnSettingsChanged? onSettingsChanged,
|
|
||||||
required OnArrangeWithNewField? onArrangeWithNewField,
|
|
||||||
required OnError? onError,
|
|
||||||
}) {
|
|
||||||
_onDatabaseChanged = onDatabaseChanged;
|
|
||||||
_onFieldsChanged = onFieldsChanged;
|
|
||||||
_onRowsChanged = onRowsChanged;
|
|
||||||
_onSettingsChanged = onSettingsChanged;
|
|
||||||
_onArrangeWithNewField = onArrangeWithNewField;
|
|
||||||
_onError = onError;
|
|
||||||
|
|
||||||
fieldController.addListener(onFields: (fields) {
|
|
||||||
_onFieldsChanged?.call(UnmodifiableListView(fields));
|
|
||||||
});
|
|
||||||
|
|
||||||
_listener.start(
|
|
||||||
onCalendarSettingsChanged: (result) {
|
|
||||||
result.fold(
|
|
||||||
(settings) => _onSettingsChanged?.call(settings),
|
|
||||||
(e) => _onError?.call(e),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
onArrangeWithNewField: (result) {
|
|
||||||
result.fold(
|
|
||||||
(settings) => _onArrangeWithNewField?.call(settings),
|
|
||||||
(e) => _onError?.call(e),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<Either<Unit, FlowyError>> openDatabase() async {
|
|
||||||
final result = await _databaseFFIService.openGrid();
|
|
||||||
return result.fold(
|
|
||||||
(database) async {
|
|
||||||
_onDatabaseChanged?.call(database);
|
|
||||||
return fieldController
|
|
||||||
.loadFields(fieldIds: database.fields)
|
|
||||||
.then((result) {
|
|
||||||
return result.fold(
|
|
||||||
(l) => Future(() async {
|
|
||||||
_viewCache.rowCache.initializeRows(database.rows);
|
|
||||||
return left(l);
|
|
||||||
}),
|
|
||||||
(err) => right(err),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
(err) => right(err),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> dispose() async {
|
|
||||||
await _viewCache.dispose();
|
|
||||||
await _databaseFFIService.closeGrid();
|
|
||||||
await fieldController.dispose();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,65 +0,0 @@
|
|||||||
import 'dart:typed_data';
|
|
||||||
|
|
||||||
import 'package:app_flowy/core/grid_notification.dart';
|
|
||||||
import 'package:flowy_infra/notifier.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-error/protobuf.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-database/protobuf.dart';
|
|
||||||
import 'package:dartz/dartz.dart';
|
|
||||||
|
|
||||||
typedef CalendarSettingsValue = Either<CalendarSettingsPB, FlowyError>;
|
|
||||||
typedef ArrangeWithNewField = Either<FieldPB, FlowyError>;
|
|
||||||
|
|
||||||
class CalendarListener {
|
|
||||||
final String viewId;
|
|
||||||
PublishNotifier<CalendarSettingsValue>? _calendarSettingsNotifier =
|
|
||||||
PublishNotifier();
|
|
||||||
PublishNotifier<ArrangeWithNewField>? _arrangeWithNewFieldNotifier =
|
|
||||||
PublishNotifier();
|
|
||||||
DatabaseNotificationListener? _listener;
|
|
||||||
CalendarListener(this.viewId);
|
|
||||||
|
|
||||||
void start({
|
|
||||||
required void Function(CalendarSettingsValue) onCalendarSettingsChanged,
|
|
||||||
required void Function(ArrangeWithNewField) onArrangeWithNewField,
|
|
||||||
}) {
|
|
||||||
_calendarSettingsNotifier?.addPublishListener(onCalendarSettingsChanged);
|
|
||||||
_arrangeWithNewFieldNotifier?.addPublishListener(onArrangeWithNewField);
|
|
||||||
_listener = DatabaseNotificationListener(
|
|
||||||
objectId: viewId,
|
|
||||||
handler: _handler,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _handler(
|
|
||||||
DatabaseNotification ty,
|
|
||||||
Either<Uint8List, FlowyError> result,
|
|
||||||
) {
|
|
||||||
switch (ty) {
|
|
||||||
case DatabaseNotification.DidUpdateCalendarSettings:
|
|
||||||
result.fold(
|
|
||||||
(payload) => _calendarSettingsNotifier?.value =
|
|
||||||
left(CalendarSettingsPB.fromBuffer(payload)),
|
|
||||||
(error) => _calendarSettingsNotifier?.value = right(error),
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case DatabaseNotification.DidArrangeCalendarWithNewField:
|
|
||||||
result.fold(
|
|
||||||
(payload) => _arrangeWithNewFieldNotifier?.value =
|
|
||||||
left(FieldPB.fromBuffer(payload)),
|
|
||||||
(error) => _arrangeWithNewFieldNotifier?.value = right(error),
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> stop() async {
|
|
||||||
await _listener?.stop();
|
|
||||||
_calendarSettingsNotifier?.dispose();
|
|
||||||
_calendarSettingsNotifier = null;
|
|
||||||
|
|
||||||
_arrangeWithNewFieldNotifier?.dispose();
|
|
||||||
_arrangeWithNewFieldNotifier = null;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,89 +0,0 @@
|
|||||||
import 'package:app_flowy/generated/locale_keys.g.dart';
|
|
||||||
import 'package:app_flowy/startup/plugin/plugin.dart';
|
|
||||||
import 'package:app_flowy/workspace/presentation/home/home_stack.dart';
|
|
||||||
import 'package:app_flowy/workspace/presentation/widgets/left_bar_item.dart';
|
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import '../util.dart';
|
|
||||||
import 'presentation/calendar_page.dart';
|
|
||||||
|
|
||||||
class CalendarPluginBuilder extends PluginBuilder {
|
|
||||||
@override
|
|
||||||
Plugin build(dynamic data) {
|
|
||||||
if (data is ViewPB) {
|
|
||||||
return CalendarPlugin(pluginType: pluginType, view: data);
|
|
||||||
} else {
|
|
||||||
throw FlowyPluginException.invalidData;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get menuName => LocaleKeys.calendar_menuName.tr();
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get menuIcon => "editor/date";
|
|
||||||
|
|
||||||
@override
|
|
||||||
PluginType get pluginType => PluginType.calendar;
|
|
||||||
|
|
||||||
@override
|
|
||||||
ViewDataFormatPB get dataFormatType => ViewDataFormatPB.DatabaseFormat;
|
|
||||||
|
|
||||||
@override
|
|
||||||
ViewLayoutTypePB? get layoutType => ViewLayoutTypePB.Calendar;
|
|
||||||
}
|
|
||||||
|
|
||||||
class CalendarPluginConfig implements PluginConfig {
|
|
||||||
@override
|
|
||||||
bool get creatable => false;
|
|
||||||
}
|
|
||||||
|
|
||||||
class CalendarPlugin extends Plugin {
|
|
||||||
@override
|
|
||||||
final ViewPluginNotifier notifier;
|
|
||||||
final PluginType _pluginType;
|
|
||||||
|
|
||||||
CalendarPlugin({
|
|
||||||
required ViewPB view,
|
|
||||||
required PluginType pluginType,
|
|
||||||
}) : _pluginType = pluginType,
|
|
||||||
notifier = ViewPluginNotifier(view: view);
|
|
||||||
|
|
||||||
@override
|
|
||||||
PluginDisplay get display => CalendarPluginDisplay(notifier: notifier);
|
|
||||||
|
|
||||||
@override
|
|
||||||
PluginId get id => notifier.view.id;
|
|
||||||
|
|
||||||
@override
|
|
||||||
PluginType get ty => _pluginType;
|
|
||||||
}
|
|
||||||
|
|
||||||
class CalendarPluginDisplay extends PluginDisplay {
|
|
||||||
final ViewPluginNotifier notifier;
|
|
||||||
CalendarPluginDisplay({required this.notifier, Key? key});
|
|
||||||
|
|
||||||
ViewPB get view => notifier.view;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget get leftBarItem => ViewLeftBarItem(view: view);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget buildWidget(PluginContext context) {
|
|
||||||
notifier.isDeleted.addListener(() {
|
|
||||||
notifier.isDeleted.value.fold(() => null, (deletedView) {
|
|
||||||
if (deletedView.hasIndex()) {
|
|
||||||
context.onDeleted(view, deletedView.index);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return CalendarPage(key: ValueKey(view.id));
|
|
||||||
// return CalendarPage(key: ValueKey(view.id), view: view);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<NavigationItem> get navigationItems => [this];
|
|
||||||
}
|
|
@ -1,162 +0,0 @@
|
|||||||
import 'package:app_flowy/generated/locale_keys.g.dart';
|
|
||||||
import 'package:app_flowy/plugins/grid/presentation/layout/sizes.dart';
|
|
||||||
import 'package:calendar_view/calendar_view.dart';
|
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
|
||||||
import 'package:flowy_infra/image.dart';
|
|
||||||
import 'package:flowy_infra/size.dart';
|
|
||||||
import 'package:flowy_infra/theme_extension.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/text.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
|
||||||
|
|
||||||
import 'layout/sizes.dart';
|
|
||||||
import 'toolbar/calendar_toolbar.dart';
|
|
||||||
|
|
||||||
class CalendarPage extends StatelessWidget {
|
|
||||||
const CalendarPage({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return const CalendarContent();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class CalendarContent extends StatefulWidget {
|
|
||||||
const CalendarContent({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<CalendarContent> createState() => _CalendarContentState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _CalendarContentState extends State<CalendarContent> {
|
|
||||||
late EventController _eventController;
|
|
||||||
GlobalKey<MonthViewState>? _calendarState;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
_eventController = EventController();
|
|
||||||
_calendarState = GlobalKey<MonthViewState>();
|
|
||||||
super.initState();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return CalendarControllerProvider(
|
|
||||||
controller: _eventController,
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
// const _ToolbarBlocAdaptor(),
|
|
||||||
_toolbar(),
|
|
||||||
_buildCalendar(_eventController),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _toolbar() {
|
|
||||||
return const CalendarToolbar();
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildCalendar(EventController eventController) {
|
|
||||||
return Expanded(
|
|
||||||
child: MonthView(
|
|
||||||
key: _calendarState,
|
|
||||||
controller: _eventController,
|
|
||||||
cellAspectRatio: 1.75,
|
|
||||||
borderColor: Theme.of(context).dividerColor,
|
|
||||||
headerBuilder: _headerNavigatorBuilder,
|
|
||||||
weekDayBuilder: _headerWeekDayBuilder,
|
|
||||||
cellBuilder: _calendarDayBuilder,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _headerNavigatorBuilder(DateTime currentMonth) {
|
|
||||||
return Row(
|
|
||||||
children: [
|
|
||||||
FlowyText.medium(
|
|
||||||
DateFormat('MMMM y', context.locale.toLanguageTag())
|
|
||||||
.format(currentMonth),
|
|
||||||
),
|
|
||||||
const Spacer(),
|
|
||||||
FlowyIconButton(
|
|
||||||
width: CalendarSize.navigatorButtonWidth,
|
|
||||||
height: CalendarSize.navigatorButtonHeight,
|
|
||||||
icon: svgWidget('home/arrow_left'),
|
|
||||||
tooltipText: LocaleKeys.calendar_navigation_previousMonth.tr(),
|
|
||||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
|
||||||
onPressed: () => _calendarState?.currentState?.previousPage(),
|
|
||||||
),
|
|
||||||
FlowyTextButton(
|
|
||||||
LocaleKeys.calendar_navigation_today.tr(),
|
|
||||||
fillColor: Colors.transparent,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
tooltip: LocaleKeys.calendar_navigation_jumpToday.tr(),
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
|
|
||||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
|
||||||
onPressed: () =>
|
|
||||||
_calendarState?.currentState?.animateToMonth(DateTime.now()),
|
|
||||||
),
|
|
||||||
FlowyIconButton(
|
|
||||||
width: CalendarSize.navigatorButtonWidth,
|
|
||||||
height: CalendarSize.navigatorButtonHeight,
|
|
||||||
icon: svgWidget('home/arrow_right'),
|
|
||||||
tooltipText: LocaleKeys.calendar_navigation_nextMonth.tr(),
|
|
||||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
|
||||||
onPressed: () => _calendarState?.currentState?.nextPage(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _headerWeekDayBuilder(day) {
|
|
||||||
final symbols = DateFormat.EEEE(context.locale.toLanguageTag()).dateSymbols;
|
|
||||||
final weekDayString = symbols.WEEKDAYS[day];
|
|
||||||
return Center(
|
|
||||||
child: Padding(
|
|
||||||
padding: CalendarSize.daysOfWeekInsets,
|
|
||||||
child: FlowyText.medium(
|
|
||||||
weekDayString,
|
|
||||||
color: Theme.of(context).hintColor,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _calendarDayBuilder(date, event, isToday, isInMonth) {
|
|
||||||
Color dayTextColor = Theme.of(context).colorScheme.onSurface;
|
|
||||||
Color cellBackgroundColor = Theme.of(context).colorScheme.surface;
|
|
||||||
String dayString = date.day == 1
|
|
||||||
? DateFormat('MMM d', context.locale.toLanguageTag()).format(date)
|
|
||||||
: date.day.toString();
|
|
||||||
|
|
||||||
if (isToday) {
|
|
||||||
dayTextColor = Theme.of(context).colorScheme.onPrimary;
|
|
||||||
}
|
|
||||||
if (!isInMonth) {
|
|
||||||
dayTextColor = Theme.of(context).disabledColor;
|
|
||||||
cellBackgroundColor = AFThemeExtension.of(context).lightGreyHover;
|
|
||||||
}
|
|
||||||
Widget day = Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: isToday ? Theme.of(context).colorScheme.primary : null,
|
|
||||||
borderRadius: Corners.s6Border,
|
|
||||||
),
|
|
||||||
padding: GridSize.typeOptionContentInsets,
|
|
||||||
child: FlowyText.medium(
|
|
||||||
dayString,
|
|
||||||
color: dayTextColor,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
return Container(
|
|
||||||
color: cellBackgroundColor,
|
|
||||||
child: Align(
|
|
||||||
alignment: Alignment.topRight,
|
|
||||||
child: day.padding(all: 6.0),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,216 +0,0 @@
|
|||||||
import 'dart:convert';
|
|
||||||
import 'package:app_flowy/plugins/trash/application/trash_service.dart';
|
|
||||||
import 'package:app_flowy/user/application/user_service.dart';
|
|
||||||
import 'package:app_flowy/workspace/application/view/view_listener.dart';
|
|
||||||
import 'package:app_flowy/plugins/document/application/doc_service.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pbserver.dart';
|
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart'
|
|
||||||
show EditorState, Document, Transaction;
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-folder/trash.pb.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
|
||||||
import 'package:appflowy_backend/log.dart';
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
|
||||||
import 'package:dartz/dartz.dart';
|
|
||||||
import 'dart:async';
|
|
||||||
import 'package:app_flowy/util/either_extension.dart';
|
|
||||||
|
|
||||||
part 'doc_bloc.freezed.dart';
|
|
||||||
|
|
||||||
class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
|
|
||||||
final ViewPB view;
|
|
||||||
final DocumentService _documentService;
|
|
||||||
|
|
||||||
final ViewListener _listener;
|
|
||||||
final TrashService _trashService;
|
|
||||||
EditorState? editorState;
|
|
||||||
StreamSubscription? _subscription;
|
|
||||||
|
|
||||||
DocumentBloc({
|
|
||||||
required this.view,
|
|
||||||
}) : _documentService = DocumentService(),
|
|
||||||
_listener = ViewListener(view: view),
|
|
||||||
_trashService = TrashService(),
|
|
||||||
super(DocumentState.initial()) {
|
|
||||||
on<DocumentEvent>((event, emit) async {
|
|
||||||
await event.map(
|
|
||||||
initial: (Initial value) async {
|
|
||||||
await _initial(value, emit);
|
|
||||||
_listenOnViewChange();
|
|
||||||
},
|
|
||||||
deleted: (Deleted value) async {
|
|
||||||
emit(state.copyWith(isDeleted: true));
|
|
||||||
},
|
|
||||||
restore: (Restore value) async {
|
|
||||||
emit(state.copyWith(isDeleted: false));
|
|
||||||
},
|
|
||||||
deletePermanently: (DeletePermanently value) async {
|
|
||||||
final result = await _trashService
|
|
||||||
.deleteViews([Tuple2(view.id, TrashType.TrashView)]);
|
|
||||||
|
|
||||||
final newState = result.fold(
|
|
||||||
(l) => state.copyWith(forceClose: true), (r) => state);
|
|
||||||
emit(newState);
|
|
||||||
},
|
|
||||||
restorePage: (RestorePage value) async {
|
|
||||||
final result = await _trashService.putback(view.id);
|
|
||||||
final newState = result.fold(
|
|
||||||
(l) => state.copyWith(isDeleted: false), (r) => state);
|
|
||||||
emit(newState);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> close() async {
|
|
||||||
await _listener.stop();
|
|
||||||
|
|
||||||
if (_subscription != null) {
|
|
||||||
await _subscription?.cancel();
|
|
||||||
}
|
|
||||||
|
|
||||||
await _documentService.closeDocument(docId: view.id);
|
|
||||||
return super.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _initial(Initial value, Emitter<DocumentState> emit) async {
|
|
||||||
final userProfile = await UserService.getCurrentUserProfile();
|
|
||||||
if (userProfile.isRight()) {
|
|
||||||
emit(
|
|
||||||
state.copyWith(
|
|
||||||
loadingState:
|
|
||||||
DocumentLoadingState.finish(right(userProfile.asRight())),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final result = await _documentService.openDocument(view: view);
|
|
||||||
result.fold(
|
|
||||||
(documentData) {
|
|
||||||
final document = Document.fromJson(jsonDecode(documentData.content));
|
|
||||||
editorState = EditorState(document: document);
|
|
||||||
_listenOnDocumentChange();
|
|
||||||
emit(
|
|
||||||
state.copyWith(
|
|
||||||
loadingState: DocumentLoadingState.finish(left(unit)),
|
|
||||||
userProfilePB: userProfile.asLeft(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
(err) {
|
|
||||||
emit(
|
|
||||||
state.copyWith(
|
|
||||||
loadingState: DocumentLoadingState.finish(right(err)),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _listenOnViewChange() {
|
|
||||||
_listener.start(
|
|
||||||
onViewDeleted: (result) {
|
|
||||||
result.fold(
|
|
||||||
(view) => add(const DocumentEvent.deleted()),
|
|
||||||
(error) {},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
onViewRestored: (result) {
|
|
||||||
result.fold(
|
|
||||||
(view) => add(const DocumentEvent.restore()),
|
|
||||||
(error) {},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _listenOnDocumentChange() {
|
|
||||||
_subscription = editorState?.transactionStream.listen((transaction) {
|
|
||||||
final json = jsonEncode(TransactionAdaptor(transaction).toJson());
|
|
||||||
_documentService
|
|
||||||
.applyEdit(docId: view.id, operations: json)
|
|
||||||
.then((result) {
|
|
||||||
result.fold(
|
|
||||||
(l) => null,
|
|
||||||
(err) => Log.error(err),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@freezed
|
|
||||||
class DocumentEvent with _$DocumentEvent {
|
|
||||||
const factory DocumentEvent.initial() = Initial;
|
|
||||||
const factory DocumentEvent.deleted() = Deleted;
|
|
||||||
const factory DocumentEvent.restore() = Restore;
|
|
||||||
const factory DocumentEvent.restorePage() = RestorePage;
|
|
||||||
const factory DocumentEvent.deletePermanently() = DeletePermanently;
|
|
||||||
}
|
|
||||||
|
|
||||||
@freezed
|
|
||||||
class DocumentState with _$DocumentState {
|
|
||||||
const factory DocumentState({
|
|
||||||
required DocumentLoadingState loadingState,
|
|
||||||
required bool isDeleted,
|
|
||||||
required bool forceClose,
|
|
||||||
UserProfilePB? userProfilePB,
|
|
||||||
}) = _DocumentState;
|
|
||||||
|
|
||||||
factory DocumentState.initial() => const DocumentState(
|
|
||||||
loadingState: _Loading(),
|
|
||||||
isDeleted: false,
|
|
||||||
forceClose: false,
|
|
||||||
userProfilePB: null,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@freezed
|
|
||||||
class DocumentLoadingState with _$DocumentLoadingState {
|
|
||||||
const factory DocumentLoadingState.loading() = _Loading;
|
|
||||||
const factory DocumentLoadingState.finish(
|
|
||||||
Either<Unit, FlowyError> successOrFail) = _Finish;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Uses to erase the different between appflowy editor and the backend
|
|
||||||
class TransactionAdaptor {
|
|
||||||
final Transaction transaction;
|
|
||||||
TransactionAdaptor(this.transaction);
|
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
|
||||||
final json = <String, dynamic>{};
|
|
||||||
if (transaction.operations.isNotEmpty) {
|
|
||||||
// The backend uses [0,0] as the beginning path, but the editor uses [0].
|
|
||||||
// So it needs to extend the path by inserting `0` at the head for all
|
|
||||||
// operations before passing to the backend.
|
|
||||||
json['operations'] = transaction.operations
|
|
||||||
.map((e) => e.copyWith(path: [0, ...e.path]).toJson())
|
|
||||||
.toList();
|
|
||||||
}
|
|
||||||
if (transaction.afterSelection != null) {
|
|
||||||
final selection = transaction.afterSelection!;
|
|
||||||
final start = selection.start;
|
|
||||||
final end = selection.end;
|
|
||||||
json['after_selection'] = selection
|
|
||||||
.copyWith(
|
|
||||||
start: start.copyWith(path: [0, ...start.path]),
|
|
||||||
end: end.copyWith(path: [0, ...end.path]),
|
|
||||||
)
|
|
||||||
.toJson();
|
|
||||||
}
|
|
||||||
if (transaction.beforeSelection != null) {
|
|
||||||
final selection = transaction.beforeSelection!;
|
|
||||||
final start = selection.start;
|
|
||||||
final end = selection.end;
|
|
||||||
json['before_selection'] = selection
|
|
||||||
.copyWith(
|
|
||||||
start: start.copyWith(path: [0, ...start.path]),
|
|
||||||
end: end.copyWith(path: [0, ...end.path]),
|
|
||||||
)
|
|
||||||
.toJson();
|
|
||||||
}
|
|
||||||
return json;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,73 +0,0 @@
|
|||||||
import 'dart:convert';
|
|
||||||
import 'dart:io';
|
|
||||||
import 'package:app_flowy/plugins/document/application/share_service.dart';
|
|
||||||
import 'package:app_flowy/plugins/document/presentation/plugins/parsers/divider_node_parser.dart';
|
|
||||||
import 'package:app_flowy/plugins/document/presentation/plugins/parsers/math_equation_node_parser.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-document/entities.pb.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
|
||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
||||||
import 'package:dartz/dartz.dart';
|
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart'
|
|
||||||
show Document, documentToMarkdown;
|
|
||||||
part 'share_bloc.freezed.dart';
|
|
||||||
|
|
||||||
class DocShareBloc extends Bloc<DocShareEvent, DocShareState> {
|
|
||||||
ShareService service;
|
|
||||||
ViewPB view;
|
|
||||||
DocShareBloc({required this.view, required this.service})
|
|
||||||
: super(const DocShareState.initial()) {
|
|
||||||
on<DocShareEvent>((event, emit) async {
|
|
||||||
await event.map(
|
|
||||||
shareMarkdown: (ShareMarkdown shareMarkdown) async {
|
|
||||||
await service.exportMarkdown(view).then((result) {
|
|
||||||
result.fold(
|
|
||||||
(value) => emit(
|
|
||||||
DocShareState.finish(
|
|
||||||
left(_saveMarkdown(value, shareMarkdown.path)),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(error) => emit(DocShareState.finish(right(error))),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
emit(const DocShareState.loading());
|
|
||||||
},
|
|
||||||
shareLink: (ShareLink value) {},
|
|
||||||
shareText: (ShareText value) {},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
ExportDataPB _saveMarkdown(ExportDataPB value, String path) {
|
|
||||||
final markdown = _convertDocumentToMarkdown(value);
|
|
||||||
value.data = markdown;
|
|
||||||
File(path).writeAsStringSync(markdown);
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
String _convertDocumentToMarkdown(ExportDataPB value) {
|
|
||||||
final json = jsonDecode(value.data);
|
|
||||||
final document = Document.fromJson(json);
|
|
||||||
return documentToMarkdown(document, customParsers: [
|
|
||||||
const DividerNodeParser(),
|
|
||||||
const MathEquationNodeParser(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@freezed
|
|
||||||
class DocShareEvent with _$DocShareEvent {
|
|
||||||
const factory DocShareEvent.shareMarkdown(String path) = ShareMarkdown;
|
|
||||||
const factory DocShareEvent.shareText() = ShareText;
|
|
||||||
const factory DocShareEvent.shareLink() = ShareLink;
|
|
||||||
}
|
|
||||||
|
|
||||||
@freezed
|
|
||||||
class DocShareState with _$DocShareState {
|
|
||||||
const factory DocShareState.initial() = _Initial;
|
|
||||||
const factory DocShareState.loading() = _Loading;
|
|
||||||
const factory DocShareState.finish(
|
|
||||||
Either<ExportDataPB, FlowyError> successOrFail) = _Finish;
|
|
||||||
}
|
|
@ -1,133 +0,0 @@
|
|||||||
library document_plugin;
|
|
||||||
|
|
||||||
import 'package:app_flowy/generated/locale_keys.g.dart';
|
|
||||||
import 'package:app_flowy/plugins/document/document_page.dart';
|
|
||||||
import 'package:app_flowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart';
|
|
||||||
import 'package:app_flowy/plugins/document/presentation/more/more_button.dart';
|
|
||||||
import 'package:app_flowy/plugins/document/presentation/share/share_button.dart';
|
|
||||||
import 'package:app_flowy/plugins/util.dart';
|
|
||||||
import 'package:app_flowy/startup/plugin/plugin.dart';
|
|
||||||
import 'package:app_flowy/workspace/presentation/home/home_stack.dart';
|
|
||||||
import 'package:app_flowy/workspace/presentation/widgets/left_bar_item.dart';
|
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
||||||
|
|
||||||
class DocumentPluginBuilder extends PluginBuilder {
|
|
||||||
@override
|
|
||||||
Plugin build(dynamic data) {
|
|
||||||
if (data is ViewPB) {
|
|
||||||
return DocumentPlugin(pluginType: pluginType, view: data);
|
|
||||||
} else {
|
|
||||||
throw FlowyPluginException.invalidData;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get menuName => LocaleKeys.document_menuName.tr();
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get menuIcon => "editor/documents";
|
|
||||||
|
|
||||||
@override
|
|
||||||
PluginType get pluginType => PluginType.editor;
|
|
||||||
|
|
||||||
@override
|
|
||||||
ViewDataFormatPB get dataFormatType => ViewDataFormatPB.NodeFormat;
|
|
||||||
}
|
|
||||||
|
|
||||||
class DocumentPlugin extends Plugin<int> {
|
|
||||||
late PluginType _pluginType;
|
|
||||||
final DocumentAppearanceCubit _documentAppearanceCubit =
|
|
||||||
DocumentAppearanceCubit();
|
|
||||||
|
|
||||||
@override
|
|
||||||
final ViewPluginNotifier notifier;
|
|
||||||
|
|
||||||
DocumentPlugin({
|
|
||||||
required PluginType pluginType,
|
|
||||||
required ViewPB view,
|
|
||||||
Key? key,
|
|
||||||
}) : notifier = ViewPluginNotifier(view: view) {
|
|
||||||
_pluginType = pluginType;
|
|
||||||
_documentAppearanceCubit.fetch();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_documentAppearanceCubit.close();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
PluginDisplay get display {
|
|
||||||
return DocumentPluginDisplay(
|
|
||||||
notifier: notifier,
|
|
||||||
documentAppearanceCubit: _documentAppearanceCubit,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
PluginType get ty => _pluginType;
|
|
||||||
|
|
||||||
@override
|
|
||||||
PluginId get id => notifier.view.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
class DocumentPluginDisplay extends PluginDisplay with NavigationItem {
|
|
||||||
final ViewPluginNotifier notifier;
|
|
||||||
ViewPB get view => notifier.view;
|
|
||||||
int? deletedViewIndex;
|
|
||||||
DocumentAppearanceCubit documentAppearanceCubit;
|
|
||||||
|
|
||||||
DocumentPluginDisplay({
|
|
||||||
required this.notifier,
|
|
||||||
required this.documentAppearanceCubit,
|
|
||||||
Key? key,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget buildWidget(PluginContext context) {
|
|
||||||
notifier.isDeleted.addListener(() {
|
|
||||||
notifier.isDeleted.value.fold(() => null, (deletedView) {
|
|
||||||
if (deletedView.hasIndex()) {
|
|
||||||
deletedViewIndex = deletedView.index;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return BlocProvider.value(
|
|
||||||
value: documentAppearanceCubit,
|
|
||||||
child: BlocBuilder<DocumentAppearanceCubit, DocumentAppearance>(
|
|
||||||
builder: (_, state) {
|
|
||||||
return DocumentPage(
|
|
||||||
view: view,
|
|
||||||
onDeleted: () => context.onDeleted(view, deletedViewIndex),
|
|
||||||
key: ValueKey(view.id),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget get leftBarItem => ViewLeftBarItem(view: view);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget? get rightBarItem {
|
|
||||||
return Row(
|
|
||||||
children: [
|
|
||||||
DocumentShareButton(view: view),
|
|
||||||
const SizedBox(width: 10),
|
|
||||||
BlocProvider.value(
|
|
||||||
value: documentAppearanceCubit,
|
|
||||||
child: const DocumentMoreButton(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<NavigationItem> get navigationItems => [this];
|
|
||||||
}
|
|
@ -1,222 +0,0 @@
|
|||||||
import 'package:app_flowy/plugins/document/presentation/plugins/board/board_menu_item.dart';
|
|
||||||
import 'package:app_flowy/plugins/document/presentation/plugins/board/board_node_widget.dart';
|
|
||||||
import 'package:app_flowy/plugins/document/presentation/plugins/grid/grid_menu_item.dart';
|
|
||||||
import 'package:app_flowy/plugins/document/presentation/plugins/grid/grid_node_widget.dart';
|
|
||||||
import 'package:app_flowy/plugins/document/presentation/plugins/openai/widgets/auto_completion_node_widget.dart';
|
|
||||||
import 'package:app_flowy/plugins/document/presentation/plugins/openai/widgets/auto_completion_plugins.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
|
||||||
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
|
|
||||||
import 'package:flowy_infra_ui/widget/error_page.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
||||||
import 'package:intl/intl.dart';
|
|
||||||
|
|
||||||
import '../../startup/startup.dart';
|
|
||||||
import 'application/doc_bloc.dart';
|
|
||||||
import 'editor_styles.dart';
|
|
||||||
import 'presentation/banner.dart';
|
|
||||||
|
|
||||||
class DocumentPage extends StatefulWidget {
|
|
||||||
final VoidCallback onDeleted;
|
|
||||||
final ViewPB view;
|
|
||||||
|
|
||||||
DocumentPage({
|
|
||||||
required this.view,
|
|
||||||
required this.onDeleted,
|
|
||||||
Key? key,
|
|
||||||
}) : super(key: ValueKey(view.id));
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<DocumentPage> createState() => _DocumentPageState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _DocumentPageState extends State<DocumentPage> {
|
|
||||||
late DocumentBloc documentBloc;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
// The appflowy editor use Intl as localization, set the default language as fallback.
|
|
||||||
Intl.defaultLocale = 'en_US';
|
|
||||||
documentBloc = getIt<DocumentBloc>(param1: super.widget.view)
|
|
||||||
..add(const DocumentEvent.initial());
|
|
||||||
super.initState();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
documentBloc.close();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return MultiBlocProvider(
|
|
||||||
providers: [
|
|
||||||
BlocProvider<DocumentBloc>.value(value: documentBloc),
|
|
||||||
],
|
|
||||||
child:
|
|
||||||
BlocBuilder<DocumentBloc, DocumentState>(builder: (context, state) {
|
|
||||||
return state.loadingState.map(
|
|
||||||
loading: (_) => SizedBox.expand(
|
|
||||||
child: Container(color: Colors.transparent),
|
|
||||||
),
|
|
||||||
finish: (result) => result.successOrFail.fold(
|
|
||||||
(_) {
|
|
||||||
if (state.forceClose) {
|
|
||||||
widget.onDeleted();
|
|
||||||
return const SizedBox();
|
|
||||||
} else if (documentBloc.editorState == null) {
|
|
||||||
return const SizedBox();
|
|
||||||
} else {
|
|
||||||
return _renderDocument(context, state);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
(err) => FlowyErrorPage(err.toString()),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _renderDocument(BuildContext context, DocumentState state) {
|
|
||||||
return Column(
|
|
||||||
children: [
|
|
||||||
if (state.isDeleted) _renderBanner(context),
|
|
||||||
// AppFlowy Editor
|
|
||||||
const _AppFlowyEditorPage(),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _renderBanner(BuildContext context) {
|
|
||||||
return DocumentBanner(
|
|
||||||
onRestore: () =>
|
|
||||||
context.read<DocumentBloc>().add(const DocumentEvent.restorePage()),
|
|
||||||
onDelete: () => context
|
|
||||||
.read<DocumentBloc>()
|
|
||||||
.add(const DocumentEvent.deletePermanently()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _AppFlowyEditorPage extends StatefulWidget {
|
|
||||||
const _AppFlowyEditorPage({
|
|
||||||
Key? key,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<_AppFlowyEditorPage> createState() => _AppFlowyEditorPageState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _AppFlowyEditorPageState extends State<_AppFlowyEditorPage> {
|
|
||||||
late DocumentBloc documentBloc;
|
|
||||||
late EditorState editorState;
|
|
||||||
String? get openAIKey => documentBloc.state.userProfilePB?.openaiKey;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
documentBloc = context.read<DocumentBloc>();
|
|
||||||
editorState = documentBloc.editorState ?? EditorState.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final theme = Theme.of(context);
|
|
||||||
final editor = AppFlowyEditor(
|
|
||||||
editorState: editorState,
|
|
||||||
autoFocus: editorState.document.isEmpty,
|
|
||||||
customBuilders: {
|
|
||||||
// Divider
|
|
||||||
kDividerType: DividerWidgetBuilder(),
|
|
||||||
// Math Equation
|
|
||||||
kMathEquationType: MathEquationNodeWidgetBuidler(),
|
|
||||||
// Code Block
|
|
||||||
kCodeBlockType: CodeBlockNodeWidgetBuilder(),
|
|
||||||
// Board
|
|
||||||
kBoardType: BoardNodeWidgetBuilder(),
|
|
||||||
// Grid
|
|
||||||
kGridType: GridNodeWidgetBuilder(),
|
|
||||||
// Card
|
|
||||||
kCalloutType: CalloutNodeWidgetBuilder(),
|
|
||||||
// Auto Generator,
|
|
||||||
kAutoCompletionInputType: AutoCompletionInputBuilder(),
|
|
||||||
},
|
|
||||||
shortcutEvents: [
|
|
||||||
// Divider
|
|
||||||
insertDividerEvent,
|
|
||||||
// Code Block
|
|
||||||
enterInCodeBlock,
|
|
||||||
ignoreKeysInCodeBlock,
|
|
||||||
pasteInCodeBlock,
|
|
||||||
],
|
|
||||||
selectionMenuItems: [
|
|
||||||
// Divider
|
|
||||||
dividerMenuItem,
|
|
||||||
// Math Equation
|
|
||||||
mathEquationMenuItem,
|
|
||||||
// Code Block
|
|
||||||
codeBlockMenuItem,
|
|
||||||
// Emoji
|
|
||||||
emojiMenuItem,
|
|
||||||
// Board
|
|
||||||
boardMenuItem,
|
|
||||||
// Grid
|
|
||||||
gridMenuItem,
|
|
||||||
// Callout
|
|
||||||
calloutMenuItem,
|
|
||||||
// AI
|
|
||||||
// enable open ai features if needed.
|
|
||||||
if (openAIKey != null && openAIKey!.isNotEmpty) ...[
|
|
||||||
autoGeneratorMenuItem,
|
|
||||||
]
|
|
||||||
],
|
|
||||||
themeData: theme.copyWith(extensions: [
|
|
||||||
...theme.extensions.values,
|
|
||||||
customEditorTheme(context),
|
|
||||||
...customPluginTheme(context),
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
return Expanded(
|
|
||||||
child: Center(
|
|
||||||
child: Container(
|
|
||||||
constraints: const BoxConstraints(
|
|
||||||
maxWidth: double.infinity,
|
|
||||||
),
|
|
||||||
child: editor,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_clearTemporaryNodes();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _clearTemporaryNodes() async {
|
|
||||||
final document = editorState.document;
|
|
||||||
if (document.root.children.isEmpty) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final temporaryNodeTypes = [
|
|
||||||
kAutoCompletionInputType,
|
|
||||||
];
|
|
||||||
final iterator = NodeIterator(
|
|
||||||
document: document,
|
|
||||||
startNode: document.root.children.first,
|
|
||||||
);
|
|
||||||
final transaction = editorState.transaction;
|
|
||||||
while (iterator.moveNext()) {
|
|
||||||
final node = iterator.current;
|
|
||||||
if (temporaryNodeTypes.contains(node.type)) {
|
|
||||||
transaction.deleteNode(node);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (transaction.operations.isNotEmpty) {
|
|
||||||
await editorState.apply(transaction, withUpdateCursor: false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,92 +0,0 @@
|
|||||||
import 'package:app_flowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart';
|
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
|
|
||||||
EditorStyle customEditorTheme(BuildContext context) {
|
|
||||||
final documentStyle = context.watch<DocumentAppearanceCubit>().state;
|
|
||||||
var editorStyle = Theme.of(context).brightness == Brightness.dark
|
|
||||||
? EditorStyle.dark
|
|
||||||
: EditorStyle.light;
|
|
||||||
editorStyle = editorStyle.copyWith(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 100, vertical: 28),
|
|
||||||
textStyle: editorStyle.textStyle?.copyWith(
|
|
||||||
fontFamily: 'poppins',
|
|
||||||
fontSize: documentStyle.fontSize,
|
|
||||||
),
|
|
||||||
placeholderTextStyle: editorStyle.placeholderTextStyle?.copyWith(
|
|
||||||
fontFamily: 'poppins',
|
|
||||||
fontSize: documentStyle.fontSize,
|
|
||||||
),
|
|
||||||
bold: editorStyle.bold?.copyWith(
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
fontFamily: 'poppins-Bold',
|
|
||||||
),
|
|
||||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
|
||||||
selectionMenuItemSelectedIconColor:
|
|
||||||
Theme.of(context).textTheme.bodyMedium?.color,
|
|
||||||
selectionMenuItemSelectedTextColor:
|
|
||||||
Theme.of(context).textTheme.bodyMedium?.color,
|
|
||||||
);
|
|
||||||
return editorStyle;
|
|
||||||
}
|
|
||||||
|
|
||||||
Iterable<ThemeExtension<dynamic>> customPluginTheme(BuildContext context) {
|
|
||||||
final documentStyle = context.watch<DocumentAppearanceCubit>().state;
|
|
||||||
final baseFontSize = documentStyle.fontSize;
|
|
||||||
const basePadding = 12.0;
|
|
||||||
var headingPluginStyle = Theme.of(context).brightness == Brightness.dark
|
|
||||||
? HeadingPluginStyle.dark
|
|
||||||
: HeadingPluginStyle.light;
|
|
||||||
headingPluginStyle = headingPluginStyle.copyWith(
|
|
||||||
textStyle: (EditorState editorState, Node node) {
|
|
||||||
final headingToFontSize = {
|
|
||||||
'h1': baseFontSize + 12,
|
|
||||||
'h2': baseFontSize + 8,
|
|
||||||
'h3': baseFontSize + 4,
|
|
||||||
'h4': baseFontSize,
|
|
||||||
'h5': baseFontSize,
|
|
||||||
'h6': baseFontSize,
|
|
||||||
};
|
|
||||||
final fontSize =
|
|
||||||
headingToFontSize[node.attributes.heading] ?? baseFontSize;
|
|
||||||
return TextStyle(fontSize: fontSize, fontWeight: FontWeight.w600);
|
|
||||||
},
|
|
||||||
padding: (EditorState editorState, Node node) {
|
|
||||||
final headingToPadding = {
|
|
||||||
'h1': basePadding + 6,
|
|
||||||
'h2': basePadding + 4,
|
|
||||||
'h3': basePadding + 2,
|
|
||||||
'h4': basePadding,
|
|
||||||
'h5': basePadding,
|
|
||||||
'h6': basePadding,
|
|
||||||
};
|
|
||||||
final padding = headingToPadding[node.attributes.heading] ?? basePadding;
|
|
||||||
return EdgeInsets.only(bottom: padding);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
var numberListPluginStyle = Theme.of(context).brightness == Brightness.dark
|
|
||||||
? NumberListPluginStyle.dark
|
|
||||||
: NumberListPluginStyle.light;
|
|
||||||
|
|
||||||
numberListPluginStyle = numberListPluginStyle.copyWith(
|
|
||||||
icon: (_, textNode) {
|
|
||||||
const iconPadding = EdgeInsets.only(left: 5.0, right: 5.0);
|
|
||||||
return Container(
|
|
||||||
padding: iconPadding,
|
|
||||||
child: Text(
|
|
||||||
'${textNode.attributes.number.toString()}.',
|
|
||||||
style: customEditorTheme(context).textStyle,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
final pluginTheme = Theme.of(context).brightness == Brightness.dark
|
|
||||||
? darkPlguinStyleExtension
|
|
||||||
: lightPlguinStyleExtension;
|
|
||||||
return pluginTheme.toList()
|
|
||||||
..removeWhere((element) =>
|
|
||||||
element is HeadingPluginStyle || element is NumberListPluginStyle)
|
|
||||||
..add(headingPluginStyle)
|
|
||||||
..add(numberListPluginStyle);
|
|
||||||
}
|
|
@ -1,68 +0,0 @@
|
|||||||
import 'package:easy_localization/easy_localization.dart';
|
|
||||||
import 'package:flowy_infra/size.dart';
|
|
||||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
|
||||||
import 'package:flowy_infra_ui/widget/buttons/base_styled_button.dart';
|
|
||||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:app_flowy/generated/locale_keys.g.dart';
|
|
||||||
|
|
||||||
class DocumentBanner extends StatelessWidget {
|
|
||||||
final void Function() onRestore;
|
|
||||||
final void Function() onDelete;
|
|
||||||
const DocumentBanner(
|
|
||||||
{required this.onRestore, required this.onDelete, Key? key})
|
|
||||||
: super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return ConstrainedBox(
|
|
||||||
constraints: const BoxConstraints(minHeight: 60),
|
|
||||||
child: Container(
|
|
||||||
width: double.infinity,
|
|
||||||
color: Theme.of(context).colorScheme.primary,
|
|
||||||
child: FittedBox(
|
|
||||||
alignment: Alignment.center,
|
|
||||||
fit: BoxFit.scaleDown,
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
FlowyText.medium(LocaleKeys.deletePagePrompt_text.tr(),
|
|
||||||
color: Colors.white),
|
|
||||||
const HSpace(20),
|
|
||||||
BaseStyledButton(
|
|
||||||
minWidth: 160,
|
|
||||||
minHeight: 40,
|
|
||||||
contentPadding: EdgeInsets.zero,
|
|
||||||
bgColor: Colors.transparent,
|
|
||||||
hoverColor: Theme.of(context).colorScheme.primary,
|
|
||||||
downColor: Theme.of(context).colorScheme.primaryContainer,
|
|
||||||
outlineColor: Colors.white,
|
|
||||||
borderRadius: Corners.s8Border,
|
|
||||||
onPressed: onRestore,
|
|
||||||
child: FlowyText.medium(
|
|
||||||
LocaleKeys.deletePagePrompt_restore.tr(),
|
|
||||||
color: Theme.of(context).colorScheme.onPrimary,
|
|
||||||
fontSize: 14,
|
|
||||||
)),
|
|
||||||
const HSpace(20),
|
|
||||||
BaseStyledButton(
|
|
||||||
minWidth: 220,
|
|
||||||
minHeight: 40,
|
|
||||||
contentPadding: EdgeInsets.zero,
|
|
||||||
bgColor: Colors.transparent,
|
|
||||||
hoverColor: Theme.of(context).colorScheme.primaryContainer,
|
|
||||||
downColor: Theme.of(context).colorScheme.primary,
|
|
||||||
outlineColor: Colors.white,
|
|
||||||
borderRadius: Corners.s8Border,
|
|
||||||
onPressed: onDelete,
|
|
||||||
child: FlowyText.medium(
|
|
||||||
LocaleKeys.deletePagePrompt_deletePermanent.tr(),
|
|
||||||
color: Theme.of(context).colorScheme.onPrimary,
|
|
||||||
fontSize: 14,
|
|
||||||
)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,69 +0,0 @@
|
|||||||
import 'package:app_flowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart';
|
|
||||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:app_flowy/generated/locale_keys.g.dart';
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
||||||
import 'package:tuple/tuple.dart';
|
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
|
||||||
|
|
||||||
class FontSizeSwitcher extends StatefulWidget {
|
|
||||||
const FontSizeSwitcher({
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<FontSizeSwitcher> createState() => _FontSizeSwitcherState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _FontSizeSwitcherState extends State<FontSizeSwitcher> {
|
|
||||||
final List<Tuple3<String, double, bool>> _fontSizes = [
|
|
||||||
Tuple3(LocaleKeys.moreAction_small.tr(), 12.0, false),
|
|
||||||
Tuple3(LocaleKeys.moreAction_medium.tr(), 14.0, true),
|
|
||||||
Tuple3(LocaleKeys.moreAction_large.tr(), 18.0, false),
|
|
||||||
];
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return BlocBuilder<DocumentAppearanceCubit, DocumentAppearance>(
|
|
||||||
builder: (context, state) {
|
|
||||||
return Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
FlowyText.semibold(
|
|
||||||
LocaleKeys.moreAction_fontSize.tr(),
|
|
||||||
fontSize: 12,
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
height: 5,
|
|
||||||
),
|
|
||||||
ToggleButtons(
|
|
||||||
isSelected:
|
|
||||||
_fontSizes.map((e) => e.item2 == state.fontSize).toList(),
|
|
||||||
onPressed: (int index) {
|
|
||||||
_updateSelectedFontSize(_fontSizes[index].item2);
|
|
||||||
},
|
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(5)),
|
|
||||||
selectedBorderColor: Theme.of(context).colorScheme.primaryContainer,
|
|
||||||
selectedColor: Theme.of(context).colorScheme.onSurface,
|
|
||||||
fillColor: Theme.of(context).colorScheme.primaryContainer,
|
|
||||||
color: Theme.of(context).hintColor,
|
|
||||||
constraints: const BoxConstraints(
|
|
||||||
minHeight: 40.0,
|
|
||||||
minWidth: 80.0,
|
|
||||||
),
|
|
||||||
children: _fontSizes
|
|
||||||
.map((e) => Text(
|
|
||||||
e.item1,
|
|
||||||
style: TextStyle(fontSize: e.item2),
|
|
||||||
))
|
|
||||||
.toList(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void _updateSelectedFontSize(double fontSize) {
|
|
||||||
context.read<DocumentAppearanceCubit>().syncFontSize(fontSize);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,35 +0,0 @@
|
|||||||
import 'package:app_flowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart';
|
|
||||||
import 'package:app_flowy/plugins/document/presentation/more/font_size_switcher.dart';
|
|
||||||
import 'package:flowy_infra/image.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
||||||
|
|
||||||
class DocumentMoreButton extends StatelessWidget {
|
|
||||||
const DocumentMoreButton({
|
|
||||||
Key? key,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return PopupMenuButton<int>(
|
|
||||||
offset: const Offset(0, 30),
|
|
||||||
itemBuilder: (context) {
|
|
||||||
return [
|
|
||||||
PopupMenuItem(
|
|
||||||
value: 1,
|
|
||||||
enabled: false,
|
|
||||||
child: BlocProvider.value(
|
|
||||||
value: context.read<DocumentAppearanceCubit>(),
|
|
||||||
child: const FontSizeSwitcher(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
];
|
|
||||||
},
|
|
||||||
child: svgWidget(
|
|
||||||
'editor/details',
|
|
||||||
size: const Size(18, 18),
|
|
||||||
color: Theme.of(context).colorScheme.onSurface,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,193 +0,0 @@
|
|||||||
import 'package:app_flowy/plugins/document/presentation/plugins/base/insert_page_command.dart';
|
|
||||||
import 'package:app_flowy/startup/startup.dart';
|
|
||||||
import 'package:app_flowy/workspace/application/app/app_service.dart';
|
|
||||||
import 'package:app_flowy/workspace/presentation/widgets/pop_up_action.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pbserver.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
|
||||||
import 'package:dartz/dartz.dart' as dartz;
|
|
||||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
|
||||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:app_flowy/generated/locale_keys.g.dart';
|
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
|
||||||
import 'package:flowy_infra/image.dart';
|
|
||||||
import 'package:app_flowy/workspace/application/view/view_ext.dart';
|
|
||||||
import 'package:app_flowy/workspace/presentation/home/home_stack.dart';
|
|
||||||
import 'package:app_flowy/workspace/presentation/home/menu/menu.dart';
|
|
||||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
|
||||||
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
|
|
||||||
|
|
||||||
class BuiltInPageWidget extends StatefulWidget {
|
|
||||||
const BuiltInPageWidget({
|
|
||||||
Key? key,
|
|
||||||
required this.node,
|
|
||||||
required this.editorState,
|
|
||||||
required this.builder,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
final Node node;
|
|
||||||
final EditorState editorState;
|
|
||||||
final Widget Function(ViewPB viewPB) builder;
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<BuiltInPageWidget> createState() => _BuiltInPageWidgetState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _BuiltInPageWidgetState extends State<BuiltInPageWidget> {
|
|
||||||
final focusNode = FocusNode();
|
|
||||||
|
|
||||||
String get gridID {
|
|
||||||
return widget.node.attributes[kViewID];
|
|
||||||
}
|
|
||||||
|
|
||||||
String get appID {
|
|
||||||
return widget.node.attributes[kAppID];
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return FutureBuilder<dartz.Either<ViewPB, FlowyError>>(
|
|
||||||
builder: (context, snapshot) {
|
|
||||||
if (snapshot.hasData) {
|
|
||||||
final board = snapshot.data?.getLeftOrNull<ViewPB>();
|
|
||||||
if (board != null) {
|
|
||||||
return _build(context, board);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return const Center(
|
|
||||||
child: CircularProgressIndicator(),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
future: AppService().getView(appID, gridID),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
focusNode.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _build(BuildContext context, ViewPB viewPB) {
|
|
||||||
return MouseRegion(
|
|
||||||
onEnter: (event) {
|
|
||||||
widget.editorState.service.scrollService?.disable();
|
|
||||||
},
|
|
||||||
onExit: (event) {
|
|
||||||
widget.editorState.service.scrollService?.enable();
|
|
||||||
},
|
|
||||||
child: SizedBox(
|
|
||||||
height: 400,
|
|
||||||
child: Stack(
|
|
||||||
children: [
|
|
||||||
_buildMenu(context, viewPB),
|
|
||||||
_buildGrid(context, viewPB),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildGrid(BuildContext context, ViewPB viewPB) {
|
|
||||||
return Focus(
|
|
||||||
focusNode: focusNode,
|
|
||||||
onFocusChange: (value) {
|
|
||||||
if (value) {
|
|
||||||
widget.editorState.service.selectionService.clearSelection();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
child: widget.builder(viewPB),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildMenu(BuildContext context, ViewPB viewPB) {
|
|
||||||
return Positioned(
|
|
||||||
top: 5,
|
|
||||||
left: 5,
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
// information
|
|
||||||
FlowyIconButton(
|
|
||||||
tooltipText: LocaleKeys.tooltip_referencePage.tr(namedArgs: {
|
|
||||||
'name': viewPB.layout.name,
|
|
||||||
}),
|
|
||||||
width: 24,
|
|
||||||
height: 24,
|
|
||||||
iconPadding: const EdgeInsets.all(3),
|
|
||||||
icon: svgWidget(
|
|
||||||
'common/information',
|
|
||||||
color: Theme.of(context).colorScheme.onSurface,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
// Name
|
|
||||||
const Space(7, 0),
|
|
||||||
FlowyText.medium(
|
|
||||||
viewPB.name,
|
|
||||||
fontSize: 16.0,
|
|
||||||
),
|
|
||||||
// setting
|
|
||||||
const Space(7, 0),
|
|
||||||
PopoverActionList<_ActionWrapper>(
|
|
||||||
direction: PopoverDirection.bottomWithCenterAligned,
|
|
||||||
actions: _ActionType.values
|
|
||||||
.map((action) => _ActionWrapper(action))
|
|
||||||
.toList(),
|
|
||||||
buildChild: (controller) {
|
|
||||||
return FlowyIconButton(
|
|
||||||
tooltipText: LocaleKeys.tooltip_openMenu.tr(),
|
|
||||||
width: 24,
|
|
||||||
height: 24,
|
|
||||||
iconPadding: const EdgeInsets.all(3),
|
|
||||||
icon: svgWidget(
|
|
||||||
'common/settings',
|
|
||||||
color: Theme.of(context).colorScheme.onSurface,
|
|
||||||
),
|
|
||||||
onPressed: () => controller.show(),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
onSelected: (action, controller) async {
|
|
||||||
switch (action.inner) {
|
|
||||||
case _ActionType.viewDatabase:
|
|
||||||
getIt<MenuSharedState>().latestOpenView = viewPB;
|
|
||||||
getIt<HomeStackManager>().setPlugin(viewPB.plugin());
|
|
||||||
break;
|
|
||||||
case _ActionType.delete:
|
|
||||||
final transaction = widget.editorState.transaction;
|
|
||||||
transaction.deleteNode(widget.node);
|
|
||||||
widget.editorState.apply(transaction);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
controller.close();
|
|
||||||
},
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enum _ActionType {
|
|
||||||
viewDatabase,
|
|
||||||
delete,
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ActionWrapper extends ActionCell {
|
|
||||||
final _ActionType inner;
|
|
||||||
|
|
||||||
_ActionWrapper(this.inner);
|
|
||||||
|
|
||||||
Widget? icon(Color iconColor) => null;
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get name {
|
|
||||||
switch (inner) {
|
|
||||||
case _ActionType.viewDatabase:
|
|
||||||
return LocaleKeys.tooltip_viewDataBase.tr();
|
|
||||||
case _ActionType.delete:
|
|
||||||
return LocaleKeys.disclosureAction_delete.tr();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,42 +0,0 @@
|
|||||||
import 'package:app_flowy/plugins/document/presentation/plugins/board/board_node_widget.dart';
|
|
||||||
import 'package:app_flowy/plugins/document/presentation/plugins/grid/grid_node_widget.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-folder/app.pb.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
|
||||||
|
|
||||||
const String kAppID = 'app_id';
|
|
||||||
const String kViewID = 'view_id';
|
|
||||||
|
|
||||||
extension InsertPage on EditorState {
|
|
||||||
void insertPage(AppPB appPB, ViewPB viewPB) {
|
|
||||||
final selection = service.selectionService.currentSelection.value;
|
|
||||||
final textNodes =
|
|
||||||
service.selectionService.currentSelectedNodes.whereType<TextNode>();
|
|
||||||
if (selection == null || textNodes.isEmpty) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final transaction = this.transaction;
|
|
||||||
transaction.insertNode(
|
|
||||||
selection.end.path,
|
|
||||||
Node(
|
|
||||||
type: _convertPageType(viewPB),
|
|
||||||
attributes: {
|
|
||||||
kAppID: appPB.id,
|
|
||||||
kViewID: viewPB.id,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
apply(transaction);
|
|
||||||
}
|
|
||||||
|
|
||||||
String _convertPageType(ViewPB viewPB) {
|
|
||||||
switch (viewPB.layout) {
|
|
||||||
case ViewLayoutTypePB.Grid:
|
|
||||||
return kGridType;
|
|
||||||
case ViewLayoutTypePB.Board:
|
|
||||||
return kBoardType;
|
|
||||||
default:
|
|
||||||
throw Exception('Unknown layout type');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,186 +0,0 @@
|
|||||||
import 'package:app_flowy/workspace/application/app/app_service.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-folder/app.pb.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
|
||||||
import 'package:dartz/dartz.dart' as dartz;
|
|
||||||
import 'package:flowy_infra/image.dart';
|
|
||||||
import 'package:flowy_infra_ui/style_widget/button.dart';
|
|
||||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'insert_page_command.dart';
|
|
||||||
import 'package:app_flowy/generated/locale_keys.g.dart';
|
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
|
||||||
|
|
||||||
EditorState? _editorState;
|
|
||||||
OverlayEntry? _linkToPageMenu;
|
|
||||||
|
|
||||||
void showLinkToPageMenu(
|
|
||||||
EditorState editorState,
|
|
||||||
SelectionMenuService menuService,
|
|
||||||
BuildContext context,
|
|
||||||
ViewLayoutTypePB pageType,
|
|
||||||
) {
|
|
||||||
final aligment = menuService.alignment;
|
|
||||||
final offset = menuService.offset;
|
|
||||||
menuService.dismiss();
|
|
||||||
|
|
||||||
_editorState = editorState;
|
|
||||||
|
|
||||||
String hintText = '';
|
|
||||||
switch (pageType) {
|
|
||||||
case ViewLayoutTypePB.Grid:
|
|
||||||
hintText = LocaleKeys.document_slashMenu_grid_selectAGridToLinkTo.tr();
|
|
||||||
break;
|
|
||||||
case ViewLayoutTypePB.Board:
|
|
||||||
hintText = LocaleKeys.document_slashMenu_board_selectABoardToLinkTo.tr();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw Exception('Unknown layout type');
|
|
||||||
}
|
|
||||||
|
|
||||||
_linkToPageMenu?.remove();
|
|
||||||
_linkToPageMenu = OverlayEntry(builder: (context) {
|
|
||||||
return Positioned(
|
|
||||||
top: aligment == Alignment.bottomLeft ? offset.dy : null,
|
|
||||||
bottom: aligment == Alignment.topLeft ? offset.dy : null,
|
|
||||||
left: offset.dx,
|
|
||||||
child: Material(
|
|
||||||
color: Colors.transparent,
|
|
||||||
child: LinkToPageMenu(
|
|
||||||
editorState: editorState,
|
|
||||||
layoutType: pageType,
|
|
||||||
hintText: hintText,
|
|
||||||
onSelected: (appPB, viewPB) {
|
|
||||||
editorState.insertPage(appPB, viewPB);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
Overlay.of(context)?.insert(_linkToPageMenu!);
|
|
||||||
|
|
||||||
editorState.service.selectionService.currentSelection
|
|
||||||
.addListener(dismissLinkToPageMenu);
|
|
||||||
}
|
|
||||||
|
|
||||||
void dismissLinkToPageMenu() {
|
|
||||||
_linkToPageMenu?.remove();
|
|
||||||
_linkToPageMenu = null;
|
|
||||||
|
|
||||||
_editorState?.service.selectionService.currentSelection
|
|
||||||
.removeListener(dismissLinkToPageMenu);
|
|
||||||
_editorState = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
class LinkToPageMenu extends StatefulWidget {
|
|
||||||
const LinkToPageMenu({
|
|
||||||
super.key,
|
|
||||||
required this.editorState,
|
|
||||||
required this.layoutType,
|
|
||||||
required this.hintText,
|
|
||||||
required this.onSelected,
|
|
||||||
});
|
|
||||||
|
|
||||||
final EditorState editorState;
|
|
||||||
final ViewLayoutTypePB layoutType;
|
|
||||||
final String hintText;
|
|
||||||
final void Function(AppPB appPB, ViewPB viewPB) onSelected;
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<LinkToPageMenu> createState() => _LinkToPageMenuState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _LinkToPageMenuState extends State<LinkToPageMenu> {
|
|
||||||
EditorStyle get style => widget.editorState.editorStyle;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Container(
|
|
||||||
color: Colors.transparent,
|
|
||||||
width: 300,
|
|
||||||
child: Container(
|
|
||||||
padding: const EdgeInsets.fromLTRB(10, 6, 10, 6),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: style.selectionMenuBackgroundColor,
|
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(
|
|
||||||
blurRadius: 5,
|
|
||||||
spreadRadius: 1,
|
|
||||||
color: Colors.black.withOpacity(0.1),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
borderRadius: BorderRadius.circular(6.0),
|
|
||||||
),
|
|
||||||
child: _buildListWidget(context),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildListWidget(BuildContext context) {
|
|
||||||
return FutureBuilder<List<dartz.Tuple2<AppPB, List<ViewPB>>>>(
|
|
||||||
builder: (context, snapshot) {
|
|
||||||
if (snapshot.hasData &&
|
|
||||||
snapshot.connectionState == ConnectionState.done) {
|
|
||||||
final apps = snapshot.data;
|
|
||||||
final children = <Widget>[
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
|
||||||
child: FlowyText.regular(
|
|
||||||
widget.hintText,
|
|
||||||
fontSize: 10,
|
|
||||||
color: Colors.grey,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
];
|
|
||||||
if (apps != null && apps.isNotEmpty) {
|
|
||||||
for (final app in apps) {
|
|
||||||
if (app.value2.isNotEmpty) {
|
|
||||||
children.add(
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
|
||||||
child: FlowyText.regular(
|
|
||||||
app.value1.name,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
for (final value in app.value2) {
|
|
||||||
children.add(
|
|
||||||
FlowyButton(
|
|
||||||
leftIcon: svgWidget(
|
|
||||||
_iconName(value),
|
|
||||||
color: Theme.of(context).colorScheme.onSurface,
|
|
||||||
),
|
|
||||||
text: FlowyText.regular(value.name),
|
|
||||||
onTap: () => widget.onSelected(app.value1, value),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
||||||
children: children,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return const Center(
|
|
||||||
child: CircularProgressIndicator(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
future: AppService().fetchViews(widget.layoutType),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
String _iconName(ViewPB viewPB) {
|
|
||||||
switch (viewPB.layout) {
|
|
||||||
case ViewLayoutTypePB.Grid:
|
|
||||||
return 'editor/grid';
|
|
||||||
case ViewLayoutTypePB.Board:
|
|
||||||
return 'editor/board';
|
|
||||||
default:
|
|
||||||
throw Exception('Unknown layout type');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,29 +0,0 @@
|
|||||||
import 'package:app_flowy/generated/locale_keys.g.dart';
|
|
||||||
import 'package:app_flowy/plugins/document/presentation/plugins/base/link_to_page_widget.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
|
||||||
import 'package:flowy_infra/image.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
SelectionMenuItem boardMenuItem = SelectionMenuItem(
|
|
||||||
name: () => LocaleKeys.document_plugins_referencedBoard.tr(),
|
|
||||||
icon: (editorState, onSelected) {
|
|
||||||
return svgWidget(
|
|
||||||
'editor/board',
|
|
||||||
size: const Size.square(18.0),
|
|
||||||
color: onSelected
|
|
||||||
? editorState.editorStyle.selectionMenuItemSelectedIconColor
|
|
||||||
: editorState.editorStyle.selectionMenuItemIconColor,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
keywords: ['board', 'kanban'],
|
|
||||||
handler: (editorState, menuService, context) {
|
|
||||||
showLinkToPageMenu(
|
|
||||||
editorState,
|
|
||||||
menuService,
|
|
||||||
context,
|
|
||||||
ViewLayoutTypePB.Board,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
@ -1,54 +0,0 @@
|
|||||||
import 'package:app_flowy/plugins/board/presentation/board_page.dart';
|
|
||||||
import 'package:app_flowy/plugins/document/presentation/plugins/base/built_in_page_widget.dart';
|
|
||||||
import 'package:app_flowy/plugins/document/presentation/plugins/base/insert_page_command.dart';
|
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
const String kBoardType = 'board';
|
|
||||||
|
|
||||||
class BoardNodeWidgetBuilder extends NodeWidgetBuilder<Node> {
|
|
||||||
@override
|
|
||||||
Widget build(NodeWidgetContext<Node> context) {
|
|
||||||
return _BoardWidget(
|
|
||||||
key: context.node.key,
|
|
||||||
node: context.node,
|
|
||||||
editorState: context.editorState,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
NodeValidator<Node> get nodeValidator => (node) {
|
|
||||||
return node.attributes[kViewID] is String &&
|
|
||||||
node.attributes[kAppID] is String;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
class _BoardWidget extends StatefulWidget {
|
|
||||||
const _BoardWidget({
|
|
||||||
Key? key,
|
|
||||||
required this.node,
|
|
||||||
required this.editorState,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
final Node node;
|
|
||||||
final EditorState editorState;
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<_BoardWidget> createState() => _BoardWidgetState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _BoardWidgetState extends State<_BoardWidget> {
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return BuiltInPageWidget(
|
|
||||||
node: widget.node,
|
|
||||||
editorState: widget.editorState,
|
|
||||||
builder: (viewPB) {
|
|
||||||
return BoardPage(
|
|
||||||
key: ValueKey(viewPB.id),
|
|
||||||
view: viewPB,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,29 +0,0 @@
|
|||||||
import 'package:app_flowy/generated/locale_keys.g.dart';
|
|
||||||
import 'package:app_flowy/plugins/document/presentation/plugins/base/link_to_page_widget.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
|
||||||
import 'package:flowy_infra/image.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
SelectionMenuItem gridMenuItem = SelectionMenuItem(
|
|
||||||
name: () => LocaleKeys.document_plugins_referencedGrid.tr(),
|
|
||||||
icon: (editorState, onSelected) {
|
|
||||||
return svgWidget(
|
|
||||||
'editor/grid',
|
|
||||||
size: const Size.square(18.0),
|
|
||||||
color: onSelected
|
|
||||||
? editorState.editorStyle.selectionMenuItemSelectedIconColor
|
|
||||||
: editorState.editorStyle.selectionMenuItemIconColor,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
keywords: ['grid'],
|
|
||||||
handler: (editorState, menuService, context) {
|
|
||||||
showLinkToPageMenu(
|
|
||||||
editorState,
|
|
||||||
menuService,
|
|
||||||
context,
|
|
||||||
ViewLayoutTypePB.Grid,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
@ -1,54 +0,0 @@
|
|||||||
import 'package:app_flowy/plugins/document/presentation/plugins/base/built_in_page_widget.dart';
|
|
||||||
import 'package:app_flowy/plugins/document/presentation/plugins/base/insert_page_command.dart';
|
|
||||||
import 'package:app_flowy/plugins/grid/presentation/grid_page.dart';
|
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
const String kGridType = 'grid';
|
|
||||||
|
|
||||||
class GridNodeWidgetBuilder extends NodeWidgetBuilder<Node> {
|
|
||||||
@override
|
|
||||||
Widget build(NodeWidgetContext<Node> context) {
|
|
||||||
return _GridWidget(
|
|
||||||
key: context.node.key,
|
|
||||||
node: context.node,
|
|
||||||
editorState: context.editorState,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
NodeValidator<Node> get nodeValidator => (node) {
|
|
||||||
return node.attributes[kAppID] is String &&
|
|
||||||
node.attributes[kViewID] is String;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
class _GridWidget extends StatefulWidget {
|
|
||||||
const _GridWidget({
|
|
||||||
Key? key,
|
|
||||||
required this.node,
|
|
||||||
required this.editorState,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
final Node node;
|
|
||||||
final EditorState editorState;
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<_GridWidget> createState() => _GridWidgetState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _GridWidgetState extends State<_GridWidget> {
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return BuiltInPageWidget(
|
|
||||||
node: widget.node,
|
|
||||||
editorState: widget.editorState,
|
|
||||||
builder: (viewPB) {
|
|
||||||
return GridPage(
|
|
||||||
key: ValueKey(viewPB.id),
|
|
||||||
view: viewPB,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,349 +0,0 @@
|
|||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
import 'package:app_flowy/plugins/document/presentation/plugins/openai/service/openai_client.dart';
|
|
||||||
import 'package:app_flowy/plugins/document/presentation/plugins/openai/widgets/loading.dart';
|
|
||||||
import 'package:app_flowy/user/application/user_service.dart';
|
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
|
||||||
import 'package:flowy_infra_ui/style_widget/button.dart';
|
|
||||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
|
||||||
import 'package:flowy_infra_ui/style_widget/text_field.dart';
|
|
||||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
import 'package:http/http.dart' as http;
|
|
||||||
import 'package:app_flowy/generated/locale_keys.g.dart';
|
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
|
||||||
import '../util/editor_extension.dart';
|
|
||||||
|
|
||||||
const String kAutoCompletionInputType = 'auto_completion_input';
|
|
||||||
const String kAutoCompletionInputString = 'auto_completion_input_string';
|
|
||||||
const String kAutoCompletionInputStartSelection =
|
|
||||||
'auto_completion_input_start_selection';
|
|
||||||
|
|
||||||
class AutoCompletionInputBuilder extends NodeWidgetBuilder<Node> {
|
|
||||||
@override
|
|
||||||
NodeValidator<Node> get nodeValidator => (node) {
|
|
||||||
return node.attributes[kAutoCompletionInputString] is String;
|
|
||||||
};
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(NodeWidgetContext<Node> context) {
|
|
||||||
return _AutoCompletionInput(
|
|
||||||
key: context.node.key,
|
|
||||||
node: context.node,
|
|
||||||
editorState: context.editorState,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _AutoCompletionInput extends StatefulWidget {
|
|
||||||
final Node node;
|
|
||||||
|
|
||||||
final EditorState editorState;
|
|
||||||
const _AutoCompletionInput({
|
|
||||||
Key? key,
|
|
||||||
required this.node,
|
|
||||||
required this.editorState,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<_AutoCompletionInput> createState() => _AutoCompletionInputState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _AutoCompletionInputState extends State<_AutoCompletionInput> {
|
|
||||||
String get text => widget.node.attributes[kAutoCompletionInputString];
|
|
||||||
|
|
||||||
final controller = TextEditingController();
|
|
||||||
final focusNode = FocusNode();
|
|
||||||
final textFieldFocusNode = FocusNode();
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
|
|
||||||
focusNode.addListener(() {
|
|
||||||
if (focusNode.hasFocus) {
|
|
||||||
widget.editorState.service.selectionService.clearSelection();
|
|
||||||
} else {
|
|
||||||
widget.editorState.service.keyboardService?.enable();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
textFieldFocusNode.requestFocus();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
controller.dispose();
|
|
||||||
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Card(
|
|
||||||
elevation: 5,
|
|
||||||
color: Theme.of(context).colorScheme.surface,
|
|
||||||
child: Container(
|
|
||||||
margin: const EdgeInsets.all(10),
|
|
||||||
child: _buildAutoGeneratorPanel(context),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildAutoGeneratorPanel(BuildContext context) {
|
|
||||||
if (text.isEmpty) {
|
|
||||||
return Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
_buildHeaderWidget(context),
|
|
||||||
const Space(0, 10),
|
|
||||||
_buildInputWidget(context),
|
|
||||||
const Space(0, 10),
|
|
||||||
_buildInputFooterWidget(context),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
_buildHeaderWidget(context),
|
|
||||||
const Space(0, 10),
|
|
||||||
_buildFooterWidget(context),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildHeaderWidget(BuildContext context) {
|
|
||||||
return Row(
|
|
||||||
children: [
|
|
||||||
FlowyText.medium(
|
|
||||||
LocaleKeys.document_plugins_autoGeneratorTitleName.tr(),
|
|
||||||
fontSize: 14,
|
|
||||||
),
|
|
||||||
const Spacer(),
|
|
||||||
FlowyText.regular(
|
|
||||||
LocaleKeys.document_plugins_autoGeneratorLearnMore.tr(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildInputWidget(BuildContext context) {
|
|
||||||
return RawKeyboardListener(
|
|
||||||
focusNode: focusNode,
|
|
||||||
onKey: (RawKeyEvent event) async {
|
|
||||||
if (event is! RawKeyDownEvent) return;
|
|
||||||
if (event.logicalKey == LogicalKeyboardKey.enter) {
|
|
||||||
if (controller.text.isNotEmpty) {
|
|
||||||
textFieldFocusNode.unfocus();
|
|
||||||
await _onGenerate();
|
|
||||||
}
|
|
||||||
} else if (event.logicalKey == LogicalKeyboardKey.escape) {
|
|
||||||
await _onExit();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
child: FlowyTextField(
|
|
||||||
hintText: LocaleKeys.document_plugins_autoGeneratorHintText.tr(),
|
|
||||||
controller: controller,
|
|
||||||
maxLines: 3,
|
|
||||||
focusNode: textFieldFocusNode,
|
|
||||||
autoFocus: false,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildInputFooterWidget(BuildContext context) {
|
|
||||||
return Row(
|
|
||||||
children: [
|
|
||||||
FlowyRichTextButton(
|
|
||||||
TextSpan(
|
|
||||||
children: [
|
|
||||||
TextSpan(
|
|
||||||
text: '${LocaleKeys.button_generate.tr()} ',
|
|
||||||
style: Theme.of(context).textTheme.bodyMedium,
|
|
||||||
),
|
|
||||||
TextSpan(
|
|
||||||
text: '↵',
|
|
||||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
|
||||||
color: Colors.grey,
|
|
||||||
), // FIXME: color
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
onPressed: () async => await _onGenerate(),
|
|
||||||
),
|
|
||||||
const Space(10, 0),
|
|
||||||
FlowyRichTextButton(
|
|
||||||
TextSpan(
|
|
||||||
children: [
|
|
||||||
TextSpan(
|
|
||||||
text: '${LocaleKeys.button_Cancel.tr()} ',
|
|
||||||
style: Theme.of(context).textTheme.bodyMedium,
|
|
||||||
),
|
|
||||||
TextSpan(
|
|
||||||
text: LocaleKeys.button_esc.tr(),
|
|
||||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
|
||||||
color: Colors.grey,
|
|
||||||
), // FIXME: color
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
onPressed: () async => await _onExit(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildFooterWidget(BuildContext context) {
|
|
||||||
return Row(
|
|
||||||
children: [
|
|
||||||
// FIXME: l10n
|
|
||||||
FlowyRichTextButton(
|
|
||||||
TextSpan(
|
|
||||||
children: [
|
|
||||||
TextSpan(
|
|
||||||
text: '${LocaleKeys.button_keep.tr()} ',
|
|
||||||
style: Theme.of(context).textTheme.bodyMedium,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
onPressed: () => _onExit(),
|
|
||||||
),
|
|
||||||
const Space(10, 0),
|
|
||||||
FlowyRichTextButton(
|
|
||||||
TextSpan(
|
|
||||||
children: [
|
|
||||||
TextSpan(
|
|
||||||
text: '${LocaleKeys.button_discard.tr()} ',
|
|
||||||
style: Theme.of(context).textTheme.bodyMedium,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
onPressed: () => _onDiscard(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _onExit() async {
|
|
||||||
final transaction = widget.editorState.transaction;
|
|
||||||
transaction.deleteNode(widget.node);
|
|
||||||
await widget.editorState.apply(
|
|
||||||
transaction,
|
|
||||||
options: const ApplyOptions(
|
|
||||||
recordRedo: false,
|
|
||||||
recordUndo: false,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _onGenerate() async {
|
|
||||||
final loading = Loading(context);
|
|
||||||
loading.start();
|
|
||||||
await _updateEditingText();
|
|
||||||
final result = await UserService.getCurrentUserProfile();
|
|
||||||
result.fold((userProfile) async {
|
|
||||||
final openAIRepository = HttpOpenAIRepository(
|
|
||||||
client: http.Client(),
|
|
||||||
apiKey: userProfile.openaiKey,
|
|
||||||
);
|
|
||||||
final completions = await openAIRepository.getCompletions(
|
|
||||||
prompt: controller.text,
|
|
||||||
);
|
|
||||||
completions.fold((error) async {
|
|
||||||
loading.stop();
|
|
||||||
await _showError(error.message);
|
|
||||||
}, (textCompletion) async {
|
|
||||||
loading.stop();
|
|
||||||
await _makeSurePreviousNodeIsEmptyTextNode();
|
|
||||||
// Open AI result uses two '\n' as the begin syntax.
|
|
||||||
var texts = textCompletion.choices.first.text.split('\n');
|
|
||||||
if (texts.length > 2) {
|
|
||||||
texts.removeRange(0, 2);
|
|
||||||
await widget.editorState.autoInsertText(
|
|
||||||
texts.join('\n'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
focusNode.requestFocus();
|
|
||||||
});
|
|
||||||
}, (error) async {
|
|
||||||
loading.stop();
|
|
||||||
await _showError(
|
|
||||||
LocaleKeys.document_plugins_autoGeneratorCantGetOpenAIKey.tr(),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _onDiscard() async {
|
|
||||||
final selection =
|
|
||||||
widget.node.attributes[kAutoCompletionInputStartSelection];
|
|
||||||
if (selection != null) {
|
|
||||||
final start = Selection.fromJson(json.decode(selection)).start.path;
|
|
||||||
final end = widget.node.previous?.path;
|
|
||||||
if (end != null) {
|
|
||||||
final transaction = widget.editorState.transaction;
|
|
||||||
transaction.deleteNodesAtPath(
|
|
||||||
start,
|
|
||||||
end.last - start.last + 1,
|
|
||||||
);
|
|
||||||
await widget.editorState.apply(transaction);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_onExit();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _updateEditingText() async {
|
|
||||||
final transaction = widget.editorState.transaction;
|
|
||||||
transaction.updateNode(
|
|
||||||
widget.node,
|
|
||||||
{
|
|
||||||
kAutoCompletionInputString: controller.text,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
await widget.editorState.apply(transaction);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _makeSurePreviousNodeIsEmptyTextNode() async {
|
|
||||||
// make sure the previous node is a empty text node without any styles.
|
|
||||||
final transaction = widget.editorState.transaction;
|
|
||||||
final Selection selection;
|
|
||||||
if (widget.node.previous is! TextNode ||
|
|
||||||
(widget.node.previous as TextNode).toPlainText().isNotEmpty ||
|
|
||||||
(widget.node.previous as TextNode).subtype != null) {
|
|
||||||
transaction.insertNode(
|
|
||||||
widget.node.path,
|
|
||||||
TextNode.empty(),
|
|
||||||
);
|
|
||||||
selection = Selection.single(
|
|
||||||
path: widget.node.path,
|
|
||||||
startOffset: 0,
|
|
||||||
);
|
|
||||||
transaction.afterSelection = selection;
|
|
||||||
} else {
|
|
||||||
selection = Selection.single(
|
|
||||||
path: widget.node.path.previous,
|
|
||||||
startOffset: 0,
|
|
||||||
);
|
|
||||||
transaction.afterSelection = selection;
|
|
||||||
}
|
|
||||||
transaction.updateNode(widget.node, {
|
|
||||||
kAutoCompletionInputStartSelection: jsonEncode(selection.toJson()),
|
|
||||||
});
|
|
||||||
await widget.editorState.apply(transaction);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _showError(String message) async {
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
SnackBar(
|
|
||||||
action: SnackBarAction(
|
|
||||||
label: LocaleKeys.button_Cancel.tr(),
|
|
||||||
onPressed: () {
|
|
||||||
ScaffoldMessenger.of(context).hideCurrentSnackBar();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
content: FlowyText(message),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,20 +0,0 @@
|
|||||||
import 'package:app_flowy/plugins/document/presentation/plugins/openai/widgets/auto_completion_node_widget.dart';
|
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
SelectionMenuItem autoGeneratorMenuItem = SelectionMenuItem.node(
|
|
||||||
name: 'Auto Generator',
|
|
||||||
iconData: Icons.generating_tokens,
|
|
||||||
keywords: ['autogenerator', 'auto generator'],
|
|
||||||
nodeBuilder: (editorState) {
|
|
||||||
final node = Node(
|
|
||||||
type: kAutoCompletionInputType,
|
|
||||||
attributes: {
|
|
||||||
kAutoCompletionInputString: '',
|
|
||||||
},
|
|
||||||
);
|
|
||||||
return node;
|
|
||||||
},
|
|
||||||
replace: (_, textNode) => textNode.toPlainText().isEmpty,
|
|
||||||
updateSelection: null,
|
|
||||||
);
|
|
@ -1,139 +0,0 @@
|
|||||||
library document_plugin;
|
|
||||||
|
|
||||||
import 'package:app_flowy/generated/locale_keys.g.dart';
|
|
||||||
import 'package:app_flowy/startup/startup.dart';
|
|
||||||
import 'package:app_flowy/plugins/document/application/share_bloc.dart';
|
|
||||||
import 'package:app_flowy/workspace/presentation/home/toast.dart';
|
|
||||||
import 'package:app_flowy/workspace/presentation/widgets/dialogs.dart';
|
|
||||||
import 'package:app_flowy/workspace/presentation/widgets/pop_up_action.dart';
|
|
||||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
|
||||||
import 'package:clipboard/clipboard.dart';
|
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
|
||||||
import 'package:file_picker/file_picker.dart';
|
|
||||||
import 'package:flowy_infra_ui/widget/rounded_button.dart';
|
|
||||||
import 'package:appflowy_backend/log.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-document/entities.pb.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
||||||
|
|
||||||
class DocumentShareButton extends StatelessWidget {
|
|
||||||
final ViewPB view;
|
|
||||||
DocumentShareButton({Key? key, required this.view})
|
|
||||||
: super(key: ValueKey(view.hashCode));
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return BlocProvider(
|
|
||||||
create: (context) => getIt<DocShareBloc>(param1: view),
|
|
||||||
child: BlocListener<DocShareBloc, DocShareState>(
|
|
||||||
listener: (context, state) {
|
|
||||||
state.map(
|
|
||||||
initial: (_) {},
|
|
||||||
loading: (_) {},
|
|
||||||
finish: (state) {
|
|
||||||
state.successOrFail.fold(
|
|
||||||
_handleExportData,
|
|
||||||
_handleExportError,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: BlocBuilder<DocShareBloc, DocShareState>(
|
|
||||||
builder: (context, state) => ConstrainedBox(
|
|
||||||
constraints: const BoxConstraints.expand(
|
|
||||||
height: 30,
|
|
||||||
width: 100,
|
|
||||||
),
|
|
||||||
child: ShareActionList(view: view),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _handleExportData(ExportDataPB exportData) {
|
|
||||||
switch (exportData.exportType) {
|
|
||||||
case ExportType.Link:
|
|
||||||
break;
|
|
||||||
case ExportType.Markdown:
|
|
||||||
FlutterClipboard.copy(exportData.data)
|
|
||||||
.then((value) => Log.info('copied to clipboard'));
|
|
||||||
break;
|
|
||||||
case ExportType.Text:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _handleExportError(FlowyError error) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ShareActionList extends StatelessWidget {
|
|
||||||
const ShareActionList({
|
|
||||||
Key? key,
|
|
||||||
required this.view,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
final ViewPB view;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final docShareBloc = context.read<DocShareBloc>();
|
|
||||||
return PopoverActionList<ShareActionWrapper>(
|
|
||||||
direction: PopoverDirection.bottomWithCenterAligned,
|
|
||||||
actions: ShareAction.values
|
|
||||||
.map((action) => ShareActionWrapper(action))
|
|
||||||
.toList(),
|
|
||||||
buildChild: (controller) {
|
|
||||||
return RoundedTextButton(
|
|
||||||
title: LocaleKeys.shareAction_buttonText.tr(),
|
|
||||||
onPressed: () => controller.show(),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
onSelected: (action, controller) async {
|
|
||||||
switch (action.inner) {
|
|
||||||
case ShareAction.markdown:
|
|
||||||
final exportPath = await FilePicker.platform.saveFile(
|
|
||||||
dialogTitle: '',
|
|
||||||
fileName: '${view.name}.md',
|
|
||||||
);
|
|
||||||
if (exportPath != null) {
|
|
||||||
docShareBloc.add(DocShareEvent.shareMarkdown(exportPath));
|
|
||||||
showMessageToast('Exported to: $exportPath');
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case ShareAction.copyLink:
|
|
||||||
NavigatorAlertDialog(
|
|
||||||
title: LocaleKeys.shareAction_workInProgress.tr(),
|
|
||||||
).show(context);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
controller.close();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enum ShareAction {
|
|
||||||
markdown,
|
|
||||||
copyLink,
|
|
||||||
}
|
|
||||||
|
|
||||||
class ShareActionWrapper extends ActionCell {
|
|
||||||
final ShareAction inner;
|
|
||||||
|
|
||||||
ShareActionWrapper(this.inner);
|
|
||||||
|
|
||||||
Widget? icon(Color iconColor) => null;
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get name {
|
|
||||||
switch (inner) {
|
|
||||||
case ShareAction.markdown:
|
|
||||||
return LocaleKeys.shareAction_markdown.tr();
|
|
||||||
case ShareAction.copyLink:
|
|
||||||
return LocaleKeys.shareAction_copyLink.tr();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,43 +0,0 @@
|
|||||||
import 'package:app_flowy/core/grid_notification.dart';
|
|
||||||
import 'package:dartz/dartz.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-database/notification.pb.dart';
|
|
||||||
import 'package:flowy_infra/notifier.dart';
|
|
||||||
import 'dart:async';
|
|
||||||
import 'dart:typed_data';
|
|
||||||
|
|
||||||
typedef UpdateFieldNotifiedValue = Either<Unit, FlowyError>;
|
|
||||||
|
|
||||||
class CellListener {
|
|
||||||
final String rowId;
|
|
||||||
final String fieldId;
|
|
||||||
PublishNotifier<UpdateFieldNotifiedValue>? _updateCellNotifier =
|
|
||||||
PublishNotifier();
|
|
||||||
DatabaseNotificationListener? _listener;
|
|
||||||
CellListener({required this.rowId, required this.fieldId});
|
|
||||||
|
|
||||||
void start({required void Function(UpdateFieldNotifiedValue) onCellChanged}) {
|
|
||||||
_updateCellNotifier?.addPublishListener(onCellChanged);
|
|
||||||
_listener = DatabaseNotificationListener(
|
|
||||||
objectId: "$rowId:$fieldId", handler: _handler);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _handler(DatabaseNotification ty, Either<Uint8List, FlowyError> result) {
|
|
||||||
switch (ty) {
|
|
||||||
case DatabaseNotification.DidUpdateCell:
|
|
||||||
result.fold(
|
|
||||||
(payload) => _updateCellNotifier?.value = left(unit),
|
|
||||||
(error) => _updateCellNotifier?.value = right(error),
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> stop() async {
|
|
||||||
await _listener?.stop();
|
|
||||||
_updateCellNotifier?.dispose();
|
|
||||||
_updateCellNotifier = null;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,77 +0,0 @@
|
|||||||
part of 'cell_service.dart';
|
|
||||||
|
|
||||||
typedef GridCellMap = LinkedHashMap<String, GridCellIdentifier>;
|
|
||||||
|
|
||||||
class GridCell {
|
|
||||||
dynamic object;
|
|
||||||
GridCell({
|
|
||||||
required this.object,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Use to index the cell in the grid.
|
|
||||||
/// We use [fieldId + rowId] to identify the cell.
|
|
||||||
class GridCellCacheKey {
|
|
||||||
final String fieldId;
|
|
||||||
final String rowId;
|
|
||||||
GridCellCacheKey({
|
|
||||||
required this.fieldId,
|
|
||||||
required this.rowId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// GridCellCache is used to cache cell data of each block.
|
|
||||||
/// We use GridCellCacheKey to index the cell in the cache.
|
|
||||||
/// Read https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/architecture/frontend/grid
|
|
||||||
/// for more information
|
|
||||||
class GridCellCache {
|
|
||||||
final String databaseId;
|
|
||||||
|
|
||||||
/// fieldId: {cacheKey: GridCell}
|
|
||||||
final Map<String, Map<String, dynamic>> _cellDataByFieldId = {};
|
|
||||||
GridCellCache({
|
|
||||||
required this.databaseId,
|
|
||||||
});
|
|
||||||
|
|
||||||
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) {
|
|
||||||
_cellDataByFieldId[key.fieldId] = {};
|
|
||||||
map = _cellDataByFieldId[key.fieldId];
|
|
||||||
}
|
|
||||||
|
|
||||||
map![key.rowId] = value.object;
|
|
||||||
}
|
|
||||||
|
|
||||||
T? get<T>(GridCellCacheKey key) {
|
|
||||||
final map = _cellDataByFieldId[key.fieldId];
|
|
||||||
if (map == null) {
|
|
||||||
return null;
|
|
||||||
} else {
|
|
||||||
final value = map[key.rowId];
|
|
||||||
if (value is T) {
|
|
||||||
return value;
|
|
||||||
} else {
|
|
||||||
if (value != null) {
|
|
||||||
Log.error("Expected value type: $T, but receive $value");
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> dispose() async {
|
|
||||||
_cellDataByFieldId.clear();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,378 +0,0 @@
|
|||||||
part of 'cell_service.dart';
|
|
||||||
|
|
||||||
typedef GridTextCellController = GridCellController<String, String>;
|
|
||||||
typedef GridCheckboxCellController = GridCellController<String, String>;
|
|
||||||
typedef GridNumberCellController = GridCellController<String, String>;
|
|
||||||
typedef GridSelectOptionCellController
|
|
||||||
= GridCellController<SelectOptionCellDataPB, String>;
|
|
||||||
typedef GridChecklistCellController
|
|
||||||
= GridCellController<SelectOptionCellDataPB, String>;
|
|
||||||
typedef GridDateCellController
|
|
||||||
= GridCellController<DateCellDataPB, CalendarData>;
|
|
||||||
typedef GridURLCellController = GridCellController<URLCellDataPB, String>;
|
|
||||||
|
|
||||||
abstract class GridCellControllerBuilderDelegate {
|
|
||||||
GridCellFieldNotifier buildFieldNotifier();
|
|
||||||
}
|
|
||||||
|
|
||||||
class GridCellControllerBuilder {
|
|
||||||
final GridCellIdentifier _cellId;
|
|
||||||
final GridCellCache _cellCache;
|
|
||||||
final GridCellControllerBuilderDelegate delegate;
|
|
||||||
|
|
||||||
GridCellControllerBuilder({
|
|
||||||
required this.delegate,
|
|
||||||
required GridCellIdentifier cellId,
|
|
||||||
required GridCellCache cellCache,
|
|
||||||
}) : _cellCache = cellCache,
|
|
||||||
_cellId = cellId;
|
|
||||||
|
|
||||||
GridCellController build() {
|
|
||||||
final cellFieldNotifier = delegate.buildFieldNotifier();
|
|
||||||
switch (_cellId.fieldType) {
|
|
||||||
case FieldType.Checkbox:
|
|
||||||
final cellDataLoader = GridCellDataLoader(
|
|
||||||
cellId: _cellId,
|
|
||||||
parser: StringCellDataParser(),
|
|
||||||
);
|
|
||||||
return GridTextCellController(
|
|
||||||
cellId: _cellId,
|
|
||||||
cellCache: _cellCache,
|
|
||||||
cellDataLoader: cellDataLoader,
|
|
||||||
fieldNotifier: cellFieldNotifier,
|
|
||||||
cellDataPersistence: TextCellDataPersistence(cellId: _cellId),
|
|
||||||
);
|
|
||||||
case FieldType.DateTime:
|
|
||||||
final cellDataLoader = GridCellDataLoader(
|
|
||||||
cellId: _cellId,
|
|
||||||
parser: DateCellDataParser(),
|
|
||||||
reloadOnFieldChanged: true,
|
|
||||||
);
|
|
||||||
|
|
||||||
return GridDateCellController(
|
|
||||||
cellId: _cellId,
|
|
||||||
cellCache: _cellCache,
|
|
||||||
cellDataLoader: cellDataLoader,
|
|
||||||
fieldNotifier: cellFieldNotifier,
|
|
||||||
cellDataPersistence: DateCellDataPersistence(cellId: _cellId),
|
|
||||||
);
|
|
||||||
case FieldType.Number:
|
|
||||||
final cellDataLoader = GridCellDataLoader(
|
|
||||||
cellId: _cellId,
|
|
||||||
parser: StringCellDataParser(),
|
|
||||||
reloadOnFieldChanged: true,
|
|
||||||
);
|
|
||||||
return GridNumberCellController(
|
|
||||||
cellId: _cellId,
|
|
||||||
cellCache: _cellCache,
|
|
||||||
cellDataLoader: cellDataLoader,
|
|
||||||
fieldNotifier: cellFieldNotifier,
|
|
||||||
cellDataPersistence: TextCellDataPersistence(cellId: _cellId),
|
|
||||||
);
|
|
||||||
case FieldType.RichText:
|
|
||||||
final cellDataLoader = GridCellDataLoader(
|
|
||||||
cellId: _cellId,
|
|
||||||
parser: StringCellDataParser(),
|
|
||||||
);
|
|
||||||
return GridTextCellController(
|
|
||||||
cellId: _cellId,
|
|
||||||
cellCache: _cellCache,
|
|
||||||
cellDataLoader: cellDataLoader,
|
|
||||||
fieldNotifier: cellFieldNotifier,
|
|
||||||
cellDataPersistence: TextCellDataPersistence(cellId: _cellId),
|
|
||||||
);
|
|
||||||
case FieldType.MultiSelect:
|
|
||||||
case FieldType.SingleSelect:
|
|
||||||
case FieldType.Checklist:
|
|
||||||
final cellDataLoader = GridCellDataLoader(
|
|
||||||
cellId: _cellId,
|
|
||||||
parser: SelectOptionCellDataParser(),
|
|
||||||
reloadOnFieldChanged: true,
|
|
||||||
);
|
|
||||||
|
|
||||||
return GridSelectOptionCellController(
|
|
||||||
cellId: _cellId,
|
|
||||||
cellCache: _cellCache,
|
|
||||||
cellDataLoader: cellDataLoader,
|
|
||||||
fieldNotifier: cellFieldNotifier,
|
|
||||||
cellDataPersistence: TextCellDataPersistence(cellId: _cellId),
|
|
||||||
);
|
|
||||||
|
|
||||||
case FieldType.URL:
|
|
||||||
final cellDataLoader = GridCellDataLoader(
|
|
||||||
cellId: _cellId,
|
|
||||||
parser: URLCellDataParser(),
|
|
||||||
);
|
|
||||||
return GridURLCellController(
|
|
||||||
cellId: _cellId,
|
|
||||||
cellCache: _cellCache,
|
|
||||||
cellDataLoader: cellDataLoader,
|
|
||||||
fieldNotifier: cellFieldNotifier,
|
|
||||||
cellDataPersistence: TextCellDataPersistence(cellId: _cellId),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
throw UnimplementedError;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// IGridCellController is used to manipulate the cell and receive notifications.
|
|
||||||
/// * Read/Write cell data
|
|
||||||
/// * Listen on field/cell notifications.
|
|
||||||
///
|
|
||||||
/// Generic T represents the type of the cell data.
|
|
||||||
/// Generic D represents the type of data that will be saved to the disk
|
|
||||||
///
|
|
||||||
// ignore: must_be_immutable
|
|
||||||
class GridCellController<T, D> extends Equatable {
|
|
||||||
final GridCellIdentifier cellId;
|
|
||||||
final GridCellCache _cellsCache;
|
|
||||||
final GridCellCacheKey _cacheKey;
|
|
||||||
final FieldService _fieldService;
|
|
||||||
final GridCellFieldNotifier _fieldNotifier;
|
|
||||||
final GridCellDataLoader<T> _cellDataLoader;
|
|
||||||
final GridCellDataPersistence<D> _cellDataPersistence;
|
|
||||||
|
|
||||||
CellListener? _cellListener;
|
|
||||||
CellDataNotifier<T?>? _cellDataNotifier;
|
|
||||||
|
|
||||||
bool isListening = false;
|
|
||||||
VoidCallback? _onFieldChangedFn;
|
|
||||||
Timer? _loadDataOperation;
|
|
||||||
Timer? _saveDataOperation;
|
|
||||||
bool _isDispose = false;
|
|
||||||
|
|
||||||
GridCellController({
|
|
||||||
required this.cellId,
|
|
||||||
required GridCellCache cellCache,
|
|
||||||
required GridCellFieldNotifier fieldNotifier,
|
|
||||||
required GridCellDataLoader<T> cellDataLoader,
|
|
||||||
required GridCellDataPersistence<D> cellDataPersistence,
|
|
||||||
}) : _cellsCache = cellCache,
|
|
||||||
_cellDataLoader = cellDataLoader,
|
|
||||||
_cellDataPersistence = cellDataPersistence,
|
|
||||||
_fieldNotifier = fieldNotifier,
|
|
||||||
_fieldService = FieldService(
|
|
||||||
viewId: cellId.databaseId,
|
|
||||||
fieldId: cellId.fieldInfo.id,
|
|
||||||
),
|
|
||||||
_cacheKey = GridCellCacheKey(
|
|
||||||
rowId: cellId.rowId,
|
|
||||||
fieldId: cellId.fieldInfo.id,
|
|
||||||
);
|
|
||||||
|
|
||||||
String get databaseId => cellId.databaseId;
|
|
||||||
|
|
||||||
String get rowId => cellId.rowId;
|
|
||||||
|
|
||||||
String get fieldId => cellId.fieldInfo.id;
|
|
||||||
|
|
||||||
FieldInfo get fieldInfo => cellId.fieldInfo;
|
|
||||||
|
|
||||||
FieldType get fieldType => cellId.fieldInfo.fieldType;
|
|
||||||
|
|
||||||
/// Listen on the cell content or field changes
|
|
||||||
///
|
|
||||||
/// An optional [listenWhenOnCellChanged] can be implemented for more
|
|
||||||
/// granular control over when [listener] is called.
|
|
||||||
/// [listenWhenOnCellChanged] will be invoked on each [onCellChanged]
|
|
||||||
/// get called.
|
|
||||||
/// [listenWhenOnCellChanged] takes the previous `value` and current
|
|
||||||
/// `value` and must return a [bool] which determines whether or not
|
|
||||||
/// the [onCellChanged] function will be invoked.
|
|
||||||
/// [onCellChanged] is optional and if omitted, it will default to `true`.
|
|
||||||
///
|
|
||||||
VoidCallback? startListening({
|
|
||||||
required void Function(T?) onCellChanged,
|
|
||||||
bool Function(T? oldValue, T? newValue)? listenWhenOnCellChanged,
|
|
||||||
VoidCallback? onCellFieldChanged,
|
|
||||||
}) {
|
|
||||||
if (isListening) {
|
|
||||||
Log.error("Already started. It seems like you should call clone first");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
isListening = true;
|
|
||||||
|
|
||||||
_cellDataNotifier = CellDataNotifier(
|
|
||||||
value: _cellsCache.get(_cacheKey),
|
|
||||||
listenWhen: listenWhenOnCellChanged,
|
|
||||||
);
|
|
||||||
_cellListener =
|
|
||||||
CellListener(rowId: cellId.rowId, fieldId: cellId.fieldInfo.id);
|
|
||||||
|
|
||||||
/// 1.Listen on user edit event and load the new cell data if needed.
|
|
||||||
/// For example:
|
|
||||||
/// user input: 12
|
|
||||||
/// cell display: $12
|
|
||||||
_cellListener?.start(onCellChanged: (result) {
|
|
||||||
result.fold(
|
|
||||||
(_) {
|
|
||||||
_cellsCache.remove(_cacheKey);
|
|
||||||
_loadData();
|
|
||||||
},
|
|
||||||
(err) => Log.error(err),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
/// 2.Listen on the field event and load the cell data if needed.
|
|
||||||
_onFieldChangedFn = () {
|
|
||||||
if (onCellFieldChanged != null) {
|
|
||||||
onCellFieldChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// reloadOnFieldChanged should be true if you need to load the data when the corresponding field is changed
|
|
||||||
/// For example:
|
|
||||||
/// ¥12 -> $12
|
|
||||||
if (_cellDataLoader.reloadOnFieldChanged) {
|
|
||||||
_loadData();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
_fieldNotifier.register(_cacheKey, _onFieldChangedFn!);
|
|
||||||
|
|
||||||
/// Notify the listener, the cell data was changed.
|
|
||||||
onCellChangedFn() => onCellChanged(_cellDataNotifier?.value);
|
|
||||||
_cellDataNotifier?.addListener(onCellChangedFn);
|
|
||||||
|
|
||||||
// Return the function pointer that can be used when calling removeListener.
|
|
||||||
return onCellChangedFn;
|
|
||||||
}
|
|
||||||
|
|
||||||
void removeListener(VoidCallback fn) {
|
|
||||||
_cellDataNotifier?.removeListener(fn);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Return the cell data.
|
|
||||||
/// The cell data will be read from the Cache first, and load from disk if it does not exist.
|
|
||||||
/// You can set [loadIfNotExist] to false (default is true) to disable loading the cell data.
|
|
||||||
T? getCellData({bool loadIfNotExist = true}) {
|
|
||||||
final data = _cellsCache.get(_cacheKey);
|
|
||||||
if (data == null && loadIfNotExist) {
|
|
||||||
_loadData();
|
|
||||||
}
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Return the TypeOptionPB that can be parsed into corresponding class using the [parser].
|
|
||||||
/// [PD] is the type that the parser return.
|
|
||||||
Future<Either<PD, FlowyError>>
|
|
||||||
getFieldTypeOption<PD, P extends TypeOptionDataParser>(P parser) {
|
|
||||||
return _fieldService
|
|
||||||
.getFieldTypeOptionData(fieldType: fieldType)
|
|
||||||
.then((result) {
|
|
||||||
return result.fold(
|
|
||||||
(data) => left(parser.fromBuffer(data.typeOptionData)),
|
|
||||||
(err) => right(err),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Save the cell data to disk
|
|
||||||
/// You can set [deduplicate] to true (default is false) to reduce the save operation.
|
|
||||||
/// It's useful when you call this method when user editing the [TextField].
|
|
||||||
/// The default debounce interval is 300 milliseconds.
|
|
||||||
Future<void> saveCellData(
|
|
||||||
D data, {
|
|
||||||
bool deduplicate = false,
|
|
||||||
void Function(Option<FlowyError>)? onFinish,
|
|
||||||
}) async {
|
|
||||||
_loadDataOperation?.cancel();
|
|
||||||
if (deduplicate) {
|
|
||||||
_saveDataOperation?.cancel();
|
|
||||||
_saveDataOperation = Timer(const Duration(milliseconds: 300), () async {
|
|
||||||
final result = await _cellDataPersistence.save(data);
|
|
||||||
onFinish?.call(result);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
final result = await _cellDataPersistence.save(data);
|
|
||||||
onFinish?.call(result);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _loadData() {
|
|
||||||
_saveDataOperation?.cancel();
|
|
||||||
|
|
||||||
_loadDataOperation?.cancel();
|
|
||||||
_loadDataOperation = Timer(const Duration(milliseconds: 10), () {
|
|
||||||
_cellDataLoader.loadData().then((data) {
|
|
||||||
if (data != null) {
|
|
||||||
_cellsCache.insert(_cacheKey, GridCell(object: data));
|
|
||||||
} else {
|
|
||||||
_cellsCache.remove(_cacheKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
_cellDataNotifier?.value = data;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> dispose() async {
|
|
||||||
if (_isDispose) {
|
|
||||||
Log.error("$this should only dispose once");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
_isDispose = true;
|
|
||||||
await _cellListener?.stop();
|
|
||||||
_loadDataOperation?.cancel();
|
|
||||||
_saveDataOperation?.cancel();
|
|
||||||
_cellDataNotifier?.dispose();
|
|
||||||
_cellDataNotifier = null;
|
|
||||||
|
|
||||||
if (_onFieldChangedFn != null) {
|
|
||||||
_fieldNotifier.unregister(_cacheKey, _onFieldChangedFn!);
|
|
||||||
await _fieldNotifier.dispose();
|
|
||||||
_onFieldChangedFn = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<Object> get props =>
|
|
||||||
[_cellsCache.get(_cacheKey) ?? "", cellId.rowId + cellId.fieldInfo.id];
|
|
||||||
}
|
|
||||||
|
|
||||||
class GridCellFieldNotifierImpl extends IGridCellFieldNotifier {
|
|
||||||
final GridFieldController _fieldController;
|
|
||||||
OnReceiveUpdateFields? _onChangesetFn;
|
|
||||||
|
|
||||||
GridCellFieldNotifierImpl(GridFieldController cache)
|
|
||||||
: _fieldController = cache;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void onCellDispose() {
|
|
||||||
if (_onChangesetFn != null) {
|
|
||||||
_fieldController.removeListener(onChangesetListener: _onChangesetFn!);
|
|
||||||
_onChangesetFn = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void onCellFieldChanged(void Function(FieldInfo) callback) {
|
|
||||||
_onChangesetFn = (List<FieldInfo> filedInfos) {
|
|
||||||
for (final field in filedInfos) {
|
|
||||||
callback(field);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
_fieldController.addListener(
|
|
||||||
onFieldsUpdated: _onChangesetFn,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class CellDataNotifier<T> extends ChangeNotifier {
|
|
||||||
T _value;
|
|
||||||
bool Function(T? oldValue, T? newValue)? listenWhen;
|
|
||||||
CellDataNotifier({required T value, this.listenWhen}) : _value = value;
|
|
||||||
|
|
||||||
set value(T newValue) {
|
|
||||||
if (listenWhen?.call(_value, newValue) ?? false) {
|
|
||||||
_value = newValue;
|
|
||||||
notifyListeners();
|
|
||||||
} else {
|
|
||||||
if (_value != newValue) {
|
|
||||||
_value = newValue;
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
T get value => _value;
|
|
||||||
}
|
|
@ -1,83 +0,0 @@
|
|||||||
part of 'cell_service.dart';
|
|
||||||
|
|
||||||
abstract class IGridCellDataConfig {
|
|
||||||
// The cell data will reload if it receives the field's change notification.
|
|
||||||
bool get reloadOnFieldChanged;
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract class GridCellDataParser<T> {
|
|
||||||
T? parserData(List<int> data);
|
|
||||||
}
|
|
||||||
|
|
||||||
class GridCellDataLoader<T> {
|
|
||||||
final CellService service = CellService();
|
|
||||||
final GridCellIdentifier cellId;
|
|
||||||
final GridCellDataParser<T> parser;
|
|
||||||
final bool reloadOnFieldChanged;
|
|
||||||
|
|
||||||
GridCellDataLoader({
|
|
||||||
required this.cellId,
|
|
||||||
required this.parser,
|
|
||||||
this.reloadOnFieldChanged = false,
|
|
||||||
});
|
|
||||||
|
|
||||||
Future<T?> loadData() {
|
|
||||||
final fut = service.getCell(cellId: cellId);
|
|
||||||
return fut.then(
|
|
||||||
(result) => result.fold(
|
|
||||||
(CellPB 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;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class StringCellDataParser implements GridCellDataParser<String> {
|
|
||||||
@override
|
|
||||||
String? parserData(List<int> data) {
|
|
||||||
final s = utf8.decode(data);
|
|
||||||
return s;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class DateCellDataParser implements GridCellDataParser<DateCellDataPB> {
|
|
||||||
@override
|
|
||||||
DateCellDataPB? parserData(List<int> data) {
|
|
||||||
if (data.isEmpty) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return DateCellDataPB.fromBuffer(data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class SelectOptionCellDataParser
|
|
||||||
implements GridCellDataParser<SelectOptionCellDataPB> {
|
|
||||||
@override
|
|
||||||
SelectOptionCellDataPB? parserData(List<int> data) {
|
|
||||||
if (data.isEmpty) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return SelectOptionCellDataPB.fromBuffer(data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class URLCellDataParser implements GridCellDataParser<URLCellDataPB> {
|
|
||||||
@override
|
|
||||||
URLCellDataPB? parserData(List<int> data) {
|
|
||||||
if (data.isEmpty) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return URLCellDataPB.fromBuffer(data);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,68 +0,0 @@
|
|||||||
part of 'cell_service.dart';
|
|
||||||
|
|
||||||
/// Save the cell data to disk
|
|
||||||
/// You can extend this class to do custom operations. For example, the DateCellDataPersistence.
|
|
||||||
abstract class GridCellDataPersistence<D> {
|
|
||||||
Future<Option<FlowyError>> save(D data);
|
|
||||||
}
|
|
||||||
|
|
||||||
class TextCellDataPersistence implements GridCellDataPersistence<String> {
|
|
||||||
final GridCellIdentifier cellId;
|
|
||||||
|
|
||||||
TextCellDataPersistence({
|
|
||||||
required this.cellId,
|
|
||||||
});
|
|
||||||
final CellService _cellService = CellService();
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<Option<FlowyError>> save(String data) async {
|
|
||||||
final fut = _cellService.updateCell(cellId: cellId, data: data);
|
|
||||||
|
|
||||||
return fut.then((result) {
|
|
||||||
return result.fold(
|
|
||||||
(l) => none(),
|
|
||||||
(err) => Some(err),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@freezed
|
|
||||||
class CalendarData with _$CalendarData {
|
|
||||||
const factory CalendarData({required DateTime date, String? time}) =
|
|
||||||
_CalendarData;
|
|
||||||
}
|
|
||||||
|
|
||||||
class DateCellDataPersistence implements GridCellDataPersistence<CalendarData> {
|
|
||||||
final GridCellIdentifier cellId;
|
|
||||||
DateCellDataPersistence({
|
|
||||||
required this.cellId,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<Option<FlowyError>> save(CalendarData data) {
|
|
||||||
var payload = DateChangesetPB.create()..cellPath = _makeCellPath(cellId);
|
|
||||||
|
|
||||||
final date = (data.date.millisecondsSinceEpoch ~/ 1000).toString();
|
|
||||||
payload.date = date;
|
|
||||||
payload.isUtc = data.date.isUtc;
|
|
||||||
|
|
||||||
if (data.time != null) {
|
|
||||||
payload.time = data.time!;
|
|
||||||
}
|
|
||||||
|
|
||||||
return DatabaseEventUpdateDateCell(payload).send().then((result) {
|
|
||||||
return result.fold(
|
|
||||||
(l) => none(),
|
|
||||||
(err) => Some(err),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
CellIdPB _makeCellPath(GridCellIdentifier cellId) {
|
|
||||||
return CellIdPB.create()
|
|
||||||
..databaseId = cellId.databaseId
|
|
||||||
..fieldId = cellId.fieldId
|
|
||||||
..rowId = cellId.rowId;
|
|
||||||
}
|
|
@ -1,64 +0,0 @@
|
|||||||
import 'package:app_flowy/plugins/grid/application/field/field_controller.dart';
|
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
|
|
||||||
import 'cell_service.dart';
|
|
||||||
|
|
||||||
abstract class IGridCellFieldNotifier {
|
|
||||||
void onCellFieldChanged(void Function(FieldInfo) callback);
|
|
||||||
void onCellDispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// DatabasePB's cell helper wrapper that enables each cell will get notified when the corresponding field was changed.
|
|
||||||
/// You Register an onFieldChanged callback to listen to the cell changes, and unregister if you don't want to listen.
|
|
||||||
class GridCellFieldNotifier {
|
|
||||||
final IGridCellFieldNotifier notifier;
|
|
||||||
|
|
||||||
/// fieldId: {objectId: callback}
|
|
||||||
final Map<String, Map<String, List<VoidCallback>>> _fieldListenerByFieldId =
|
|
||||||
{};
|
|
||||||
|
|
||||||
GridCellFieldNotifier({required this.notifier}) {
|
|
||||||
notifier.onCellFieldChanged(
|
|
||||||
(field) {
|
|
||||||
final map = _fieldListenerByFieldId[field.id];
|
|
||||||
if (map != null) {
|
|
||||||
for (final callbacks in map.values) {
|
|
||||||
for (final callback in callbacks) {
|
|
||||||
callback();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
///
|
|
||||||
void register(GridCellCacheKey cacheKey, VoidCallback onFieldChanged) {
|
|
||||||
var map = _fieldListenerByFieldId[cacheKey.fieldId];
|
|
||||||
if (map == null) {
|
|
||||||
_fieldListenerByFieldId[cacheKey.fieldId] = {};
|
|
||||||
map = _fieldListenerByFieldId[cacheKey.fieldId];
|
|
||||||
map![cacheKey.rowId] = [onFieldChanged];
|
|
||||||
} else {
|
|
||||||
var objects = map[cacheKey.rowId];
|
|
||||||
if (objects == null) {
|
|
||||||
map[cacheKey.rowId] = [onFieldChanged];
|
|
||||||
} else {
|
|
||||||
objects.add(onFieldChanged);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void unregister(GridCellCacheKey cacheKey, VoidCallback fn) {
|
|
||||||
var callbacks = _fieldListenerByFieldId[cacheKey.fieldId]?[cacheKey.rowId];
|
|
||||||
final index = callbacks?.indexWhere((callback) => callback == fn);
|
|
||||||
if (index != null && index != -1) {
|
|
||||||
callbacks?.removeAt(index);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> dispose() async {
|
|
||||||
notifier.onCellDispose();
|
|
||||||
_fieldListenerByFieldId.clear();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,76 +0,0 @@
|
|||||||
import 'dart:async';
|
|
||||||
import 'dart:collection';
|
|
||||||
import 'package:dartz/dartz.dart';
|
|
||||||
import 'package:equatable/equatable.dart';
|
|
||||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
|
||||||
import 'package:appflowy_backend/log.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-database/cell_entities.pb.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-database/date_type_option_entities.pb.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-database/field_entities.pb.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-database/select_type_option.pb.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-database/url_type_option_entities.pb.dart';
|
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
|
||||||
import 'package:app_flowy/plugins/grid/application/cell/cell_listener.dart';
|
|
||||||
import 'package:app_flowy/plugins/grid/application/field/field_service.dart';
|
|
||||||
import 'dart:convert' show utf8;
|
|
||||||
|
|
||||||
import '../../field/field_controller.dart';
|
|
||||||
import '../../field/type_option/type_option_context.dart';
|
|
||||||
import 'cell_field_notifier.dart';
|
|
||||||
part 'cell_service.freezed.dart';
|
|
||||||
part 'cell_data_loader.dart';
|
|
||||||
part 'cell_controller.dart';
|
|
||||||
part 'cell_cache.dart';
|
|
||||||
part 'cell_data_persistence.dart';
|
|
||||||
|
|
||||||
// key: rowId
|
|
||||||
|
|
||||||
class CellService {
|
|
||||||
CellService();
|
|
||||||
|
|
||||||
Future<Either<void, FlowyError>> updateCell({
|
|
||||||
required GridCellIdentifier cellId,
|
|
||||||
required String data,
|
|
||||||
}) {
|
|
||||||
final payload = CellChangesetPB.create()
|
|
||||||
..databaseId = cellId.databaseId
|
|
||||||
..fieldId = cellId.fieldId
|
|
||||||
..rowId = cellId.rowId
|
|
||||||
..typeCellData = data;
|
|
||||||
return DatabaseEventUpdateCell(payload).send();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<Either<CellPB, FlowyError>> getCell({
|
|
||||||
required GridCellIdentifier cellId,
|
|
||||||
}) {
|
|
||||||
final payload = CellIdPB.create()
|
|
||||||
..databaseId = cellId.databaseId
|
|
||||||
..fieldId = cellId.fieldId
|
|
||||||
..rowId = cellId.rowId;
|
|
||||||
return DatabaseEventGetCell(payload).send();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Id of the cell
|
|
||||||
/// We can locate the cell by using database + rowId + field.id.
|
|
||||||
@freezed
|
|
||||||
class GridCellIdentifier with _$GridCellIdentifier {
|
|
||||||
const factory GridCellIdentifier({
|
|
||||||
required String databaseId,
|
|
||||||
required String rowId,
|
|
||||||
required FieldInfo fieldInfo,
|
|
||||||
}) = _GridCellIdentifier;
|
|
||||||
|
|
||||||
// ignore: unused_element
|
|
||||||
const GridCellIdentifier._();
|
|
||||||
|
|
||||||
String get fieldId => fieldInfo.id;
|
|
||||||
|
|
||||||
FieldType get fieldType => fieldInfo.fieldType;
|
|
||||||
|
|
||||||
ValueKey key() {
|
|
||||||
return ValueKey("$rowId$fieldId${fieldInfo.fieldType}");
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,75 +0,0 @@
|
|||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
|
||||||
import 'dart:async';
|
|
||||||
import 'cell_service/cell_service.dart';
|
|
||||||
|
|
||||||
part 'checkbox_cell_bloc.freezed.dart';
|
|
||||||
|
|
||||||
class CheckboxCellBloc extends Bloc<CheckboxCellEvent, CheckboxCellState> {
|
|
||||||
final GridCheckboxCellController cellController;
|
|
||||||
void Function()? _onCellChangedFn;
|
|
||||||
|
|
||||||
CheckboxCellBloc({
|
|
||||||
required CellService service,
|
|
||||||
required this.cellController,
|
|
||||||
}) : super(CheckboxCellState.initial(cellController)) {
|
|
||||||
on<CheckboxCellEvent>(
|
|
||||||
(event, emit) async {
|
|
||||||
await event.when(
|
|
||||||
initial: () {
|
|
||||||
_startListening();
|
|
||||||
},
|
|
||||||
select: () async {
|
|
||||||
cellController.saveCellData(!state.isSelected ? "Yes" : "No");
|
|
||||||
},
|
|
||||||
didReceiveCellUpdate: (cellData) {
|
|
||||||
emit(state.copyWith(isSelected: _isSelected(cellData)));
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> close() async {
|
|
||||||
if (_onCellChangedFn != null) {
|
|
||||||
cellController.removeListener(_onCellChangedFn!);
|
|
||||||
_onCellChangedFn = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
await cellController.dispose();
|
|
||||||
return super.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _startListening() {
|
|
||||||
_onCellChangedFn =
|
|
||||||
cellController.startListening(onCellChanged: ((cellData) {
|
|
||||||
if (!isClosed) {
|
|
||||||
add(CheckboxCellEvent.didReceiveCellUpdate(cellData));
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@freezed
|
|
||||||
class CheckboxCellEvent with _$CheckboxCellEvent {
|
|
||||||
const factory CheckboxCellEvent.initial() = _Initial;
|
|
||||||
const factory CheckboxCellEvent.select() = _Selected;
|
|
||||||
const factory CheckboxCellEvent.didReceiveCellUpdate(String? cellData) =
|
|
||||||
_DidReceiveCellUpdate;
|
|
||||||
}
|
|
||||||
|
|
||||||
@freezed
|
|
||||||
class CheckboxCellState with _$CheckboxCellState {
|
|
||||||
const factory CheckboxCellState({
|
|
||||||
required bool isSelected,
|
|
||||||
}) = _CheckboxCellState;
|
|
||||||
|
|
||||||
factory CheckboxCellState.initial(GridTextCellController context) {
|
|
||||||
return CheckboxCellState(isSelected: _isSelected(context.getCellData()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool _isSelected(String? cellData) {
|
|
||||||
return cellData == "Yes";
|
|
||||||
}
|
|
@ -1,97 +0,0 @@
|
|||||||
import 'package:appflowy_backend/log.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-database/select_type_option.pb.dart';
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
|
||||||
import 'dart:async';
|
|
||||||
import 'cell_service/cell_service.dart';
|
|
||||||
import 'checklist_cell_editor_bloc.dart';
|
|
||||||
import 'select_option_service.dart';
|
|
||||||
part 'checklist_cell_bloc.freezed.dart';
|
|
||||||
|
|
||||||
class ChecklistCellBloc extends Bloc<ChecklistCellEvent, ChecklistCellState> {
|
|
||||||
final GridChecklistCellController cellController;
|
|
||||||
final SelectOptionFFIService _selectOptionService;
|
|
||||||
void Function()? _onCellChangedFn;
|
|
||||||
ChecklistCellBloc({
|
|
||||||
required this.cellController,
|
|
||||||
}) : _selectOptionService =
|
|
||||||
SelectOptionFFIService(cellId: cellController.cellId),
|
|
||||||
super(ChecklistCellState.initial(cellController)) {
|
|
||||||
on<ChecklistCellEvent>(
|
|
||||||
(event, emit) async {
|
|
||||||
await event.when(
|
|
||||||
initial: () async {
|
|
||||||
_startListening();
|
|
||||||
_loadOptions();
|
|
||||||
},
|
|
||||||
didReceiveOptions: (data) {
|
|
||||||
emit(state.copyWith(
|
|
||||||
allOptions: data.options,
|
|
||||||
selectedOptions: data.selectOptions,
|
|
||||||
percent: percentFromSelectOptionCellData(data),
|
|
||||||
));
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> close() async {
|
|
||||||
if (_onCellChangedFn != null) {
|
|
||||||
cellController.removeListener(_onCellChangedFn!);
|
|
||||||
_onCellChangedFn = null;
|
|
||||||
}
|
|
||||||
await cellController.dispose();
|
|
||||||
return super.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _startListening() {
|
|
||||||
_onCellChangedFn = cellController.startListening(
|
|
||||||
onCellFieldChanged: () {
|
|
||||||
_loadOptions();
|
|
||||||
},
|
|
||||||
onCellChanged: (data) {
|
|
||||||
if (!isClosed && data != null) {
|
|
||||||
add(ChecklistCellEvent.didReceiveOptions(data));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _loadOptions() {
|
|
||||||
_selectOptionService.getOptionContext().then((result) {
|
|
||||||
if (isClosed) return;
|
|
||||||
|
|
||||||
return result.fold(
|
|
||||||
(data) => add(ChecklistCellEvent.didReceiveOptions(data)),
|
|
||||||
(err) => Log.error(err),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@freezed
|
|
||||||
class ChecklistCellEvent with _$ChecklistCellEvent {
|
|
||||||
const factory ChecklistCellEvent.initial() = _InitialCell;
|
|
||||||
const factory ChecklistCellEvent.didReceiveOptions(
|
|
||||||
SelectOptionCellDataPB data) = _DidReceiveCellUpdate;
|
|
||||||
}
|
|
||||||
|
|
||||||
@freezed
|
|
||||||
class ChecklistCellState with _$ChecklistCellState {
|
|
||||||
const factory ChecklistCellState({
|
|
||||||
required List<SelectOptionPB> allOptions,
|
|
||||||
required List<SelectOptionPB> selectedOptions,
|
|
||||||
required double percent,
|
|
||||||
}) = _ChecklistCellState;
|
|
||||||
|
|
||||||
factory ChecklistCellState.initial(
|
|
||||||
GridChecklistCellController cellController) {
|
|
||||||
return const ChecklistCellState(
|
|
||||||
allOptions: [],
|
|
||||||
selectedOptions: [],
|
|
||||||
percent: 0,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,194 +0,0 @@
|
|||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart';
|
|
||||||
import 'package:dartz/dartz.dart';
|
|
||||||
import 'package:appflowy_backend/log.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-database/select_type_option.pb.dart';
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
|
||||||
|
|
||||||
import 'select_option_service.dart';
|
|
||||||
|
|
||||||
part 'checklist_cell_editor_bloc.freezed.dart';
|
|
||||||
|
|
||||||
class ChecklistCellEditorBloc
|
|
||||||
extends Bloc<ChecklistCellEditorEvent, ChecklistCellEditorState> {
|
|
||||||
final SelectOptionFFIService _selectOptionService;
|
|
||||||
final GridChecklistCellController cellController;
|
|
||||||
|
|
||||||
ChecklistCellEditorBloc({
|
|
||||||
required this.cellController,
|
|
||||||
}) : _selectOptionService =
|
|
||||||
SelectOptionFFIService(cellId: cellController.cellId),
|
|
||||||
super(ChecklistCellEditorState.initial(cellController)) {
|
|
||||||
on<ChecklistCellEditorEvent>(
|
|
||||||
(event, emit) async {
|
|
||||||
await event.when(
|
|
||||||
initial: () async {
|
|
||||||
_startListening();
|
|
||||||
_loadOptions();
|
|
||||||
},
|
|
||||||
didReceiveOptions: (data) {
|
|
||||||
emit(state.copyWith(
|
|
||||||
allOptions: _makeChecklistSelectOptions(data, state.predicate),
|
|
||||||
percent: percentFromSelectOptionCellData(data),
|
|
||||||
));
|
|
||||||
},
|
|
||||||
newOption: (optionName) {
|
|
||||||
_createOption(optionName);
|
|
||||||
emit(state.copyWith(
|
|
||||||
predicate: '',
|
|
||||||
));
|
|
||||||
},
|
|
||||||
deleteOption: (option) {
|
|
||||||
_deleteOption([option]);
|
|
||||||
},
|
|
||||||
updateOption: (option) {
|
|
||||||
_updateOption(option);
|
|
||||||
},
|
|
||||||
selectOption: (option) async {
|
|
||||||
if (option.isSelected) {
|
|
||||||
await _selectOptionService.unSelect(optionIds: [option.data.id]);
|
|
||||||
} else {
|
|
||||||
await _selectOptionService.select(optionIds: [option.data.id]);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
filterOption: (String predicate) {},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> close() async {
|
|
||||||
await cellController.dispose();
|
|
||||||
return super.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _createOption(String name) async {
|
|
||||||
final result = await _selectOptionService.create(
|
|
||||||
name: name,
|
|
||||||
isSelected: false,
|
|
||||||
);
|
|
||||||
result.fold((l) => {}, (err) => Log.error(err));
|
|
||||||
}
|
|
||||||
|
|
||||||
void _deleteOption(List<SelectOptionPB> options) async {
|
|
||||||
final result = await _selectOptionService.delete(options: options);
|
|
||||||
result.fold((l) => null, (err) => Log.error(err));
|
|
||||||
}
|
|
||||||
|
|
||||||
void _updateOption(SelectOptionPB option) async {
|
|
||||||
final result = await _selectOptionService.update(
|
|
||||||
option: option,
|
|
||||||
);
|
|
||||||
|
|
||||||
result.fold((l) => null, (err) => Log.error(err));
|
|
||||||
}
|
|
||||||
|
|
||||||
void _loadOptions() {
|
|
||||||
_selectOptionService.getOptionContext().then((result) {
|
|
||||||
if (isClosed) return;
|
|
||||||
|
|
||||||
return result.fold(
|
|
||||||
(data) => add(ChecklistCellEditorEvent.didReceiveOptions(data)),
|
|
||||||
(err) => Log.error(err),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void _startListening() {
|
|
||||||
cellController.startListening(
|
|
||||||
onCellChanged: ((data) {
|
|
||||||
if (!isClosed && data != null) {
|
|
||||||
add(ChecklistCellEditorEvent.didReceiveOptions(data));
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
onCellFieldChanged: () {
|
|
||||||
_loadOptions();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@freezed
|
|
||||||
class ChecklistCellEditorEvent with _$ChecklistCellEditorEvent {
|
|
||||||
const factory ChecklistCellEditorEvent.initial() = _Initial;
|
|
||||||
const factory ChecklistCellEditorEvent.didReceiveOptions(
|
|
||||||
SelectOptionCellDataPB data) = _DidReceiveOptions;
|
|
||||||
const factory ChecklistCellEditorEvent.newOption(String optionName) =
|
|
||||||
_NewOption;
|
|
||||||
const factory ChecklistCellEditorEvent.selectOption(
|
|
||||||
ChecklistSelectOption option) = _SelectOption;
|
|
||||||
const factory ChecklistCellEditorEvent.updateOption(SelectOptionPB option) =
|
|
||||||
_UpdateOption;
|
|
||||||
const factory ChecklistCellEditorEvent.deleteOption(SelectOptionPB option) =
|
|
||||||
_DeleteOption;
|
|
||||||
const factory ChecklistCellEditorEvent.filterOption(String predicate) =
|
|
||||||
_FilterOption;
|
|
||||||
}
|
|
||||||
|
|
||||||
@freezed
|
|
||||||
class ChecklistCellEditorState with _$ChecklistCellEditorState {
|
|
||||||
const factory ChecklistCellEditorState({
|
|
||||||
required List<ChecklistSelectOption> allOptions,
|
|
||||||
required Option<String> createOption,
|
|
||||||
required double percent,
|
|
||||||
required String predicate,
|
|
||||||
}) = _ChecklistCellEditorState;
|
|
||||||
|
|
||||||
factory ChecklistCellEditorState.initial(
|
|
||||||
GridSelectOptionCellController context) {
|
|
||||||
final data = context.getCellData(loadIfNotExist: true);
|
|
||||||
|
|
||||||
return ChecklistCellEditorState(
|
|
||||||
allOptions: _makeChecklistSelectOptions(data, ''),
|
|
||||||
createOption: none(),
|
|
||||||
percent: percentFromSelectOptionCellData(data),
|
|
||||||
predicate: '',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
double percentFromSelectOptionCellData(SelectOptionCellDataPB? data) {
|
|
||||||
if (data == null) return 0;
|
|
||||||
|
|
||||||
final b = data.options.length.toDouble();
|
|
||||||
if (b == 0) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
final a = data.selectOptions.length.toDouble();
|
|
||||||
if (a > b) return 1.0;
|
|
||||||
|
|
||||||
return a / b;
|
|
||||||
}
|
|
||||||
|
|
||||||
List<ChecklistSelectOption> _makeChecklistSelectOptions(
|
|
||||||
SelectOptionCellDataPB? data, String predicate) {
|
|
||||||
if (data == null) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
final List<ChecklistSelectOption> options = [];
|
|
||||||
final List<SelectOptionPB> allOptions = List.from(data.options);
|
|
||||||
if (predicate.isNotEmpty) {
|
|
||||||
allOptions.retainWhere((element) => element.name.contains(predicate));
|
|
||||||
}
|
|
||||||
final selectedOptionIds = data.selectOptions.map((e) => e.id).toList();
|
|
||||||
|
|
||||||
for (final option in allOptions) {
|
|
||||||
options.add(
|
|
||||||
ChecklistSelectOption(selectedOptionIds.contains(option.id), option),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return options;
|
|
||||||
}
|
|
||||||
|
|
||||||
class ChecklistSelectOption {
|
|
||||||
final bool isSelected;
|
|
||||||
final SelectOptionPB data;
|
|
||||||
|
|
||||||
ChecklistSelectOption(this.isSelected, this.data);
|
|
||||||
}
|
|
@ -1,273 +0,0 @@
|
|||||||
import 'package:app_flowy/generated/locale_keys.g.dart';
|
|
||||||
import 'package:app_flowy/plugins/grid/application/field/field_service.dart';
|
|
||||||
import 'package:easy_localization/easy_localization.dart'
|
|
||||||
show StringTranslateExtension;
|
|
||||||
import 'package:appflowy_backend/log.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-error/code.pb.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-database/date_type_option.pb.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-database/date_type_option_entities.pb.dart';
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
|
||||||
import 'package:table_calendar/table_calendar.dart';
|
|
||||||
import 'dart:async';
|
|
||||||
import 'cell_service/cell_service.dart';
|
|
||||||
import 'package:dartz/dartz.dart';
|
|
||||||
import 'package:protobuf/protobuf.dart';
|
|
||||||
import 'package:fixnum/fixnum.dart' as $fixnum;
|
|
||||||
part 'date_cal_bloc.freezed.dart';
|
|
||||||
|
|
||||||
class DateCalBloc extends Bloc<DateCalEvent, DateCalState> {
|
|
||||||
final GridDateCellController cellController;
|
|
||||||
void Function()? _onCellChangedFn;
|
|
||||||
|
|
||||||
DateCalBloc({
|
|
||||||
required DateTypeOptionPB dateTypeOptionPB,
|
|
||||||
required DateCellDataPB? cellData,
|
|
||||||
required this.cellController,
|
|
||||||
}) : super(DateCalState.initial(dateTypeOptionPB, cellData)) {
|
|
||||||
on<DateCalEvent>(
|
|
||||||
(event, emit) async {
|
|
||||||
await event.when(
|
|
||||||
initial: () async => _startListening(),
|
|
||||||
selectDay: (date) async {
|
|
||||||
await _updateDateData(emit, date: date, time: state.time);
|
|
||||||
},
|
|
||||||
setCalFormat: (format) {
|
|
||||||
emit(state.copyWith(format: format));
|
|
||||||
},
|
|
||||||
setFocusedDay: (focusedDay) {
|
|
||||||
emit(state.copyWith(focusedDay: focusedDay));
|
|
||||||
},
|
|
||||||
didReceiveCellUpdate: (DateCellDataPB? cellData) {
|
|
||||||
final calData = calDataFromCellData(cellData);
|
|
||||||
final time = calData.foldRight(
|
|
||||||
"", (dateData, previous) => dateData.time ?? '');
|
|
||||||
emit(state.copyWith(calData: calData, time: time));
|
|
||||||
},
|
|
||||||
setIncludeTime: (includeTime) async {
|
|
||||||
await _updateTypeOption(emit, includeTime: includeTime);
|
|
||||||
},
|
|
||||||
setDateFormat: (dateFormat) async {
|
|
||||||
await _updateTypeOption(emit, dateFormat: dateFormat);
|
|
||||||
},
|
|
||||||
setTimeFormat: (timeFormat) async {
|
|
||||||
await _updateTypeOption(emit, timeFormat: timeFormat);
|
|
||||||
},
|
|
||||||
setTime: (time) async {
|
|
||||||
if (state.calData.isSome()) {
|
|
||||||
await _updateDateData(emit, time: time);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
didUpdateCalData:
|
|
||||||
(Option<CalendarData> data, Option<String> timeFormatError) {
|
|
||||||
emit(state.copyWith(
|
|
||||||
calData: data, timeFormatError: timeFormatError));
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _updateDateData(Emitter<DateCalState> emit,
|
|
||||||
{DateTime? date, String? time}) {
|
|
||||||
final CalendarData newDateData = state.calData.fold(
|
|
||||||
() => CalendarData(date: date ?? DateTime.now(), time: time),
|
|
||||||
(dateData) {
|
|
||||||
var newDateData = dateData;
|
|
||||||
if (date != null && !isSameDay(newDateData.date, date)) {
|
|
||||||
newDateData = newDateData.copyWith(date: date);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newDateData.time != time) {
|
|
||||||
newDateData = newDateData.copyWith(time: time);
|
|
||||||
}
|
|
||||||
return newDateData;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
return _saveDateData(emit, newDateData);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _saveDateData(
|
|
||||||
Emitter<DateCalState> emit, CalendarData newCalData) async {
|
|
||||||
if (state.calData == Some(newCalData)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateCalData(
|
|
||||||
Option<CalendarData> calData, Option<String> timeFormatError) {
|
|
||||||
if (!isClosed) {
|
|
||||||
add(DateCalEvent.didUpdateCalData(calData, timeFormatError));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cellController.saveCellData(newCalData, onFinish: (result) {
|
|
||||||
result.fold(
|
|
||||||
() => updateCalData(Some(newCalData), none()),
|
|
||||||
(err) {
|
|
||||||
switch (ErrorCode.valueOf(err.code)!) {
|
|
||||||
case ErrorCode.InvalidDateTimeFormat:
|
|
||||||
updateCalData(state.calData, Some(timeFormatPrompt(err)));
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
Log.error(err);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
String timeFormatPrompt(FlowyError error) {
|
|
||||||
String msg = "${LocaleKeys.grid_field_invalidTimeFormat.tr()}.";
|
|
||||||
switch (state.dateTypeOptionPB.timeFormat) {
|
|
||||||
case TimeFormat.TwelveHour:
|
|
||||||
msg = "$msg e.g. 01:00 PM";
|
|
||||||
break;
|
|
||||||
case TimeFormat.TwentyFourHour:
|
|
||||||
msg = "$msg e.g. 13:00";
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return msg;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> close() async {
|
|
||||||
if (_onCellChangedFn != null) {
|
|
||||||
cellController.removeListener(_onCellChangedFn!);
|
|
||||||
_onCellChangedFn = null;
|
|
||||||
}
|
|
||||||
await cellController.dispose();
|
|
||||||
return super.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _startListening() {
|
|
||||||
_onCellChangedFn = cellController.startListening(
|
|
||||||
onCellChanged: ((cell) {
|
|
||||||
if (!isClosed) {
|
|
||||||
add(DateCalEvent.didReceiveCellUpdate(cell));
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void>? _updateTypeOption(
|
|
||||||
Emitter<DateCalState> emit, {
|
|
||||||
DateFormat? dateFormat,
|
|
||||||
TimeFormat? timeFormat,
|
|
||||||
bool? includeTime,
|
|
||||||
}) async {
|
|
||||||
state.dateTypeOptionPB.freeze();
|
|
||||||
final newDateTypeOption = state.dateTypeOptionPB.rebuild((typeOption) {
|
|
||||||
if (dateFormat != null) {
|
|
||||||
typeOption.dateFormat = dateFormat;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (timeFormat != null) {
|
|
||||||
typeOption.timeFormat = timeFormat;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (includeTime != null) {
|
|
||||||
typeOption.includeTime = includeTime;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
final result = await FieldService.updateFieldTypeOption(
|
|
||||||
viewId: cellController.databaseId,
|
|
||||||
fieldId: cellController.fieldInfo.id,
|
|
||||||
typeOptionData: newDateTypeOption.writeToBuffer(),
|
|
||||||
);
|
|
||||||
|
|
||||||
result.fold(
|
|
||||||
(l) => emit(state.copyWith(
|
|
||||||
dateTypeOptionPB: newDateTypeOption,
|
|
||||||
timeHintText: _timeHintText(newDateTypeOption))),
|
|
||||||
(err) => Log.error(err),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@freezed
|
|
||||||
class DateCalEvent with _$DateCalEvent {
|
|
||||||
const factory DateCalEvent.initial() = _Initial;
|
|
||||||
const factory DateCalEvent.selectDay(DateTime day) = _SelectDay;
|
|
||||||
const factory DateCalEvent.setCalFormat(CalendarFormat format) =
|
|
||||||
_CalendarFormat;
|
|
||||||
const factory DateCalEvent.setFocusedDay(DateTime day) = _FocusedDay;
|
|
||||||
const factory DateCalEvent.setTimeFormat(TimeFormat timeFormat) = _TimeFormat;
|
|
||||||
const factory DateCalEvent.setDateFormat(DateFormat dateFormat) = _DateFormat;
|
|
||||||
const factory DateCalEvent.setIncludeTime(bool includeTime) = _IncludeTime;
|
|
||||||
const factory DateCalEvent.setTime(String time) = _Time;
|
|
||||||
const factory DateCalEvent.didReceiveCellUpdate(DateCellDataPB? data) =
|
|
||||||
_DidReceiveCellUpdate;
|
|
||||||
const factory DateCalEvent.didUpdateCalData(
|
|
||||||
Option<CalendarData> data, Option<String> timeFormatError) =
|
|
||||||
_DidUpdateCalData;
|
|
||||||
}
|
|
||||||
|
|
||||||
@freezed
|
|
||||||
class DateCalState with _$DateCalState {
|
|
||||||
const factory DateCalState({
|
|
||||||
required DateTypeOptionPB dateTypeOptionPB,
|
|
||||||
required CalendarFormat format,
|
|
||||||
required DateTime focusedDay,
|
|
||||||
required Option<String> timeFormatError,
|
|
||||||
required Option<CalendarData> calData,
|
|
||||||
required String? time,
|
|
||||||
required String timeHintText,
|
|
||||||
}) = _DateCalState;
|
|
||||||
|
|
||||||
factory DateCalState.initial(
|
|
||||||
DateTypeOptionPB dateTypeOptionPB,
|
|
||||||
DateCellDataPB? cellData,
|
|
||||||
) {
|
|
||||||
Option<CalendarData> calData = calDataFromCellData(cellData);
|
|
||||||
final time =
|
|
||||||
calData.foldRight("", (dateData, previous) => dateData.time ?? '');
|
|
||||||
return DateCalState(
|
|
||||||
dateTypeOptionPB: dateTypeOptionPB,
|
|
||||||
format: CalendarFormat.month,
|
|
||||||
focusedDay: DateTime.now(),
|
|
||||||
time: time,
|
|
||||||
calData: calData,
|
|
||||||
timeFormatError: none(),
|
|
||||||
timeHintText: _timeHintText(dateTypeOptionPB),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String _timeHintText(DateTypeOptionPB typeOption) {
|
|
||||||
switch (typeOption.timeFormat) {
|
|
||||||
case TimeFormat.TwelveHour:
|
|
||||||
return LocaleKeys.document_date_timeHintTextInTwelveHour.tr();
|
|
||||||
case TimeFormat.TwentyFourHour:
|
|
||||||
return LocaleKeys.document_date_timeHintTextInTwentyFourHour.tr();
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
Option<CalendarData> calDataFromCellData(DateCellDataPB? cellData) {
|
|
||||||
String? time = timeFromCellData(cellData);
|
|
||||||
Option<CalendarData> calData = none();
|
|
||||||
if (cellData != null) {
|
|
||||||
final timestamp = cellData.timestamp * 1000;
|
|
||||||
final date = DateTime.fromMillisecondsSinceEpoch(timestamp.toInt());
|
|
||||||
calData = Some(CalendarData(date: date, time: time));
|
|
||||||
}
|
|
||||||
return calData;
|
|
||||||
}
|
|
||||||
|
|
||||||
$fixnum.Int64 timestampFromDateTime(DateTime dateTime) {
|
|
||||||
final timestamp = (dateTime.millisecondsSinceEpoch ~/ 1000);
|
|
||||||
return $fixnum.Int64(timestamp);
|
|
||||||
}
|
|
||||||
|
|
||||||
String? timeFromCellData(DateCellDataPB? cellData) {
|
|
||||||
String? time;
|
|
||||||
if (cellData?.hasTime() ?? false) {
|
|
||||||
time = cellData?.time;
|
|
||||||
}
|
|
||||||
return time;
|
|
||||||
}
|
|
@ -1,81 +0,0 @@
|
|||||||
import 'package:app_flowy/plugins/grid/application/field/field_controller.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-database/date_type_option_entities.pb.dart';
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
|
||||||
import 'dart:async';
|
|
||||||
import 'cell_service/cell_service.dart';
|
|
||||||
part 'date_cell_bloc.freezed.dart';
|
|
||||||
|
|
||||||
class DateCellBloc extends Bloc<DateCellEvent, DateCellState> {
|
|
||||||
final GridDateCellController cellController;
|
|
||||||
void Function()? _onCellChangedFn;
|
|
||||||
|
|
||||||
DateCellBloc({required this.cellController})
|
|
||||||
: super(DateCellState.initial(cellController)) {
|
|
||||||
on<DateCellEvent>(
|
|
||||||
(event, emit) async {
|
|
||||||
event.when(
|
|
||||||
initial: () => _startListening(),
|
|
||||||
didReceiveCellUpdate: (DateCellDataPB? cellData) {
|
|
||||||
emit(state.copyWith(
|
|
||||||
data: cellData, dateStr: _dateStrFromCellData(cellData)));
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> close() async {
|
|
||||||
if (_onCellChangedFn != null) {
|
|
||||||
cellController.removeListener(_onCellChangedFn!);
|
|
||||||
_onCellChangedFn = null;
|
|
||||||
}
|
|
||||||
await cellController.dispose();
|
|
||||||
return super.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _startListening() {
|
|
||||||
_onCellChangedFn = cellController.startListening(
|
|
||||||
onCellChanged: ((data) {
|
|
||||||
if (!isClosed) {
|
|
||||||
add(DateCellEvent.didReceiveCellUpdate(data));
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@freezed
|
|
||||||
class DateCellEvent with _$DateCellEvent {
|
|
||||||
const factory DateCellEvent.initial() = _InitialCell;
|
|
||||||
const factory DateCellEvent.didReceiveCellUpdate(DateCellDataPB? data) =
|
|
||||||
_DidReceiveCellUpdate;
|
|
||||||
}
|
|
||||||
|
|
||||||
@freezed
|
|
||||||
class DateCellState with _$DateCellState {
|
|
||||||
const factory DateCellState({
|
|
||||||
required DateCellDataPB? data,
|
|
||||||
required String dateStr,
|
|
||||||
required FieldInfo fieldInfo,
|
|
||||||
}) = _DateCellState;
|
|
||||||
|
|
||||||
factory DateCellState.initial(GridDateCellController context) {
|
|
||||||
final cellData = context.getCellData();
|
|
||||||
|
|
||||||
return DateCellState(
|
|
||||||
fieldInfo: context.fieldInfo,
|
|
||||||
data: cellData,
|
|
||||||
dateStr: _dateStrFromCellData(cellData),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String _dateStrFromCellData(DateCellDataPB? cellData) {
|
|
||||||
String dateStr = "";
|
|
||||||
if (cellData != null) {
|
|
||||||
dateStr = "${cellData.date} ${cellData.time}";
|
|
||||||
}
|
|
||||||
return dateStr;
|
|
||||||
}
|
|
@ -1,90 +0,0 @@
|
|||||||
import 'package:appflowy_backend/log.dart';
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
|
||||||
import 'dart:async';
|
|
||||||
import 'cell_service/cell_service.dart';
|
|
||||||
|
|
||||||
part 'number_cell_bloc.freezed.dart';
|
|
||||||
|
|
||||||
class NumberCellBloc extends Bloc<NumberCellEvent, NumberCellState> {
|
|
||||||
final GridNumberCellController cellController;
|
|
||||||
void Function()? _onCellChangedFn;
|
|
||||||
|
|
||||||
NumberCellBloc({
|
|
||||||
required this.cellController,
|
|
||||||
}) : super(NumberCellState.initial(cellController)) {
|
|
||||||
on<NumberCellEvent>(
|
|
||||||
(event, emit) async {
|
|
||||||
event.when(
|
|
||||||
initial: () {
|
|
||||||
_startListening();
|
|
||||||
},
|
|
||||||
didReceiveCellUpdate: (cellContent) {
|
|
||||||
emit(state.copyWith(cellContent: cellContent ?? ""));
|
|
||||||
},
|
|
||||||
updateCell: (text) {
|
|
||||||
if (state.cellContent != text) {
|
|
||||||
emit(state.copyWith(cellContent: text));
|
|
||||||
cellController.saveCellData(text, onFinish: (result) {
|
|
||||||
result.fold(
|
|
||||||
() {},
|
|
||||||
(err) => Log.error(err),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> close() async {
|
|
||||||
if (_onCellChangedFn != null) {
|
|
||||||
cellController.removeListener(_onCellChangedFn!);
|
|
||||||
_onCellChangedFn = null;
|
|
||||||
}
|
|
||||||
await cellController.dispose();
|
|
||||||
return super.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _startListening() {
|
|
||||||
_onCellChangedFn =
|
|
||||||
cellController.startListening(onCellChanged: ((cellContent) {
|
|
||||||
if (!isClosed) {
|
|
||||||
add(NumberCellEvent.didReceiveCellUpdate(cellContent));
|
|
||||||
}
|
|
||||||
}), listenWhenOnCellChanged: (oldValue, newValue) {
|
|
||||||
// If the new value is not the same as the content, which means the
|
|
||||||
// backend formatted the content that user enter. For example:
|
|
||||||
//
|
|
||||||
// state.cellContent: "abc"
|
|
||||||
// oldValue: ""
|
|
||||||
// newValue: ""
|
|
||||||
// The oldValue is the same as newValue. So the [onCellChanged] won't
|
|
||||||
// get called. So just return true to refresh the cell content
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@freezed
|
|
||||||
class NumberCellEvent with _$NumberCellEvent {
|
|
||||||
const factory NumberCellEvent.initial() = _Initial;
|
|
||||||
const factory NumberCellEvent.updateCell(String text) = _UpdateCell;
|
|
||||||
const factory NumberCellEvent.didReceiveCellUpdate(String? cellContent) =
|
|
||||||
_DidReceiveCellUpdate;
|
|
||||||
}
|
|
||||||
|
|
||||||
@freezed
|
|
||||||
class NumberCellState with _$NumberCellState {
|
|
||||||
const factory NumberCellState({
|
|
||||||
required String cellContent,
|
|
||||||
}) = _NumberCellState;
|
|
||||||
|
|
||||||
factory NumberCellState.initial(GridTextCellController context) {
|
|
||||||
return NumberCellState(
|
|
||||||
cellContent: context.getCellData() ?? "",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,78 +0,0 @@
|
|||||||
import 'dart:async';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-database/select_type_option.pb.dart';
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
|
||||||
import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart';
|
|
||||||
|
|
||||||
part 'select_option_cell_bloc.freezed.dart';
|
|
||||||
|
|
||||||
class SelectOptionCellBloc
|
|
||||||
extends Bloc<SelectOptionCellEvent, SelectOptionCellState> {
|
|
||||||
final GridSelectOptionCellController cellController;
|
|
||||||
void Function()? _onCellChangedFn;
|
|
||||||
|
|
||||||
SelectOptionCellBloc({
|
|
||||||
required this.cellController,
|
|
||||||
}) : super(SelectOptionCellState.initial(cellController)) {
|
|
||||||
on<SelectOptionCellEvent>(
|
|
||||||
(event, emit) async {
|
|
||||||
await event.map(
|
|
||||||
initial: (_InitialCell value) async {
|
|
||||||
_startListening();
|
|
||||||
},
|
|
||||||
didReceiveOptions: (_DidReceiveOptions value) {
|
|
||||||
emit(state.copyWith(
|
|
||||||
selectedOptions: value.selectedOptions,
|
|
||||||
));
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> close() async {
|
|
||||||
if (_onCellChangedFn != null) {
|
|
||||||
cellController.removeListener(_onCellChangedFn!);
|
|
||||||
_onCellChangedFn = null;
|
|
||||||
}
|
|
||||||
await cellController.dispose();
|
|
||||||
return super.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _startListening() {
|
|
||||||
_onCellChangedFn = cellController.startListening(
|
|
||||||
onCellChanged: ((selectOptionContext) {
|
|
||||||
if (!isClosed) {
|
|
||||||
add(SelectOptionCellEvent.didReceiveOptions(
|
|
||||||
selectOptionContext?.selectOptions ?? [],
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@freezed
|
|
||||||
class SelectOptionCellEvent with _$SelectOptionCellEvent {
|
|
||||||
const factory SelectOptionCellEvent.initial() = _InitialCell;
|
|
||||||
const factory SelectOptionCellEvent.didReceiveOptions(
|
|
||||||
List<SelectOptionPB> selectedOptions,
|
|
||||||
) = _DidReceiveOptions;
|
|
||||||
}
|
|
||||||
|
|
||||||
@freezed
|
|
||||||
class SelectOptionCellState with _$SelectOptionCellState {
|
|
||||||
const factory SelectOptionCellState({
|
|
||||||
required List<SelectOptionPB> selectedOptions,
|
|
||||||
}) = _SelectOptionCellState;
|
|
||||||
|
|
||||||
factory SelectOptionCellState.initial(
|
|
||||||
GridSelectOptionCellController context) {
|
|
||||||
final data = context.getCellData();
|
|
||||||
|
|
||||||
return SelectOptionCellState(
|
|
||||||
selectedOptions: data?.selectOptions ?? [],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,280 +0,0 @@
|
|||||||
import 'dart:async';
|
|
||||||
import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart';
|
|
||||||
import 'package:dartz/dartz.dart';
|
|
||||||
import 'package:appflowy_backend/log.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-database/select_type_option.pb.dart';
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
|
||||||
import 'select_option_service.dart';
|
|
||||||
|
|
||||||
part 'select_option_editor_bloc.freezed.dart';
|
|
||||||
|
|
||||||
class SelectOptionCellEditorBloc
|
|
||||||
extends Bloc<SelectOptionEditorEvent, SelectOptionEditorState> {
|
|
||||||
final SelectOptionFFIService _selectOptionService;
|
|
||||||
final GridSelectOptionCellController cellController;
|
|
||||||
|
|
||||||
SelectOptionCellEditorBloc({
|
|
||||||
required this.cellController,
|
|
||||||
}) : _selectOptionService =
|
|
||||||
SelectOptionFFIService(cellId: cellController.cellId),
|
|
||||||
super(SelectOptionEditorState.initial(cellController)) {
|
|
||||||
on<SelectOptionEditorEvent>(
|
|
||||||
(event, emit) async {
|
|
||||||
await event.map(
|
|
||||||
initial: (_Initial value) async {
|
|
||||||
_startListening();
|
|
||||||
await _loadOptions();
|
|
||||||
},
|
|
||||||
didReceiveOptions: (_DidReceiveOptions value) {
|
|
||||||
final result = _makeOptions(state.filter, value.options);
|
|
||||||
emit(state.copyWith(
|
|
||||||
allOptions: value.options,
|
|
||||||
options: result.options,
|
|
||||||
createOption: result.createOption,
|
|
||||||
selectedOptions: value.selectedOptions,
|
|
||||||
));
|
|
||||||
},
|
|
||||||
newOption: (_NewOption value) async {
|
|
||||||
await _createOption(value.optionName);
|
|
||||||
emit(state.copyWith(
|
|
||||||
filter: none(),
|
|
||||||
));
|
|
||||||
},
|
|
||||||
deleteOption: (_DeleteOption value) async {
|
|
||||||
await _deleteOption([value.option]);
|
|
||||||
},
|
|
||||||
deleteAllOptions: (_DeleteAllOptions value) async {
|
|
||||||
if (state.allOptions.isNotEmpty) {
|
|
||||||
await _deleteOption(state.allOptions);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
updateOption: (_UpdateOption value) async {
|
|
||||||
await _updateOption(value.option);
|
|
||||||
},
|
|
||||||
selectOption: (_SelectOption value) async {
|
|
||||||
await _selectOptionService.select(optionIds: [value.optionId]);
|
|
||||||
},
|
|
||||||
unSelectOption: (_UnSelectOption value) async {
|
|
||||||
await _selectOptionService.unSelect(optionIds: [value.optionId]);
|
|
||||||
},
|
|
||||||
trySelectOption: (_TrySelectOption value) {
|
|
||||||
_trySelectOption(value.optionName, emit);
|
|
||||||
},
|
|
||||||
selectMultipleOptions: (_SelectMultipleOptions value) {
|
|
||||||
if (value.optionNames.isNotEmpty) {
|
|
||||||
_selectMultipleOptions(value.optionNames);
|
|
||||||
}
|
|
||||||
_filterOption(value.remainder, emit);
|
|
||||||
},
|
|
||||||
filterOption: (_SelectOptionFilter value) {
|
|
||||||
_filterOption(value.optionName, emit);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> close() async {
|
|
||||||
await cellController.dispose();
|
|
||||||
return super.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _createOption(String name) async {
|
|
||||||
final result = await _selectOptionService.create(name: name);
|
|
||||||
result.fold((l) => {}, (err) => Log.error(err));
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _deleteOption(List<SelectOptionPB> options) async {
|
|
||||||
final result = await _selectOptionService.delete(options: options);
|
|
||||||
result.fold((l) => null, (err) => Log.error(err));
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _updateOption(SelectOptionPB option) async {
|
|
||||||
final result = await _selectOptionService.update(
|
|
||||||
option: option,
|
|
||||||
);
|
|
||||||
|
|
||||||
result.fold((l) => null, (err) => Log.error(err));
|
|
||||||
}
|
|
||||||
|
|
||||||
void _trySelectOption(
|
|
||||||
String optionName, Emitter<SelectOptionEditorState> emit) {
|
|
||||||
SelectOptionPB? matchingOption;
|
|
||||||
bool optionExistsButSelected = false;
|
|
||||||
|
|
||||||
for (final option in state.options) {
|
|
||||||
if (option.name.toLowerCase() == optionName.toLowerCase()) {
|
|
||||||
if (!state.selectedOptions.contains(option)) {
|
|
||||||
matchingOption = option;
|
|
||||||
break;
|
|
||||||
} else {
|
|
||||||
optionExistsButSelected = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// if there isn't a matching option at all, then create it
|
|
||||||
if (matchingOption == null && !optionExistsButSelected) {
|
|
||||||
_createOption(optionName);
|
|
||||||
}
|
|
||||||
|
|
||||||
// if there is an unselected matching option, select it
|
|
||||||
if (matchingOption != null) {
|
|
||||||
_selectOptionService.select(optionIds: [matchingOption.id]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// clear the filter
|
|
||||||
emit(state.copyWith(filter: none()));
|
|
||||||
}
|
|
||||||
|
|
||||||
void _selectMultipleOptions(List<String> optionNames) {
|
|
||||||
// The options are unordered. So in order to keep the inserted [optionNames]
|
|
||||||
// order, it needs to get the option id in the [optionNames] order.
|
|
||||||
final lowerCaseNames = optionNames.map((e) => e.toLowerCase());
|
|
||||||
final Map<String, String> optionIdsMap = {};
|
|
||||||
for (final option in state.options) {
|
|
||||||
optionIdsMap[option.name.toLowerCase()] = option.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
final optionIds = lowerCaseNames
|
|
||||||
.where((name) => optionIdsMap[name] != null)
|
|
||||||
.map((name) => optionIdsMap[name]!)
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
_selectOptionService.select(optionIds: optionIds);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _filterOption(String optionName, Emitter<SelectOptionEditorState> emit) {
|
|
||||||
final _MakeOptionResult result = _makeOptions(
|
|
||||||
Some(optionName),
|
|
||||||
state.allOptions,
|
|
||||||
);
|
|
||||||
emit(state.copyWith(
|
|
||||||
filter: Some(optionName),
|
|
||||||
options: result.options,
|
|
||||||
createOption: result.createOption,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _loadOptions() async {
|
|
||||||
final result = await _selectOptionService.getOptionContext();
|
|
||||||
if (isClosed) {
|
|
||||||
Log.warn("Unexpected closing the bloc");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
return result.fold(
|
|
||||||
(data) => add(
|
|
||||||
SelectOptionEditorEvent.didReceiveOptions(
|
|
||||||
data.options,
|
|
||||||
data.selectOptions,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(err) {
|
|
||||||
Log.error(err);
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
_MakeOptionResult _makeOptions(
|
|
||||||
Option<String> filter,
|
|
||||||
List<SelectOptionPB> allOptions,
|
|
||||||
) {
|
|
||||||
final List<SelectOptionPB> options = List.from(allOptions);
|
|
||||||
Option<String> createOption = filter;
|
|
||||||
|
|
||||||
filter.foldRight(null, (filter, previous) {
|
|
||||||
if (filter.isNotEmpty) {
|
|
||||||
options.retainWhere((option) {
|
|
||||||
final name = option.name.toLowerCase();
|
|
||||||
final lFilter = filter.toLowerCase();
|
|
||||||
|
|
||||||
if (name == lFilter) {
|
|
||||||
createOption = none();
|
|
||||||
}
|
|
||||||
|
|
||||||
return name.contains(lFilter);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
createOption = none();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return _MakeOptionResult(
|
|
||||||
options: options,
|
|
||||||
createOption: createOption,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _startListening() {
|
|
||||||
cellController.startListening(
|
|
||||||
onCellChanged: ((selectOptionContext) {
|
|
||||||
_loadOptions();
|
|
||||||
}),
|
|
||||||
onCellFieldChanged: () {
|
|
||||||
_loadOptions();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@freezed
|
|
||||||
class SelectOptionEditorEvent with _$SelectOptionEditorEvent {
|
|
||||||
const factory SelectOptionEditorEvent.initial() = _Initial;
|
|
||||||
const factory SelectOptionEditorEvent.didReceiveOptions(
|
|
||||||
List<SelectOptionPB> options, List<SelectOptionPB> selectedOptions) =
|
|
||||||
_DidReceiveOptions;
|
|
||||||
const factory SelectOptionEditorEvent.newOption(String optionName) =
|
|
||||||
_NewOption;
|
|
||||||
const factory SelectOptionEditorEvent.selectOption(String optionId) =
|
|
||||||
_SelectOption;
|
|
||||||
const factory SelectOptionEditorEvent.unSelectOption(String optionId) =
|
|
||||||
_UnSelectOption;
|
|
||||||
const factory SelectOptionEditorEvent.updateOption(SelectOptionPB option) =
|
|
||||||
_UpdateOption;
|
|
||||||
const factory SelectOptionEditorEvent.deleteOption(SelectOptionPB option) =
|
|
||||||
_DeleteOption;
|
|
||||||
const factory SelectOptionEditorEvent.deleteAllOptions() = _DeleteAllOptions;
|
|
||||||
const factory SelectOptionEditorEvent.filterOption(String optionName) =
|
|
||||||
_SelectOptionFilter;
|
|
||||||
const factory SelectOptionEditorEvent.trySelectOption(String optionName) =
|
|
||||||
_TrySelectOption;
|
|
||||||
const factory SelectOptionEditorEvent.selectMultipleOptions(
|
|
||||||
List<String> optionNames, String remainder) = _SelectMultipleOptions;
|
|
||||||
}
|
|
||||||
|
|
||||||
@freezed
|
|
||||||
class SelectOptionEditorState with _$SelectOptionEditorState {
|
|
||||||
const factory SelectOptionEditorState({
|
|
||||||
required List<SelectOptionPB> options,
|
|
||||||
required List<SelectOptionPB> allOptions,
|
|
||||||
required List<SelectOptionPB> selectedOptions,
|
|
||||||
required Option<String> createOption,
|
|
||||||
required Option<String> filter,
|
|
||||||
}) = _SelectOptionEditorState;
|
|
||||||
|
|
||||||
factory SelectOptionEditorState.initial(
|
|
||||||
GridSelectOptionCellController context) {
|
|
||||||
final data = context.getCellData(loadIfNotExist: false);
|
|
||||||
return SelectOptionEditorState(
|
|
||||||
options: data?.options ?? [],
|
|
||||||
allOptions: data?.options ?? [],
|
|
||||||
selectedOptions: data?.selectOptions ?? [],
|
|
||||||
createOption: none(),
|
|
||||||
filter: none(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _MakeOptionResult {
|
|
||||||
List<SelectOptionPB> options;
|
|
||||||
Option<String> createOption;
|
|
||||||
|
|
||||||
_MakeOptionResult({
|
|
||||||
required this.options,
|
|
||||||
required this.createOption,
|
|
||||||
});
|
|
||||||
}
|
|
@ -1,94 +0,0 @@
|
|||||||
import 'package:dartz/dartz.dart';
|
|
||||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-database/cell_entities.pb.dart';
|
|
||||||
import 'package:app_flowy/plugins/grid/application/field/type_option/type_option_service.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-database/select_type_option.pb.dart';
|
|
||||||
import 'cell_service/cell_service.dart';
|
|
||||||
|
|
||||||
class SelectOptionFFIService {
|
|
||||||
final GridCellIdentifier cellId;
|
|
||||||
SelectOptionFFIService({required this.cellId});
|
|
||||||
|
|
||||||
String get databaseId => cellId.databaseId;
|
|
||||||
String get fieldId => cellId.fieldInfo.id;
|
|
||||||
String get rowId => cellId.rowId;
|
|
||||||
|
|
||||||
Future<Either<Unit, FlowyError>> create(
|
|
||||||
{required String name, bool isSelected = true}) {
|
|
||||||
return TypeOptionFFIService(databaseId: databaseId, fieldId: fieldId)
|
|
||||||
.newOption(name: name)
|
|
||||||
.then(
|
|
||||||
(result) {
|
|
||||||
return result.fold(
|
|
||||||
(option) {
|
|
||||||
final cellIdentifier = CellIdPB.create()
|
|
||||||
..databaseId = databaseId
|
|
||||||
..fieldId = fieldId
|
|
||||||
..rowId = rowId;
|
|
||||||
final payload = SelectOptionChangesetPB.create()
|
|
||||||
..cellIdentifier = cellIdentifier;
|
|
||||||
|
|
||||||
if (isSelected) {
|
|
||||||
payload.insertOptions.add(option);
|
|
||||||
} else {
|
|
||||||
payload.updateOptions.add(option);
|
|
||||||
}
|
|
||||||
return DatabaseEventUpdateSelectOption(payload).send();
|
|
||||||
},
|
|
||||||
(r) => right(r),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<Either<Unit, FlowyError>> update({
|
|
||||||
required SelectOptionPB option,
|
|
||||||
}) {
|
|
||||||
final payload = SelectOptionChangesetPB.create()
|
|
||||||
..updateOptions.add(option)
|
|
||||||
..cellIdentifier = _cellIdentifier();
|
|
||||||
return DatabaseEventUpdateSelectOption(payload).send();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<Either<Unit, FlowyError>> delete(
|
|
||||||
{required Iterable<SelectOptionPB> options}) {
|
|
||||||
final payload = SelectOptionChangesetPB.create()
|
|
||||||
..deleteOptions.addAll(options)
|
|
||||||
..cellIdentifier = _cellIdentifier();
|
|
||||||
|
|
||||||
return DatabaseEventUpdateSelectOption(payload).send();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<Either<SelectOptionCellDataPB, FlowyError>> getOptionContext() {
|
|
||||||
final payload = CellIdPB.create()
|
|
||||||
..databaseId = databaseId
|
|
||||||
..fieldId = fieldId
|
|
||||||
..rowId = rowId;
|
|
||||||
|
|
||||||
return DatabaseEventGetSelectOptionCellData(payload).send();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<Either<void, FlowyError>> select(
|
|
||||||
{required Iterable<String> optionIds}) {
|
|
||||||
final payload = SelectOptionCellChangesetPB.create()
|
|
||||||
..cellIdentifier = _cellIdentifier()
|
|
||||||
..insertOptionIds.addAll(optionIds);
|
|
||||||
return DatabaseEventUpdateSelectOptionCell(payload).send();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<Either<void, FlowyError>> unSelect(
|
|
||||||
{required Iterable<String> optionIds}) {
|
|
||||||
final payload = SelectOptionCellChangesetPB.create()
|
|
||||||
..cellIdentifier = _cellIdentifier()
|
|
||||||
..deleteOptionIds.addAll(optionIds);
|
|
||||||
return DatabaseEventUpdateSelectOptionCell(payload).send();
|
|
||||||
}
|
|
||||||
|
|
||||||
CellIdPB _cellIdentifier() {
|
|
||||||
return CellIdPB.create()
|
|
||||||
..databaseId = databaseId
|
|
||||||
..fieldId = fieldId
|
|
||||||
..rowId = rowId;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,73 +0,0 @@
|
|||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
|
||||||
import 'dart:async';
|
|
||||||
import 'cell_service/cell_service.dart';
|
|
||||||
|
|
||||||
part 'text_cell_bloc.freezed.dart';
|
|
||||||
|
|
||||||
class TextCellBloc extends Bloc<TextCellEvent, TextCellState> {
|
|
||||||
final GridTextCellController cellController;
|
|
||||||
void Function()? _onCellChangedFn;
|
|
||||||
TextCellBloc({
|
|
||||||
required this.cellController,
|
|
||||||
}) : super(TextCellState.initial(cellController)) {
|
|
||||||
on<TextCellEvent>(
|
|
||||||
(event, emit) async {
|
|
||||||
await event.when(
|
|
||||||
initial: () async {
|
|
||||||
_startListening();
|
|
||||||
},
|
|
||||||
updateText: (text) {
|
|
||||||
if (state.content != text) {
|
|
||||||
cellController.saveCellData(text);
|
|
||||||
emit(state.copyWith(content: text));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
didReceiveCellUpdate: (content) {
|
|
||||||
emit(state.copyWith(content: content));
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> close() async {
|
|
||||||
if (_onCellChangedFn != null) {
|
|
||||||
cellController.removeListener(_onCellChangedFn!);
|
|
||||||
_onCellChangedFn = null;
|
|
||||||
}
|
|
||||||
await cellController.dispose();
|
|
||||||
return super.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _startListening() {
|
|
||||||
_onCellChangedFn = cellController.startListening(
|
|
||||||
onCellChanged: ((cellContent) {
|
|
||||||
if (!isClosed) {
|
|
||||||
add(TextCellEvent.didReceiveCellUpdate(cellContent ?? ""));
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@freezed
|
|
||||||
class TextCellEvent with _$TextCellEvent {
|
|
||||||
const factory TextCellEvent.initial() = _InitialCell;
|
|
||||||
const factory TextCellEvent.didReceiveCellUpdate(String cellContent) =
|
|
||||||
_DidReceiveCellUpdate;
|
|
||||||
const factory TextCellEvent.updateText(String text) = _UpdateText;
|
|
||||||
}
|
|
||||||
|
|
||||||
@freezed
|
|
||||||
class TextCellState with _$TextCellState {
|
|
||||||
const factory TextCellState({
|
|
||||||
required String content,
|
|
||||||
}) = _TextCellState;
|
|
||||||
|
|
||||||
factory TextCellState.initial(GridTextCellController context) =>
|
|
||||||
TextCellState(
|
|
||||||
content: context.getCellData() ?? "",
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,78 +0,0 @@
|
|||||||
import 'package:appflowy_backend/protobuf/flowy-database/url_type_option_entities.pb.dart';
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
|
||||||
import 'dart:async';
|
|
||||||
import 'cell_service/cell_service.dart';
|
|
||||||
|
|
||||||
part 'url_cell_bloc.freezed.dart';
|
|
||||||
|
|
||||||
class URLCellBloc extends Bloc<URLCellEvent, URLCellState> {
|
|
||||||
final GridURLCellController cellController;
|
|
||||||
void Function()? _onCellChangedFn;
|
|
||||||
URLCellBloc({
|
|
||||||
required this.cellController,
|
|
||||||
}) : super(URLCellState.initial(cellController)) {
|
|
||||||
on<URLCellEvent>(
|
|
||||||
(event, emit) async {
|
|
||||||
event.when(
|
|
||||||
initial: () {
|
|
||||||
_startListening();
|
|
||||||
},
|
|
||||||
didReceiveCellUpdate: (cellData) {
|
|
||||||
emit(state.copyWith(
|
|
||||||
content: cellData?.content ?? "",
|
|
||||||
url: cellData?.url ?? "",
|
|
||||||
));
|
|
||||||
},
|
|
||||||
updateURL: (String url) {
|
|
||||||
cellController.saveCellData(url, deduplicate: true);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> close() async {
|
|
||||||
if (_onCellChangedFn != null) {
|
|
||||||
cellController.removeListener(_onCellChangedFn!);
|
|
||||||
_onCellChangedFn = null;
|
|
||||||
}
|
|
||||||
await cellController.dispose();
|
|
||||||
return super.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _startListening() {
|
|
||||||
_onCellChangedFn = cellController.startListening(
|
|
||||||
onCellChanged: ((cellData) {
|
|
||||||
if (!isClosed) {
|
|
||||||
add(URLCellEvent.didReceiveCellUpdate(cellData));
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@freezed
|
|
||||||
class URLCellEvent with _$URLCellEvent {
|
|
||||||
const factory URLCellEvent.initial() = _InitialCell;
|
|
||||||
const factory URLCellEvent.updateURL(String url) = _UpdateURL;
|
|
||||||
const factory URLCellEvent.didReceiveCellUpdate(URLCellDataPB? cell) =
|
|
||||||
_DidReceiveCellUpdate;
|
|
||||||
}
|
|
||||||
|
|
||||||
@freezed
|
|
||||||
class URLCellState with _$URLCellState {
|
|
||||||
const factory URLCellState({
|
|
||||||
required String content,
|
|
||||||
required String url,
|
|
||||||
}) = _URLCellState;
|
|
||||||
|
|
||||||
factory URLCellState.initial(GridURLCellController context) {
|
|
||||||
final cellData = context.getCellData();
|
|
||||||
return URLCellState(
|
|
||||||
content: cellData?.content ?? "",
|
|
||||||
url: cellData?.url ?? "",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,79 +0,0 @@
|
|||||||
import 'package:appflowy_backend/protobuf/flowy-database/url_type_option_entities.pb.dart';
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
|
||||||
import 'dart:async';
|
|
||||||
import 'cell_service/cell_service.dart';
|
|
||||||
|
|
||||||
part 'url_cell_editor_bloc.freezed.dart';
|
|
||||||
|
|
||||||
class URLCellEditorBloc extends Bloc<URLCellEditorEvent, URLCellEditorState> {
|
|
||||||
final GridURLCellController cellController;
|
|
||||||
void Function()? _onCellChangedFn;
|
|
||||||
URLCellEditorBloc({
|
|
||||||
required this.cellController,
|
|
||||||
}) : super(URLCellEditorState.initial(cellController)) {
|
|
||||||
on<URLCellEditorEvent>(
|
|
||||||
(event, emit) async {
|
|
||||||
await event.when(
|
|
||||||
initial: () {
|
|
||||||
_startListening();
|
|
||||||
},
|
|
||||||
updateText: (text) async {
|
|
||||||
await cellController.saveCellData(text);
|
|
||||||
emit(state.copyWith(
|
|
||||||
content: text,
|
|
||||||
isFinishEditing: true,
|
|
||||||
));
|
|
||||||
},
|
|
||||||
didReceiveCellUpdate: (cellData) {
|
|
||||||
emit(state.copyWith(content: cellData?.content ?? ""));
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> close() async {
|
|
||||||
if (_onCellChangedFn != null) {
|
|
||||||
cellController.removeListener(_onCellChangedFn!);
|
|
||||||
_onCellChangedFn = null;
|
|
||||||
}
|
|
||||||
await cellController.dispose();
|
|
||||||
return super.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _startListening() {
|
|
||||||
_onCellChangedFn = cellController.startListening(
|
|
||||||
onCellChanged: ((cellData) {
|
|
||||||
if (!isClosed) {
|
|
||||||
add(URLCellEditorEvent.didReceiveCellUpdate(cellData));
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@freezed
|
|
||||||
class URLCellEditorEvent with _$URLCellEditorEvent {
|
|
||||||
const factory URLCellEditorEvent.initial() = _InitialCell;
|
|
||||||
const factory URLCellEditorEvent.didReceiveCellUpdate(URLCellDataPB? cell) =
|
|
||||||
_DidReceiveCellUpdate;
|
|
||||||
const factory URLCellEditorEvent.updateText(String text) = _UpdateText;
|
|
||||||
}
|
|
||||||
|
|
||||||
@freezed
|
|
||||||
class URLCellEditorState with _$URLCellEditorState {
|
|
||||||
const factory URLCellEditorState({
|
|
||||||
required String content,
|
|
||||||
required bool isFinishEditing,
|
|
||||||
}) = _URLCellEditorState;
|
|
||||||
|
|
||||||
factory URLCellEditorState.initial(GridURLCellController context) {
|
|
||||||
final cellData = context.getCellData();
|
|
||||||
return URLCellEditorState(
|
|
||||||
content: cellData?.content ?? "",
|
|
||||||
isFinishEditing: true,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,99 +0,0 @@
|
|||||||
import 'package:appflowy_backend/log.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-database/field_entities.pb.dart';
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
|
||||||
import 'dart:async';
|
|
||||||
import 'field_service.dart';
|
|
||||||
|
|
||||||
part 'field_action_sheet_bloc.freezed.dart';
|
|
||||||
|
|
||||||
class FieldActionSheetBloc
|
|
||||||
extends Bloc<FieldActionSheetEvent, FieldActionSheetState> {
|
|
||||||
final FieldService fieldService;
|
|
||||||
|
|
||||||
FieldActionSheetBloc({required GridFieldCellContext fieldCellContext})
|
|
||||||
: fieldService = FieldService(
|
|
||||||
viewId: fieldCellContext.viewId,
|
|
||||||
fieldId: fieldCellContext.field.id,
|
|
||||||
),
|
|
||||||
super(
|
|
||||||
FieldActionSheetState.initial(
|
|
||||||
TypeOptionPB.create()..field_2 = fieldCellContext.field,
|
|
||||||
),
|
|
||||||
) {
|
|
||||||
on<FieldActionSheetEvent>(
|
|
||||||
(event, emit) async {
|
|
||||||
await event.map(
|
|
||||||
updateFieldName: (_UpdateFieldName value) async {
|
|
||||||
final result = await fieldService.updateField(name: value.name);
|
|
||||||
result.fold(
|
|
||||||
(l) => null,
|
|
||||||
(err) => Log.error(err),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
hideField: (_HideField value) async {
|
|
||||||
final result = await fieldService.updateField(visibility: false);
|
|
||||||
result.fold(
|
|
||||||
(l) => null,
|
|
||||||
(err) => Log.error(err),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
showField: (_ShowField value) async {
|
|
||||||
final result = await fieldService.updateField(visibility: true);
|
|
||||||
result.fold(
|
|
||||||
(l) => null,
|
|
||||||
(err) => Log.error(err),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
deleteField: (_DeleteField value) async {
|
|
||||||
final result = await fieldService.deleteField();
|
|
||||||
result.fold(
|
|
||||||
(l) => null,
|
|
||||||
(err) => Log.error(err),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
duplicateField: (_DuplicateField value) async {
|
|
||||||
final result = await fieldService.duplicateField();
|
|
||||||
result.fold(
|
|
||||||
(l) => null,
|
|
||||||
(err) => Log.error(err),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
saveField: (_SaveField value) {},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> close() async {
|
|
||||||
return super.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@freezed
|
|
||||||
class FieldActionSheetEvent with _$FieldActionSheetEvent {
|
|
||||||
const factory FieldActionSheetEvent.updateFieldName(String name) =
|
|
||||||
_UpdateFieldName;
|
|
||||||
const factory FieldActionSheetEvent.hideField() = _HideField;
|
|
||||||
const factory FieldActionSheetEvent.showField() = _ShowField;
|
|
||||||
const factory FieldActionSheetEvent.duplicateField() = _DuplicateField;
|
|
||||||
const factory FieldActionSheetEvent.deleteField() = _DeleteField;
|
|
||||||
const factory FieldActionSheetEvent.saveField() = _SaveField;
|
|
||||||
}
|
|
||||||
|
|
||||||
@freezed
|
|
||||||
class FieldActionSheetState with _$FieldActionSheetState {
|
|
||||||
const factory FieldActionSheetState({
|
|
||||||
required TypeOptionPB fieldTypeOptionData,
|
|
||||||
required String errorText,
|
|
||||||
required String fieldName,
|
|
||||||
}) = _FieldActionSheetState;
|
|
||||||
|
|
||||||
factory FieldActionSheetState.initial(TypeOptionPB data) =>
|
|
||||||
FieldActionSheetState(
|
|
||||||
fieldTypeOptionData: data,
|
|
||||||
errorText: '',
|
|
||||||
fieldName: data.field_2.name,
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,87 +0,0 @@
|
|||||||
import 'package:app_flowy/plugins/grid/application/field/field_listener.dart';
|
|
||||||
import 'package:app_flowy/plugins/grid/application/field/field_service.dart';
|
|
||||||
import 'package:appflowy_backend/log.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-database/field_entities.pb.dart';
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
|
||||||
import 'dart:async';
|
|
||||||
|
|
||||||
part 'field_cell_bloc.freezed.dart';
|
|
||||||
|
|
||||||
class FieldCellBloc extends Bloc<FieldCellEvent, FieldCellState> {
|
|
||||||
final SingleFieldListener _fieldListener;
|
|
||||||
final FieldService _fieldService;
|
|
||||||
|
|
||||||
FieldCellBloc({
|
|
||||||
required GridFieldCellContext cellContext,
|
|
||||||
}) : _fieldListener = SingleFieldListener(fieldId: cellContext.field.id),
|
|
||||||
_fieldService = FieldService(
|
|
||||||
viewId: cellContext.viewId, fieldId: cellContext.field.id),
|
|
||||||
super(FieldCellState.initial(cellContext)) {
|
|
||||||
on<FieldCellEvent>(
|
|
||||||
(event, emit) async {
|
|
||||||
event.when(
|
|
||||||
initial: () {
|
|
||||||
_startListening();
|
|
||||||
},
|
|
||||||
didReceiveFieldUpdate: (field) {
|
|
||||||
emit(state.copyWith(field: cellContext.field));
|
|
||||||
},
|
|
||||||
startUpdateWidth: (offset) {
|
|
||||||
final width = state.width + offset;
|
|
||||||
emit(state.copyWith(width: width));
|
|
||||||
},
|
|
||||||
endUpdateWidth: () {
|
|
||||||
if (state.width != state.field.width.toDouble()) {
|
|
||||||
_fieldService.updateField(width: state.width);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> close() async {
|
|
||||||
await _fieldListener.stop();
|
|
||||||
return super.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _startListening() {
|
|
||||||
_fieldListener.start(onFieldChanged: (result) {
|
|
||||||
if (isClosed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
result.fold(
|
|
||||||
(field) => add(FieldCellEvent.didReceiveFieldUpdate(field)),
|
|
||||||
(err) => Log.error(err),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@freezed
|
|
||||||
class FieldCellEvent with _$FieldCellEvent {
|
|
||||||
const factory FieldCellEvent.initial() = _InitialCell;
|
|
||||||
const factory FieldCellEvent.didReceiveFieldUpdate(FieldPB field) =
|
|
||||||
_DidReceiveFieldUpdate;
|
|
||||||
const factory FieldCellEvent.startUpdateWidth(double offset) =
|
|
||||||
_StartUpdateWidth;
|
|
||||||
const factory FieldCellEvent.endUpdateWidth() = _EndUpdateWidth;
|
|
||||||
}
|
|
||||||
|
|
||||||
@freezed
|
|
||||||
class FieldCellState with _$FieldCellState {
|
|
||||||
const factory FieldCellState({
|
|
||||||
required String databaseId,
|
|
||||||
required FieldPB field,
|
|
||||||
required double width,
|
|
||||||
}) = _FieldCellState;
|
|
||||||
|
|
||||||
factory FieldCellState.initial(GridFieldCellContext cellContext) =>
|
|
||||||
FieldCellState(
|
|
||||||
databaseId: cellContext.viewId,
|
|
||||||
field: cellContext.field,
|
|
||||||
width: cellContext.field.width.toDouble(),
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,779 +0,0 @@
|
|||||||
import 'dart:collection';
|
|
||||||
import 'package:app_flowy/plugins/grid/application/field/grid_listener.dart';
|
|
||||||
import 'package:app_flowy/plugins/grid/application/filter/filter_listener.dart';
|
|
||||||
import 'package:app_flowy/plugins/grid/application/filter/filter_service.dart';
|
|
||||||
import 'package:app_flowy/plugins/grid/application/grid_service.dart';
|
|
||||||
import 'package:app_flowy/plugins/grid/application/setting/setting_listener.dart';
|
|
||||||
import 'package:app_flowy/plugins/grid/application/setting/setting_service.dart';
|
|
||||||
import 'package:app_flowy/plugins/grid/application/sort/sort_listener.dart';
|
|
||||||
import 'package:app_flowy/plugins/grid/application/sort/sort_service.dart';
|
|
||||||
import 'package:app_flowy/plugins/grid/presentation/widgets/filter/filter_info.dart';
|
|
||||||
import 'package:app_flowy/plugins/grid/presentation/widgets/sort/sort_info.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-database/filter_changeset.pb.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-database/sort_entities.pb.dart';
|
|
||||||
import 'package:dartz/dartz.dart';
|
|
||||||
import 'package:appflowy_backend/log.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-database/field_entities.pb.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-database/group.pb.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-database/setting_entities.pb.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-database/util.pb.dart';
|
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import '../row/row_cache.dart';
|
|
||||||
|
|
||||||
class _GridFieldNotifier extends ChangeNotifier {
|
|
||||||
List<FieldInfo> _fieldInfos = [];
|
|
||||||
|
|
||||||
set fieldInfos(List<FieldInfo> fieldInfos) {
|
|
||||||
_fieldInfos = fieldInfos;
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
void notify() {
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
List<FieldInfo> get fieldInfos => _fieldInfos;
|
|
||||||
}
|
|
||||||
|
|
||||||
class _GridFilterNotifier extends ChangeNotifier {
|
|
||||||
List<FilterInfo> _filters = [];
|
|
||||||
|
|
||||||
set filters(List<FilterInfo> filters) {
|
|
||||||
_filters = filters;
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
void notify() {
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
List<FilterInfo> get filters => _filters;
|
|
||||||
}
|
|
||||||
|
|
||||||
class _GridSortNotifier extends ChangeNotifier {
|
|
||||||
List<SortInfo> _sorts = [];
|
|
||||||
|
|
||||||
set sorts(List<SortInfo> sorts) {
|
|
||||||
_sorts = sorts;
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
void notify() {
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
List<SortInfo> get sorts => _sorts;
|
|
||||||
}
|
|
||||||
|
|
||||||
typedef OnReceiveUpdateFields = void Function(List<FieldInfo>);
|
|
||||||
typedef OnReceiveFields = void Function(List<FieldInfo>);
|
|
||||||
typedef OnReceiveFilters = void Function(List<FilterInfo>);
|
|
||||||
typedef OnReceiveSorts = void Function(List<SortInfo>);
|
|
||||||
|
|
||||||
class GridFieldController {
|
|
||||||
final String databaseId;
|
|
||||||
// Listeners
|
|
||||||
final DatabaseFieldsListener _fieldListener;
|
|
||||||
final DatabaseSettingListener _settingListener;
|
|
||||||
final FiltersListener _filtersListener;
|
|
||||||
final SortsListener _sortsListener;
|
|
||||||
|
|
||||||
// FFI services
|
|
||||||
final DatabaseFFIService _gridFFIService;
|
|
||||||
final SettingFFIService _settingFFIService;
|
|
||||||
final FilterFFIService _filterFFIService;
|
|
||||||
final SortFFIService _sortFFIService;
|
|
||||||
|
|
||||||
// Field callbacks
|
|
||||||
final Map<OnReceiveFields, VoidCallback> _fieldCallbacks = {};
|
|
||||||
_GridFieldNotifier? _fieldNotifier = _GridFieldNotifier();
|
|
||||||
|
|
||||||
// Field updated callbacks
|
|
||||||
final Map<OnReceiveUpdateFields, void Function(List<FieldInfo>)>
|
|
||||||
_updatedFieldCallbacks = {};
|
|
||||||
|
|
||||||
// Group callbacks
|
|
||||||
final Map<String, GroupConfigurationPB> _groupConfigurationByFieldId = {};
|
|
||||||
|
|
||||||
// Filter callbacks
|
|
||||||
final Map<OnReceiveFilters, VoidCallback> _filterCallbacks = {};
|
|
||||||
_GridFilterNotifier? _filterNotifier = _GridFilterNotifier();
|
|
||||||
final Map<String, FilterPB> _filterPBByFieldId = {};
|
|
||||||
|
|
||||||
// Sort callbacks
|
|
||||||
final Map<OnReceiveSorts, VoidCallback> _sortCallbacks = {};
|
|
||||||
_GridSortNotifier? _sortNotifier = _GridSortNotifier();
|
|
||||||
final Map<String, SortPB> _sortPBByFieldId = {};
|
|
||||||
|
|
||||||
// Getters
|
|
||||||
List<FieldInfo> get fieldInfos => [..._fieldNotifier?.fieldInfos ?? []];
|
|
||||||
List<FilterInfo> get filterInfos => [..._filterNotifier?.filters ?? []];
|
|
||||||
List<SortInfo> get sortInfos => [..._sortNotifier?.sorts ?? []];
|
|
||||||
|
|
||||||
FieldInfo? getField(String fieldId) {
|
|
||||||
final fields = _fieldNotifier?.fieldInfos
|
|
||||||
.where((element) => element.id == fieldId)
|
|
||||||
.toList() ??
|
|
||||||
[];
|
|
||||||
if (fields.isEmpty) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
assert(fields.length == 1);
|
|
||||||
return fields.first;
|
|
||||||
}
|
|
||||||
|
|
||||||
FilterInfo? getFilter(String filterId) {
|
|
||||||
final filters = _filterNotifier?.filters
|
|
||||||
.where((element) => element.filter.id == filterId)
|
|
||||||
.toList() ??
|
|
||||||
[];
|
|
||||||
if (filters.isEmpty) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
assert(filters.length == 1);
|
|
||||||
return filters.first;
|
|
||||||
}
|
|
||||||
|
|
||||||
SortInfo? getSort(String sortId) {
|
|
||||||
final sorts = _sortNotifier?.sorts
|
|
||||||
.where((element) => element.sortId == sortId)
|
|
||||||
.toList() ??
|
|
||||||
[];
|
|
||||||
if (sorts.isEmpty) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
assert(sorts.length == 1);
|
|
||||||
return sorts.first;
|
|
||||||
}
|
|
||||||
|
|
||||||
GridFieldController({required this.databaseId})
|
|
||||||
: _fieldListener = DatabaseFieldsListener(databaseId: databaseId),
|
|
||||||
_settingListener = DatabaseSettingListener(databaseId: databaseId),
|
|
||||||
_filterFFIService = FilterFFIService(viewId: databaseId),
|
|
||||||
_filtersListener = FiltersListener(viewId: databaseId),
|
|
||||||
_gridFFIService = DatabaseFFIService(viewId: databaseId),
|
|
||||||
_sortFFIService = SortFFIService(viewId: databaseId),
|
|
||||||
_sortsListener = SortsListener(viewId: databaseId),
|
|
||||||
_settingFFIService = SettingFFIService(viewId: databaseId) {
|
|
||||||
//Listen on field's changes
|
|
||||||
_listenOnFieldChanges();
|
|
||||||
|
|
||||||
//Listen on setting changes
|
|
||||||
_listenOnSettingChanges();
|
|
||||||
|
|
||||||
//Listen on the fitler changes
|
|
||||||
_listenOnFilterChanges();
|
|
||||||
|
|
||||||
//Listen on the sort changes
|
|
||||||
_listenOnSortChanged();
|
|
||||||
|
|
||||||
_settingFFIService.getSetting().then((result) {
|
|
||||||
result.fold(
|
|
||||||
(setting) => _updateSetting(setting),
|
|
||||||
(err) => Log.error(err),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void _listenOnFilterChanges() {
|
|
||||||
//Listen on the fitler changes
|
|
||||||
|
|
||||||
deleteFilterFromChangeset(
|
|
||||||
List<FilterInfo> filters,
|
|
||||||
FilterChangesetNotificationPB changeset,
|
|
||||||
) {
|
|
||||||
final deleteFilterIds = changeset.deleteFilters.map((e) => e.id).toList();
|
|
||||||
if (deleteFilterIds.isNotEmpty) {
|
|
||||||
filters.retainWhere(
|
|
||||||
(element) => !deleteFilterIds.contains(element.filter.id),
|
|
||||||
);
|
|
||||||
|
|
||||||
_filterPBByFieldId
|
|
||||||
.removeWhere((key, value) => deleteFilterIds.contains(value.id));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
insertFilterFromChangeset(
|
|
||||||
List<FilterInfo> filters,
|
|
||||||
FilterChangesetNotificationPB changeset,
|
|
||||||
) {
|
|
||||||
for (final newFilter in changeset.insertFilters) {
|
|
||||||
final filterIndex =
|
|
||||||
filters.indexWhere((element) => element.filter.id == newFilter.id);
|
|
||||||
if (filterIndex == -1) {
|
|
||||||
final fieldInfo = _findFieldInfo(
|
|
||||||
fieldInfos: fieldInfos,
|
|
||||||
fieldId: newFilter.fieldId,
|
|
||||||
fieldType: newFilter.fieldType,
|
|
||||||
);
|
|
||||||
if (fieldInfo != null) {
|
|
||||||
_filterPBByFieldId[fieldInfo.id] = newFilter;
|
|
||||||
filters.add(FilterInfo(databaseId, newFilter, fieldInfo));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateFilterFromChangeset(
|
|
||||||
List<FilterInfo> filters,
|
|
||||||
FilterChangesetNotificationPB changeset,
|
|
||||||
) {
|
|
||||||
for (final updatedFilter in changeset.updateFilters) {
|
|
||||||
final filterIndex = filters.indexWhere(
|
|
||||||
(element) => element.filter.id == updatedFilter.filterId,
|
|
||||||
);
|
|
||||||
// Remove the old filter
|
|
||||||
if (filterIndex != -1) {
|
|
||||||
filters.removeAt(filterIndex);
|
|
||||||
_filterPBByFieldId
|
|
||||||
.removeWhere((key, value) => value.id == updatedFilter.filterId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Insert the filter if there is a fitler and its field info is
|
|
||||||
// not null
|
|
||||||
if (updatedFilter.hasFilter()) {
|
|
||||||
final fieldInfo = _findFieldInfo(
|
|
||||||
fieldInfos: fieldInfos,
|
|
||||||
fieldId: updatedFilter.filter.fieldId,
|
|
||||||
fieldType: updatedFilter.filter.fieldType,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (fieldInfo != null) {
|
|
||||||
// Insert the filter with the position: filterIndex, otherwise,
|
|
||||||
// append it to the end of the list.
|
|
||||||
final filterInfo =
|
|
||||||
FilterInfo(databaseId, updatedFilter.filter, fieldInfo);
|
|
||||||
if (filterIndex != -1) {
|
|
||||||
filters.insert(filterIndex, filterInfo);
|
|
||||||
} else {
|
|
||||||
filters.add(filterInfo);
|
|
||||||
}
|
|
||||||
_filterPBByFieldId[fieldInfo.id] = updatedFilter.filter;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_filtersListener.start(onFilterChanged: (result) {
|
|
||||||
result.fold(
|
|
||||||
(FilterChangesetNotificationPB changeset) {
|
|
||||||
final List<FilterInfo> filters = filterInfos;
|
|
||||||
// Deletes the filters
|
|
||||||
deleteFilterFromChangeset(filters, changeset);
|
|
||||||
|
|
||||||
// Inserts the new filter if it's not exist
|
|
||||||
insertFilterFromChangeset(filters, changeset);
|
|
||||||
|
|
||||||
updateFilterFromChangeset(filters, changeset);
|
|
||||||
|
|
||||||
_updateFieldInfos();
|
|
||||||
_filterNotifier?.filters = filters;
|
|
||||||
},
|
|
||||||
(err) => Log.error(err),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void _listenOnSortChanged() {
|
|
||||||
deleteSortFromChangeset(
|
|
||||||
List<SortInfo> newSortInfos,
|
|
||||||
SortChangesetNotificationPB changeset,
|
|
||||||
) {
|
|
||||||
final deleteSortIds = changeset.deleteSorts.map((e) => e.id).toList();
|
|
||||||
if (deleteSortIds.isNotEmpty) {
|
|
||||||
newSortInfos.retainWhere(
|
|
||||||
(element) => !deleteSortIds.contains(element.sortId),
|
|
||||||
);
|
|
||||||
|
|
||||||
_sortPBByFieldId
|
|
||||||
.removeWhere((key, value) => deleteSortIds.contains(value.id));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
insertSortFromChangeset(
|
|
||||||
List<SortInfo> newSortInfos,
|
|
||||||
SortChangesetNotificationPB changeset,
|
|
||||||
) {
|
|
||||||
for (final newSortPB in changeset.insertSorts) {
|
|
||||||
final sortIndex = newSortInfos
|
|
||||||
.indexWhere((element) => element.sortId == newSortPB.id);
|
|
||||||
if (sortIndex == -1) {
|
|
||||||
final fieldInfo = _findFieldInfo(
|
|
||||||
fieldInfos: fieldInfos,
|
|
||||||
fieldId: newSortPB.fieldId,
|
|
||||||
fieldType: newSortPB.fieldType,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (fieldInfo != null) {
|
|
||||||
_sortPBByFieldId[newSortPB.fieldId] = newSortPB;
|
|
||||||
newSortInfos.add(SortInfo(sortPB: newSortPB, fieldInfo: fieldInfo));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateSortFromChangeset(
|
|
||||||
List<SortInfo> newSortInfos,
|
|
||||||
SortChangesetNotificationPB changeset,
|
|
||||||
) {
|
|
||||||
for (final updatedSort in changeset.updateSorts) {
|
|
||||||
final sortIndex = newSortInfos.indexWhere(
|
|
||||||
(element) => element.sortId == updatedSort.id,
|
|
||||||
);
|
|
||||||
// Remove the old filter
|
|
||||||
if (sortIndex != -1) {
|
|
||||||
newSortInfos.removeAt(sortIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
final fieldInfo = _findFieldInfo(
|
|
||||||
fieldInfos: fieldInfos,
|
|
||||||
fieldId: updatedSort.fieldId,
|
|
||||||
fieldType: updatedSort.fieldType,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (fieldInfo != null) {
|
|
||||||
final newSortInfo = SortInfo(
|
|
||||||
sortPB: updatedSort,
|
|
||||||
fieldInfo: fieldInfo,
|
|
||||||
);
|
|
||||||
if (sortIndex != -1) {
|
|
||||||
newSortInfos.insert(sortIndex, newSortInfo);
|
|
||||||
} else {
|
|
||||||
newSortInfos.add(newSortInfo);
|
|
||||||
}
|
|
||||||
_sortPBByFieldId[updatedSort.fieldId] = updatedSort;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_sortsListener.start(onSortChanged: (result) {
|
|
||||||
result.fold(
|
|
||||||
(SortChangesetNotificationPB changeset) {
|
|
||||||
final List<SortInfo> newSortInfos = sortInfos;
|
|
||||||
deleteSortFromChangeset(newSortInfos, changeset);
|
|
||||||
insertSortFromChangeset(newSortInfos, changeset);
|
|
||||||
updateSortFromChangeset(newSortInfos, changeset);
|
|
||||||
|
|
||||||
_updateFieldInfos();
|
|
||||||
_sortNotifier?.sorts = newSortInfos;
|
|
||||||
},
|
|
||||||
(err) => Log.error(err),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void _listenOnSettingChanges() {
|
|
||||||
//Listen on setting changes
|
|
||||||
_settingListener.start(onSettingUpdated: (result) {
|
|
||||||
result.fold(
|
|
||||||
(setting) => _updateSetting(setting),
|
|
||||||
(r) => Log.error(r),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void _listenOnFieldChanges() {
|
|
||||||
//Listen on field's changes
|
|
||||||
_fieldListener.start(onFieldsChanged: (result) {
|
|
||||||
result.fold(
|
|
||||||
(changeset) {
|
|
||||||
_deleteFields(changeset.deletedFields);
|
|
||||||
_insertFields(changeset.insertedFields);
|
|
||||||
|
|
||||||
final updateFields = _updateFields(changeset.updatedFields);
|
|
||||||
for (final listener in _updatedFieldCallbacks.values) {
|
|
||||||
listener(updateFields);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
(err) => Log.error(err),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void _updateSetting(DatabaseViewSettingPB setting) {
|
|
||||||
_groupConfigurationByFieldId.clear();
|
|
||||||
for (final configuration in setting.groupConfigurations.items) {
|
|
||||||
_groupConfigurationByFieldId[configuration.fieldId] = configuration;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (final filter in setting.filters.items) {
|
|
||||||
_filterPBByFieldId[filter.fieldId] = filter;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (final sort in setting.sorts.items) {
|
|
||||||
_sortPBByFieldId[sort.fieldId] = sort;
|
|
||||||
}
|
|
||||||
|
|
||||||
_updateFieldInfos();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _updateFieldInfos() {
|
|
||||||
if (_fieldNotifier != null) {
|
|
||||||
for (var field in _fieldNotifier!.fieldInfos) {
|
|
||||||
field._isGroupField = _groupConfigurationByFieldId[field.id] != null;
|
|
||||||
field._hasFilter = _filterPBByFieldId[field.id] != null;
|
|
||||||
field._hasSort = _sortPBByFieldId[field.id] != null;
|
|
||||||
}
|
|
||||||
_fieldNotifier?.notify();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> dispose() async {
|
|
||||||
await _fieldListener.stop();
|
|
||||||
await _filtersListener.stop();
|
|
||||||
await _settingListener.stop();
|
|
||||||
await _sortsListener.stop();
|
|
||||||
|
|
||||||
for (final callback in _fieldCallbacks.values) {
|
|
||||||
_fieldNotifier?.removeListener(callback);
|
|
||||||
}
|
|
||||||
_fieldNotifier?.dispose();
|
|
||||||
_fieldNotifier = null;
|
|
||||||
|
|
||||||
for (final callback in _filterCallbacks.values) {
|
|
||||||
_filterNotifier?.removeListener(callback);
|
|
||||||
}
|
|
||||||
for (final callback in _sortCallbacks.values) {
|
|
||||||
_sortNotifier?.removeListener(callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
_filterNotifier?.dispose();
|
|
||||||
_filterNotifier = null;
|
|
||||||
|
|
||||||
_sortNotifier?.dispose();
|
|
||||||
_sortNotifier = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<Either<Unit, FlowyError>> loadFields({
|
|
||||||
required List<FieldIdPB> fieldIds,
|
|
||||||
}) async {
|
|
||||||
final result = await _gridFFIService.getFields(fieldIds: fieldIds);
|
|
||||||
return Future(
|
|
||||||
() => result.fold(
|
|
||||||
(newFields) {
|
|
||||||
_fieldNotifier?.fieldInfos =
|
|
||||||
newFields.map((field) => FieldInfo(field: field)).toList();
|
|
||||||
_loadFilters();
|
|
||||||
_loadSorts();
|
|
||||||
_updateFieldInfos();
|
|
||||||
return left(unit);
|
|
||||||
},
|
|
||||||
(err) => right(err),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<Either<Unit, FlowyError>> _loadFilters() async {
|
|
||||||
return _filterFFIService.getAllFilters().then((result) {
|
|
||||||
return result.fold(
|
|
||||||
(filterPBs) {
|
|
||||||
final List<FilterInfo> filters = [];
|
|
||||||
for (final filterPB in filterPBs) {
|
|
||||||
final fieldInfo = _findFieldInfo(
|
|
||||||
fieldInfos: fieldInfos,
|
|
||||||
fieldId: filterPB.fieldId,
|
|
||||||
fieldType: filterPB.fieldType,
|
|
||||||
);
|
|
||||||
if (fieldInfo != null) {
|
|
||||||
final filterInfo = FilterInfo(databaseId, filterPB, fieldInfo);
|
|
||||||
filters.add(filterInfo);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_filterNotifier?.filters = filters;
|
|
||||||
return left(unit);
|
|
||||||
},
|
|
||||||
(err) => right(err),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<Either<Unit, FlowyError>> _loadSorts() async {
|
|
||||||
return _sortFFIService.getAllSorts().then((result) {
|
|
||||||
return result.fold(
|
|
||||||
(sortPBs) {
|
|
||||||
final List<SortInfo> sortInfos = [];
|
|
||||||
for (final sortPB in sortPBs) {
|
|
||||||
final fieldInfo = _findFieldInfo(
|
|
||||||
fieldInfos: fieldInfos,
|
|
||||||
fieldId: sortPB.fieldId,
|
|
||||||
fieldType: sortPB.fieldType,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (fieldInfo != null) {
|
|
||||||
final sortInfo = SortInfo(sortPB: sortPB, fieldInfo: fieldInfo);
|
|
||||||
sortInfos.add(sortInfo);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_updateFieldInfos();
|
|
||||||
_sortNotifier?.sorts = sortInfos;
|
|
||||||
return left(unit);
|
|
||||||
},
|
|
||||||
(err) => right(err),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void addListener({
|
|
||||||
OnReceiveFields? onFields,
|
|
||||||
OnReceiveUpdateFields? onFieldsUpdated,
|
|
||||||
OnReceiveFilters? onFilters,
|
|
||||||
OnReceiveSorts? onSorts,
|
|
||||||
bool Function()? listenWhen,
|
|
||||||
}) {
|
|
||||||
if (onFieldsUpdated != null) {
|
|
||||||
callback(List<FieldInfo> updateFields) {
|
|
||||||
if (listenWhen != null && listenWhen() == false) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
onFieldsUpdated(updateFields);
|
|
||||||
}
|
|
||||||
|
|
||||||
_updatedFieldCallbacks[onFieldsUpdated] = callback;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (onFields != null) {
|
|
||||||
callback() {
|
|
||||||
if (listenWhen != null && listenWhen() == false) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
onFields(fieldInfos);
|
|
||||||
}
|
|
||||||
|
|
||||||
_fieldCallbacks[onFields] = callback;
|
|
||||||
_fieldNotifier?.addListener(callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (onFilters != null) {
|
|
||||||
callback() {
|
|
||||||
if (listenWhen != null && listenWhen() == false) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
onFilters(filterInfos);
|
|
||||||
}
|
|
||||||
|
|
||||||
_filterCallbacks[onFilters] = callback;
|
|
||||||
_filterNotifier?.addListener(callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (onSorts != null) {
|
|
||||||
callback() {
|
|
||||||
if (listenWhen != null && listenWhen() == false) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
onSorts(sortInfos);
|
|
||||||
}
|
|
||||||
|
|
||||||
_sortCallbacks[onSorts] = callback;
|
|
||||||
_sortNotifier?.addListener(callback);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void removeListener({
|
|
||||||
OnReceiveFields? onFieldsListener,
|
|
||||||
OnReceiveSorts? onSortsListener,
|
|
||||||
OnReceiveFilters? onFiltersListener,
|
|
||||||
OnReceiveUpdateFields? onChangesetListener,
|
|
||||||
}) {
|
|
||||||
if (onFieldsListener != null) {
|
|
||||||
final callback = _fieldCallbacks.remove(onFieldsListener);
|
|
||||||
if (callback != null) {
|
|
||||||
_fieldNotifier?.removeListener(callback);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (onFiltersListener != null) {
|
|
||||||
final callback = _filterCallbacks.remove(onFiltersListener);
|
|
||||||
if (callback != null) {
|
|
||||||
_filterNotifier?.removeListener(callback);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (onSortsListener != null) {
|
|
||||||
final callback = _sortCallbacks.remove(onSortsListener);
|
|
||||||
if (callback != null) {
|
|
||||||
_sortNotifier?.removeListener(callback);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _deleteFields(List<FieldIdPB> deletedFields) {
|
|
||||||
if (deletedFields.isEmpty) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final List<FieldInfo> newFields = fieldInfos;
|
|
||||||
final Map<String, FieldIdPB> deletedFieldMap = {
|
|
||||||
for (var fieldOrder in deletedFields) fieldOrder.fieldId: fieldOrder
|
|
||||||
};
|
|
||||||
|
|
||||||
newFields.retainWhere((field) => (deletedFieldMap[field.id] == null));
|
|
||||||
_fieldNotifier?.fieldInfos = newFields;
|
|
||||||
}
|
|
||||||
|
|
||||||
void _insertFields(List<IndexFieldPB> insertedFields) {
|
|
||||||
if (insertedFields.isEmpty) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final List<FieldInfo> newFieldInfos = fieldInfos;
|
|
||||||
for (final indexField in insertedFields) {
|
|
||||||
final fieldInfo = FieldInfo(field: indexField.field_1);
|
|
||||||
if (newFieldInfos.length > indexField.index) {
|
|
||||||
newFieldInfos.insert(indexField.index, fieldInfo);
|
|
||||||
} else {
|
|
||||||
newFieldInfos.add(fieldInfo);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_fieldNotifier?.fieldInfos = newFieldInfos;
|
|
||||||
}
|
|
||||||
|
|
||||||
List<FieldInfo> _updateFields(List<FieldPB> updatedFieldPBs) {
|
|
||||||
if (updatedFieldPBs.isEmpty) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
final List<FieldInfo> newFields = fieldInfos;
|
|
||||||
final List<FieldInfo> updatedFields = [];
|
|
||||||
for (final updatedFieldPB in updatedFieldPBs) {
|
|
||||||
final index =
|
|
||||||
newFields.indexWhere((field) => field.id == updatedFieldPB.id);
|
|
||||||
if (index != -1) {
|
|
||||||
newFields.removeAt(index);
|
|
||||||
final fieldInfo = FieldInfo(field: updatedFieldPB);
|
|
||||||
newFields.insert(index, fieldInfo);
|
|
||||||
updatedFields.add(fieldInfo);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (updatedFields.isNotEmpty) {
|
|
||||||
_fieldNotifier?.fieldInfos = newFields;
|
|
||||||
}
|
|
||||||
return updatedFields;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class GridRowFieldNotifierImpl extends RowChangesetNotifierForward
|
|
||||||
with RowCacheDelegate {
|
|
||||||
final GridFieldController _cache;
|
|
||||||
OnReceiveUpdateFields? _onChangesetFn;
|
|
||||||
OnReceiveFields? _onFieldFn;
|
|
||||||
GridRowFieldNotifierImpl(GridFieldController cache) : _cache = cache;
|
|
||||||
|
|
||||||
@override
|
|
||||||
UnmodifiableListView<FieldInfo> get fields =>
|
|
||||||
UnmodifiableListView(_cache.fieldInfos);
|
|
||||||
|
|
||||||
@override
|
|
||||||
void onRowFieldsChanged(VoidCallback callback) {
|
|
||||||
_onFieldFn = (_) => callback();
|
|
||||||
_cache.addListener(onFields: _onFieldFn);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void onRowFieldChanged(void Function(FieldInfo) callback) {
|
|
||||||
_onChangesetFn = (List<FieldInfo> fieldInfos) {
|
|
||||||
for (final updatedField in fieldInfos) {
|
|
||||||
callback(updatedField);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
_cache.addListener(onFieldsUpdated: _onChangesetFn);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void onRowDispose() {
|
|
||||||
if (_onFieldFn != null) {
|
|
||||||
_cache.removeListener(onFieldsListener: _onFieldFn!);
|
|
||||||
_onFieldFn = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_onChangesetFn != null) {
|
|
||||||
_cache.removeListener(onChangesetListener: _onChangesetFn!);
|
|
||||||
_onChangesetFn = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
FieldInfo? _findFieldInfo({
|
|
||||||
required List<FieldInfo> fieldInfos,
|
|
||||||
required String fieldId,
|
|
||||||
required FieldType fieldType,
|
|
||||||
}) {
|
|
||||||
final fieldIndex = fieldInfos.indexWhere((element) {
|
|
||||||
return element.id == fieldId && element.fieldType == fieldType;
|
|
||||||
});
|
|
||||||
if (fieldIndex != -1) {
|
|
||||||
return fieldInfos[fieldIndex];
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class FieldInfo {
|
|
||||||
final FieldPB _field;
|
|
||||||
bool _isGroupField = false;
|
|
||||||
|
|
||||||
bool _hasFilter = false;
|
|
||||||
|
|
||||||
bool _hasSort = false;
|
|
||||||
|
|
||||||
String get id => _field.id;
|
|
||||||
|
|
||||||
FieldType get fieldType => _field.fieldType;
|
|
||||||
|
|
||||||
bool get visibility => _field.visibility;
|
|
||||||
|
|
||||||
double get width => _field.width.toDouble();
|
|
||||||
|
|
||||||
bool get isPrimary => _field.isPrimary;
|
|
||||||
|
|
||||||
String get name => _field.name;
|
|
||||||
|
|
||||||
FieldPB get field => _field;
|
|
||||||
|
|
||||||
bool get isGroupField => _isGroupField;
|
|
||||||
|
|
||||||
bool get hasFilter => _hasFilter;
|
|
||||||
|
|
||||||
bool get canBeGroup {
|
|
||||||
switch (_field.fieldType) {
|
|
||||||
case FieldType.URL:
|
|
||||||
case FieldType.Checkbox:
|
|
||||||
case FieldType.MultiSelect:
|
|
||||||
case FieldType.SingleSelect:
|
|
||||||
return true;
|
|
||||||
default:
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool get canCreateFilter {
|
|
||||||
if (hasFilter) return false;
|
|
||||||
|
|
||||||
switch (_field.fieldType) {
|
|
||||||
case FieldType.Checkbox:
|
|
||||||
case FieldType.MultiSelect:
|
|
||||||
case FieldType.RichText:
|
|
||||||
case FieldType.SingleSelect:
|
|
||||||
case FieldType.Checklist:
|
|
||||||
return true;
|
|
||||||
default:
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool get canCreateSort {
|
|
||||||
if (_hasSort) return false;
|
|
||||||
|
|
||||||
switch (_field.fieldType) {
|
|
||||||
case FieldType.RichText:
|
|
||||||
case FieldType.Checkbox:
|
|
||||||
case FieldType.Number:
|
|
||||||
return true;
|
|
||||||
default:
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
FieldInfo({required FieldPB field}) : _field = field;
|
|
||||||
}
|
|
@ -1,108 +0,0 @@
|
|||||||
import 'package:appflowy_backend/protobuf/flowy-database/field_entities.pb.dart';
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
||||||
import 'dart:async';
|
|
||||||
import 'package:dartz/dartz.dart';
|
|
||||||
import 'field_service.dart';
|
|
||||||
import 'type_option/type_option_context.dart';
|
|
||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
|
||||||
import 'type_option/type_option_data_controller.dart';
|
|
||||||
|
|
||||||
part 'field_editor_bloc.freezed.dart';
|
|
||||||
|
|
||||||
class FieldEditorBloc extends Bloc<FieldEditorEvent, FieldEditorState> {
|
|
||||||
final TypeOptionDataController dataController;
|
|
||||||
|
|
||||||
FieldEditorBloc({
|
|
||||||
required String databaseId,
|
|
||||||
required String fieldName,
|
|
||||||
required bool isGroupField,
|
|
||||||
required IFieldTypeOptionLoader loader,
|
|
||||||
}) : dataController =
|
|
||||||
TypeOptionDataController(databaseId: databaseId, loader: loader),
|
|
||||||
super(FieldEditorState.initial(databaseId, fieldName, isGroupField)) {
|
|
||||||
on<FieldEditorEvent>(
|
|
||||||
(event, emit) async {
|
|
||||||
await event.when(
|
|
||||||
initial: () async {
|
|
||||||
dataController.addFieldListener((field) {
|
|
||||||
if (!isClosed) {
|
|
||||||
add(FieldEditorEvent.didReceiveFieldChanged(field));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
await dataController.loadTypeOptionData();
|
|
||||||
},
|
|
||||||
updateName: (name) {
|
|
||||||
if (state.name != name) {
|
|
||||||
dataController.fieldName = name;
|
|
||||||
emit(state.copyWith(name: name));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
didReceiveFieldChanged: (FieldPB field) {
|
|
||||||
emit(state.copyWith(
|
|
||||||
field: Some(field),
|
|
||||||
name: field.name,
|
|
||||||
canDelete: field.isPrimary,
|
|
||||||
));
|
|
||||||
},
|
|
||||||
deleteField: () {
|
|
||||||
state.field.fold(
|
|
||||||
() => null,
|
|
||||||
(field) {
|
|
||||||
final fieldService = FieldService(
|
|
||||||
viewId: databaseId,
|
|
||||||
fieldId: field.id,
|
|
||||||
);
|
|
||||||
fieldService.deleteField();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
switchToField: (FieldType fieldType) async {
|
|
||||||
await dataController.switchToField(fieldType);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> close() async {
|
|
||||||
return super.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@freezed
|
|
||||||
class FieldEditorEvent with _$FieldEditorEvent {
|
|
||||||
const factory FieldEditorEvent.initial() = _InitialField;
|
|
||||||
const factory FieldEditorEvent.updateName(String name) = _UpdateName;
|
|
||||||
const factory FieldEditorEvent.deleteField() = _DeleteField;
|
|
||||||
const factory FieldEditorEvent.switchToField(FieldType fieldType) =
|
|
||||||
_SwitchToField;
|
|
||||||
const factory FieldEditorEvent.didReceiveFieldChanged(FieldPB field) =
|
|
||||||
_DidReceiveFieldChanged;
|
|
||||||
}
|
|
||||||
|
|
||||||
@freezed
|
|
||||||
class FieldEditorState with _$FieldEditorState {
|
|
||||||
const factory FieldEditorState({
|
|
||||||
required String databaseId,
|
|
||||||
required String errorText,
|
|
||||||
required String name,
|
|
||||||
required Option<FieldPB> field,
|
|
||||||
required bool canDelete,
|
|
||||||
required bool isGroupField,
|
|
||||||
}) = _FieldEditorState;
|
|
||||||
|
|
||||||
factory FieldEditorState.initial(
|
|
||||||
String databaseId,
|
|
||||||
String fieldName,
|
|
||||||
bool isGroupField,
|
|
||||||
) =>
|
|
||||||
FieldEditorState(
|
|
||||||
databaseId: databaseId,
|
|
||||||
errorText: '',
|
|
||||||
field: none(),
|
|
||||||
canDelete: false,
|
|
||||||
name: fieldName,
|
|
||||||
isGroupField: isGroupField,
|
|
||||||
);
|
|
||||||
}
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user