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/rust-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"
|
||||
- "release/*"
|
||||
paths:
|
||||
- "frontend/app_flowy/packages/appflowy_editor/**"
|
||||
- "frontend/appflowy_flutter/packages/appflowy_editor/**"
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
@ -35,7 +35,7 @@ jobs:
|
||||
cache: true
|
||||
|
||||
- name: Run FlowyEditor tests
|
||||
working-directory: frontend/app_flowy/packages/appflowy_editor
|
||||
working-directory: frontend/appflowy_flutter/packages/appflowy_editor
|
||||
run: |
|
||||
flutter pub get
|
||||
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
|
||||
|
||||
- name: Flutter Analyzer
|
||||
working-directory: frontend/app_flowy
|
||||
working-directory: frontend/appflowy_flutter
|
||||
run: flutter analyze
|
||||
|
||||
- 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"
|
||||
- "release/*"
|
||||
paths:
|
||||
- "frontend/app_flowy/**"
|
||||
- "frontend/appflowy_flutter/**"
|
||||
|
||||
pull_request:
|
||||
branches:
|
||||
- "main"
|
||||
- "release/*"
|
||||
paths:
|
||||
- "frontend/app_flowy/**"
|
||||
- "frontend/appflowy_flutter/**"
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
@ -100,14 +100,14 @@ jobs:
|
||||
shell: bash
|
||||
|
||||
- name: Flutter Code Generation
|
||||
working-directory: frontend/app_flowy
|
||||
working-directory: frontend/appflowy_flutter
|
||||
run: |
|
||||
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 build_runner build --delete-conflicting-outputs
|
||||
|
||||
- name: Run AppFlowy tests
|
||||
working-directory: frontend/app_flowy
|
||||
working-directory: frontend/appflowy_flutter
|
||||
run: |
|
||||
if [ "$RUNNER_OS" == "Linux" ]; then
|
||||
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 }})
|
||||
needs: create-release
|
||||
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_INSTALLER_NAME: AppFlowy_${{ github.ref_name }}_windows-x86_64
|
||||
runs-on: ${{ matrix.job.os }}
|
||||
@ -129,7 +129,7 @@ jobs:
|
||||
runs-on: ${{ matrix.job.os }}
|
||||
needs: create-release
|
||||
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_DMG_NAME: AppFlowy_${{ github.ref_name }}_macos-x86_64
|
||||
strategy:
|
||||
@ -217,7 +217,7 @@ jobs:
|
||||
runs-on: ${{ matrix.job.os }}
|
||||
needs: create-release
|
||||
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_PACKAGE_NAME: AppFlowy_${{ github.ref_name }}_${{ matrix.job.os }}.deb
|
||||
# PKG_CONFIG_SYSROOT_DIR: /
|
||||
|
2
.github/workflows/translation_notify.yml
vendored
2
.github/workflows/translation_notify.yml
vendored
@ -3,7 +3,7 @@ on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
paths:
|
||||
- "frontend/app_flowy/assets/translations/en.json"
|
||||
- "frontend/appflowy_flutter/assets/translations/en.json"
|
||||
|
||||
jobs:
|
||||
Discord-Notify:
|
||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -29,7 +29,7 @@ node_modules
|
||||
|
||||
# Commit the highest level pubspec.lock, but ignore the others
|
||||
pubspec.lock
|
||||
!frontend/app_flowy/pubspec.lock
|
||||
!frontend/appflowy_flutter/pubspec.lock
|
||||
|
||||
# ignore tool used for commit linting
|
||||
.githooks/gitlint
|
||||
|
@ -10,7 +10,7 @@
|
||||
<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="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="flutter_package_lib" value="${flutter_lib}/flowy_sdk/lib" />
|
||||
</envs>
|
||||
|
@ -1,6 +1,6 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<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="channel" value="DEFAULT" />
|
||||
<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": "debug"
|
||||
},
|
||||
"cwd": "${workspaceRoot}/app_flowy"
|
||||
"cwd": "${workspaceRoot}/appflowy_flutter"
|
||||
},
|
||||
{
|
||||
// This task only builds the Dart code of AppFlowy.
|
||||
@ -26,7 +26,7 @@
|
||||
"env": {
|
||||
"RUST_LOG": "debug"
|
||||
},
|
||||
"cwd": "${workspaceRoot}/app_flowy"
|
||||
"cwd": "${workspaceRoot}/appflowy_flutter"
|
||||
},
|
||||
{
|
||||
// This task builds will:
|
||||
@ -41,7 +41,7 @@
|
||||
"env": {
|
||||
"RUST_LOG": "trace"
|
||||
},
|
||||
"cwd": "${workspaceRoot}/app_flowy"
|
||||
"cwd": "${workspaceRoot}/appflowy_flutter"
|
||||
},
|
||||
{
|
||||
"name": "AF-desktop: Debug Rust",
|
||||
@ -55,7 +55,7 @@
|
||||
// "program": "./lib/main.dart",
|
||||
// "type": "dart",
|
||||
// "flutterMode": "profile",
|
||||
// "cwd": "${workspaceRoot}/app_flowy"
|
||||
// "cwd": "${workspaceRoot}/appflowy_flutter"
|
||||
// },
|
||||
{
|
||||
// This task builds the Rust and Dart code of AppFlowy for android.
|
||||
@ -67,7 +67,7 @@
|
||||
"env": {
|
||||
"RUST_LOG": "info"
|
||||
},
|
||||
"cwd": "${workspaceRoot}/app_flowy"
|
||||
"cwd": "${workspaceRoot}/appflowy_flutter"
|
||||
},
|
||||
{
|
||||
// This task builds will:
|
||||
@ -82,7 +82,7 @@
|
||||
"env": {
|
||||
"RUST_LOG": "info"
|
||||
},
|
||||
"cwd": "${workspaceRoot}/app_flowy"
|
||||
"cwd": "${workspaceRoot}/appflowy_flutter"
|
||||
},
|
||||
{
|
||||
// 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",
|
||||
"command": "flutter clean",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/app_flowy"
|
||||
"cwd": "${workspaceFolder}/appflowy_flutter"
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -104,7 +104,7 @@
|
||||
"type": "shell",
|
||||
"command": "flutter pub get",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/app_flowy"
|
||||
"cwd": "${workspaceFolder}/appflowy_flutter"
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -112,7 +112,7 @@
|
||||
"type": "shell",
|
||||
"command": "flutter packages pub get",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/app_flowy"
|
||||
"cwd": "${workspaceFolder}/appflowy_flutter"
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -120,7 +120,7 @@
|
||||
"type": "shell",
|
||||
"command": "flutter pub run build_runner build --delete-conflicting-outputs",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/app_flowy"
|
||||
"cwd": "${workspaceFolder}/appflowy_flutter"
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -163,7 +163,7 @@
|
||||
],
|
||||
"group": "build",
|
||||
"problemMatcher": [],
|
||||
"detail": "app_flowy"
|
||||
"detail": "appflowy_flutter"
|
||||
},
|
||||
{
|
||||
"label": "AF: Tauri UI Dev",
|
||||
|
@ -43,7 +43,7 @@ PRODUCT_NAME = "AppFlowy"
|
||||
CRATE_TYPE = "staticlib"
|
||||
LIB_EXT = "a"
|
||||
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"
|
||||
# Test default config
|
||||
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