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:
Nathan.fooo 2023-02-26 16:27:17 +08:00 committed by GitHub
parent 6877607c5e
commit 61fd608200
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2213 changed files with 43935 additions and 45507 deletions

View File

@ -1,4 +1,4 @@
frontend/app_flowy/
frontend/appflowy_flutter/
frontend/scripts/
frontend/rust-lib/target
shared-lib/target/

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +0,0 @@
package com.example.app_flowy
import io.flutter.embedding.android.FlutterActivity
class MainActivity: FlutterActivity() {
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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