Forráskód Böngészése

refactor: add plugins

appflowy 3 éve
szülő
commit
8747457836
57 módosított fájl, 966 hozzáadás és 638 törlés
  1. 60 0
      frontend/app_flowy/lib/plugin/plugin.dart
  2. 1 0
      frontend/app_flowy/lib/plugin/src/runner.dart
  3. 40 0
      frontend/app_flowy/lib/plugin/src/sandbox.dart
  4. 3 0
      frontend/app_flowy/lib/startup/startup.dart
  5. 39 0
      frontend/app_flowy/lib/startup/tasks/load_plugin.dart
  6. 2 1
      frontend/app_flowy/lib/startup/tasks/prelude.dart
  7. 0 0
      frontend/app_flowy/lib/startup/tasks/rust_sdk.dart
  8. 3 2
      frontend/app_flowy/lib/workspace/application/app/app_bloc.dart
  9. 6 6
      frontend/app_flowy/lib/workspace/application/menu/menu_bloc.dart
  10. 60 71
      frontend/app_flowy/lib/workspace/application/menu/menu_bloc.freezed.dart
  11. 0 1
      frontend/app_flowy/lib/workspace/application/view/view_bloc.dart
  12. 0 27
      frontend/app_flowy/lib/workspace/domain/image.dart
  13. 20 33
      frontend/app_flowy/lib/workspace/domain/page_stack/page_stack.dart
  14. 22 32
      frontend/app_flowy/lib/workspace/domain/view_ext.dart
  15. 3 2
      frontend/app_flowy/lib/workspace/infrastructure/repos/app_repo.dart
  16. 3 1
      frontend/app_flowy/lib/workspace/presentation/home/home_screen.dart
  17. 3 3
      frontend/app_flowy/lib/workspace/presentation/home/navigation.dart
  18. 43 13
      frontend/app_flowy/lib/workspace/presentation/stack_page/blank/blank_page.dart
  19. 46 15
      frontend/app_flowy/lib/workspace/presentation/stack_page/doc/doc_stack_page.dart
  20. 0 11
      frontend/app_flowy/lib/workspace/presentation/stack_page/home_stack.dart
  21. 38 10
      frontend/app_flowy/lib/workspace/presentation/stack_page/trash/trash_page.dart
  22. 1 28
      frontend/app_flowy/lib/workspace/presentation/widgets/home_top_bar.dart
  23. 2 2
      frontend/app_flowy/lib/workspace/presentation/widgets/menu/menu.dart
  24. 25 15
      frontend/app_flowy/lib/workspace/presentation/widgets/menu/widget/app/header/add_button.dart
  25. 6 4
      frontend/app_flowy/lib/workspace/presentation/widgets/menu/widget/app/header/header.dart
  26. 2 2
      frontend/app_flowy/lib/workspace/presentation/widgets/menu/widget/app/section/item.dart
  27. 1 1
      frontend/app_flowy/lib/workspace/presentation/widgets/menu/widget/app/section/section.dart
  28. 3 2
      frontend/app_flowy/lib/workspace/presentation/widgets/menu/widget/menu_trash.dart
  29. 82 40
      frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-folder-data-model/view.pb.dart
  30. 7 7
      frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-folder-data-model/view.pbenum.dart
  31. 16 13
      frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-folder-data-model/view.pbjson.dart
  32. 3 3
      frontend/rust-lib/flowy-document/src/queue.rs
  33. 11 14
      frontend/rust-lib/flowy-document/src/web_socket.rs
  34. 2 2
      frontend/rust-lib/flowy-folder/src/services/folder_editor.rs
  35. 23 22
      frontend/rust-lib/flowy-folder/src/services/persistence/version_1/view_sql.rs
  36. 9 9
      frontend/rust-lib/flowy-folder/src/services/view/controller.rs
  37. 16 12
      frontend/rust-lib/flowy-folder/src/services/web_socket.rs
  38. 4 3
      frontend/rust-lib/flowy-folder/tests/workspace/helper.rs
  39. 3 3
      frontend/rust-lib/flowy-folder/tests/workspace/script.rs
  40. 3 1
      frontend/rust-lib/flowy-net/src/local_server/server.rs
  41. 7 1
      frontend/rust-lib/flowy-sync/src/conflict_resolve.rs
  42. 2 1
      frontend/rust-lib/flowy-test/src/helper.rs
  43. 4 4
      shared-lib/flowy-collaboration/src/client_folder/builder.rs
  44. 8 8
      shared-lib/flowy-collaboration/src/client_folder/folder_pad.rs
  45. 2 2
      shared-lib/flowy-collaboration/src/entities/folder_info.rs
  46. 2 2
      shared-lib/flowy-collaboration/src/server_folder/folder_manager.rs
  47. 5 8
      shared-lib/flowy-collaboration/src/server_folder/folder_pad.rs
  48. 32 43
      shared-lib/flowy-folder-data-model/src/entities/view.rs
  49. 267 138
      shared-lib/flowy-folder-data-model/src/protobuf/model/view.rs
  50. 9 6
      shared-lib/flowy-folder-data-model/src/protobuf/proto/view.proto
  51. 5 3
      shared-lib/flowy-folder-data-model/src/user_default.rs
  52. 2 2
      shared-lib/lib-ot/src/core/delta/builder.rs
  53. 1 1
      shared-lib/lib-ot/src/core/delta/delta.rs
  54. 2 2
      shared-lib/lib-ot/src/core/operation/builder.rs
  55. 4 4
      shared-lib/lib-ot/src/core/operation/operation.rs
  56. 2 1
      shared-lib/lib-ot/src/rich_text/attributes.rs
  57. 1 1
      shared-lib/lib-ot/src/rich_text/attributes_serde.rs

+ 60 - 0
frontend/app_flowy/lib/plugin/plugin.dart

@@ -0,0 +1,60 @@
+library flowy_plugin;
+
+import 'package:app_flowy/plugin/plugin.dart';
+import 'package:app_flowy/startup/startup.dart';
+import 'package:app_flowy/workspace/domain/page_stack/page_stack.dart';
+import 'package:flowy_sdk/protobuf/flowy-folder-data-model/view.pb.dart';
+import 'package:flutter/widgets.dart';
+
+export "./src/sandbox.dart";
+
+typedef PluginType = String;
+
+typedef PluginDataType = ViewDataType;
+
+abstract class Plugin {
+  PluginType get pluginType;
+
+  String get pluginId;
+
+  bool get enable;
+
+  void dispose();
+
+  PluginDisplay get display;
+}
+
+abstract class PluginBuilder {
+  Plugin build(dynamic data);
+
+  String get pluginName;
+
+  PluginType get pluginType;
+
+  ViewDataType get dataType;
+}
+
+abstract class PluginDisplay with NavigationItem {
+  @override
+  Widget get leftBarItem;
+
+  @override
+  Widget? get rightBarItem;
+
+  List<NavigationItem> get navigationItems;
+
+  Widget buildWidget();
+}
+
+void registerPlugin({required PluginBuilder builder}) {
+  getIt<PluginSandbox>().registerPlugin(builder.pluginType, builder);
+}
+
+Plugin makePlugin({required String pluginType, dynamic data}) {
+  final plugin = getIt<PluginSandbox>().buildPlugin(pluginType, data);
+  return plugin;
+}
+
+enum FlowyPluginException {
+  invalidData,
+}

+ 1 - 0
frontend/app_flowy/lib/plugin/src/runner.dart

@@ -0,0 +1 @@
+class PluginRunner {}

+ 40 - 0
frontend/app_flowy/lib/plugin/src/sandbox.dart

@@ -0,0 +1,40 @@
+import 'dart:collection';
+
+import 'package:flutter/services.dart';
+
+import '../plugin.dart';
+import 'runner.dart';
+
+class PluginSandbox {
+  final LinkedHashMap<String, PluginBuilder> _pluginMap = LinkedHashMap();
+  late PluginRunner pluginRunner;
+
+  PluginSandbox() {
+    pluginRunner = PluginRunner();
+  }
+
+  int indexOf(String pluginType) {
+    final index = _pluginMap.keys.toList().indexWhere((ty) => ty == pluginType);
+    if (index == -1) {
+      throw PlatformException(code: '-1', message: "Can't find the flowy plugin type: $pluginType");
+    }
+    return index;
+  }
+
+  Plugin buildPlugin(String pluginType, dynamic data) {
+    final index = indexOf(pluginType);
+    final plugin = _pluginMap[index]!.build(data);
+    return plugin;
+  }
+
+  void registerPlugin(String pluginType, PluginBuilder builder) {
+    if (_pluginMap.containsKey(pluginType)) {
+      throw PlatformException(code: '-1', message: "$pluginType was registered before");
+    }
+    _pluginMap[pluginType] = builder;
+  }
+
+  List<String> get supportPluginTypes => _pluginMap.keys.toList();
+
+  List<PluginBuilder> get builders => _pluginMap.values.toList();
+}

+ 3 - 0
frontend/app_flowy/lib/startup/startup.dart

@@ -1,5 +1,6 @@
 import 'dart:io';
 
+import 'package:app_flowy/plugin/plugin.dart';
 import 'package:app_flowy/startup/tasks/prelude.dart';
 import 'package:flutter/foundation.dart';
 import 'package:flutter/material.dart';
@@ -41,6 +42,7 @@ class FlowyRunner {
     getIt<AppLauncher>().addTask(InitRustSDKTask());
 
     if (!env.isTest()) {
+      getIt<AppLauncher>().addTask(PluginLoadTask());
       getIt<AppLauncher>().addTask(InitAppWidgetTask());
       getIt<AppLauncher>().addTask(InitPlatformServiceTask());
     }
@@ -58,6 +60,7 @@ Future<void> initGetIt(
   getIt.registerFactory<EntryPoint>(() => f);
   getIt.registerLazySingleton<FlowySDK>(() => const FlowySDK());
   getIt.registerLazySingleton<AppLauncher>(() => AppLauncher(env, getIt));
+  getIt.registerSingleton<PluginSandbox>(PluginSandbox());
 
   await UserDepsResolver.resolve(getIt);
   await HomeDepsResolver.resolve(getIt);

+ 39 - 0
frontend/app_flowy/lib/startup/tasks/load_plugin.dart

@@ -0,0 +1,39 @@
+import 'package:app_flowy/plugin/plugin.dart';
+import 'package:app_flowy/startup/startup.dart';
+import 'package:app_flowy/workspace/presentation/stack_page/blank/blank_page.dart';
+import 'package:app_flowy/workspace/presentation/stack_page/doc/doc_stack_page.dart';
+import 'package:app_flowy/workspace/presentation/stack_page/trash/trash_page.dart';
+
+enum DefaultPluginEnum {
+  blank,
+  trash,
+}
+
+extension FlowyDefaultPluginExt on DefaultPluginEnum {
+  String type() {
+    switch (this) {
+      case DefaultPluginEnum.blank:
+        return "Blank";
+      case DefaultPluginEnum.trash:
+        return "Trash";
+    }
+  }
+}
+
+bool isDefaultPlugin(String pluginType) {
+  return DefaultPluginEnum.values.map((e) => e.type()).contains(pluginType);
+}
+
+class PluginLoadTask extends LaunchTask {
+  @override
+  LaunchTaskType get type => LaunchTaskType.dataProcessing;
+
+  @override
+  Future<void> initialize(LaunchContext context) async {
+    registerPlugin(builder: DocumentPluginBuilder());
+
+    registerPlugin(builder: TrashPluginBuilder());
+
+    registerPlugin(builder: BlankPluginBuilder());
+  }
+}

+ 2 - 1
frontend/app_flowy/lib/startup/tasks/prelude.dart

@@ -1,3 +1,4 @@
 export 'app_widget.dart';
-export 'init_sdk.dart';
+export 'rust_sdk.dart';
 export 'platform_service.dart';
+export 'load_plugin.dart';

+ 0 - 0
frontend/app_flowy/lib/startup/tasks/init_sdk.dart → frontend/app_flowy/lib/startup/tasks/rust_sdk.dart


+ 3 - 2
frontend/app_flowy/lib/workspace/application/app/app_bloc.dart

@@ -1,3 +1,4 @@
+import 'package:app_flowy/plugin/plugin.dart';
 import 'package:app_flowy/workspace/infrastructure/repos/app_repo.dart';
 import 'package:flowy_sdk/log.dart';
 import 'package:flowy_sdk/protobuf/flowy-folder-data-model/app.pb.dart';
@@ -21,7 +22,7 @@ class AppBloc extends Bloc<AppEvent, AppState> {
         );
         await _fetchViews(emit);
       }, createView: (CreateView value) async {
-        final viewOrFailed = await repo.createView(name: value.name, desc: value.desc, viewType: value.viewType);
+        final viewOrFailed = await repo.createView(name: value.name, desc: value.desc, dataType: value.dataType);
         viewOrFailed.fold(
           (view) => emit(state.copyWith(
             latestCreatedView: view,
@@ -95,7 +96,7 @@ class AppBloc extends Bloc<AppEvent, AppState> {
 @freezed
 class AppEvent with _$AppEvent {
   const factory AppEvent.initial() = Initial;
-  const factory AppEvent.createView(String name, String desc, ViewType viewType) = CreateView;
+  const factory AppEvent.createView(String name, String desc, PluginDataType dataType) = CreateView;
   const factory AppEvent.delete() = Delete;
   const factory AppEvent.rename(String newName) = Rename;
   const factory AppEvent.didReceiveViews(List<View> views) = ReceiveViews;

+ 6 - 6
frontend/app_flowy/lib/workspace/application/menu/menu_bloc.dart

@@ -1,7 +1,7 @@
 import 'dart:async';
-import 'package:app_flowy/workspace/domain/page_stack/page_stack.dart';
+import 'package:app_flowy/plugin/plugin.dart';
+import 'package:app_flowy/startup/tasks/load_plugin.dart';
 import 'package:app_flowy/workspace/infrastructure/repos/workspace_repo.dart';
-import 'package:app_flowy/workspace/presentation/stack_page/blank/blank_page.dart';
 import 'package:dartz/dartz.dart';
 import 'package:flowy_sdk/log.dart';
 import 'package:flowy_sdk/protobuf/flowy-folder-data-model/app.pb.dart';
@@ -26,7 +26,7 @@ class MenuBloc extends Bloc<MenuEvent, MenuState> {
           emit(state.copyWith(isCollapse: !isCollapse));
         },
         openPage: (e) async {
-          emit(state.copyWith(stackContext: e.context));
+          emit(state.copyWith(plugin: e.plugin));
         },
         createApp: (CreateApp event) async {
           await _performActionOnCreateApp(event, emit);
@@ -82,7 +82,7 @@ class MenuBloc extends Bloc<MenuEvent, MenuState> {
 class MenuEvent with _$MenuEvent {
   const factory MenuEvent.initial() = _Initial;
   const factory MenuEvent.collapse() = Collapse;
-  const factory MenuEvent.openPage(HomeStackContext context) = OpenPage;
+  const factory MenuEvent.openPage(Plugin plugin) = OpenPage;
   const factory MenuEvent.createApp(String name, {String? desc}) = CreateApp;
   const factory MenuEvent.didReceiveApps(Either<List<App>, FlowyError> appsOrFail) = ReceiveApps;
 }
@@ -93,13 +93,13 @@ class MenuState with _$MenuState {
     required bool isCollapse,
     required Option<List<App>> apps,
     required Either<Unit, FlowyError> successOrFailure,
-    required HomeStackContext stackContext,
+    required Plugin plugin,
   }) = _MenuState;
 
   factory MenuState.initial() => MenuState(
         isCollapse: false,
         apps: none(),
         successOrFailure: left(unit),
-        stackContext: BlankStackContext(),
+        plugin: makePlugin(pluginType: DefaultPluginEnum.blank.type()),
       );
 }

+ 60 - 71
frontend/app_flowy/lib/workspace/application/menu/menu_bloc.freezed.dart

@@ -25,9 +25,9 @@ class _$MenuEventTearOff {
     return const Collapse();
   }
 
-  OpenPage openPage(HomeStackContext<dynamic, dynamic> context) {
+  OpenPage openPage(Plugin plugin) {
     return OpenPage(
-      context,
+      plugin,
     );
   }
 
@@ -54,8 +54,7 @@ mixin _$MenuEvent {
   TResult when<TResult extends Object?>({
     required TResult Function() initial,
     required TResult Function() collapse,
-    required TResult Function(HomeStackContext<dynamic, dynamic> context)
-        openPage,
+    required TResult Function(Plugin plugin) openPage,
     required TResult Function(String name, String? desc) createApp,
     required TResult Function(Either<List<App>, FlowyError> appsOrFail)
         didReceiveApps,
@@ -65,7 +64,7 @@ mixin _$MenuEvent {
   TResult? whenOrNull<TResult extends Object?>({
     TResult Function()? initial,
     TResult Function()? collapse,
-    TResult Function(HomeStackContext<dynamic, dynamic> context)? openPage,
+    TResult Function(Plugin plugin)? openPage,
     TResult Function(String name, String? desc)? createApp,
     TResult Function(Either<List<App>, FlowyError> appsOrFail)? didReceiveApps,
   }) =>
@@ -74,7 +73,7 @@ mixin _$MenuEvent {
   TResult maybeWhen<TResult extends Object?>({
     TResult Function()? initial,
     TResult Function()? collapse,
-    TResult Function(HomeStackContext<dynamic, dynamic> context)? openPage,
+    TResult Function(Plugin plugin)? openPage,
     TResult Function(String name, String? desc)? createApp,
     TResult Function(Either<List<App>, FlowyError> appsOrFail)? didReceiveApps,
     required TResult orElse(),
@@ -164,8 +163,7 @@ class _$_Initial implements _Initial {
   TResult when<TResult extends Object?>({
     required TResult Function() initial,
     required TResult Function() collapse,
-    required TResult Function(HomeStackContext<dynamic, dynamic> context)
-        openPage,
+    required TResult Function(Plugin plugin) openPage,
     required TResult Function(String name, String? desc) createApp,
     required TResult Function(Either<List<App>, FlowyError> appsOrFail)
         didReceiveApps,
@@ -178,7 +176,7 @@ class _$_Initial implements _Initial {
   TResult? whenOrNull<TResult extends Object?>({
     TResult Function()? initial,
     TResult Function()? collapse,
-    TResult Function(HomeStackContext<dynamic, dynamic> context)? openPage,
+    TResult Function(Plugin plugin)? openPage,
     TResult Function(String name, String? desc)? createApp,
     TResult Function(Either<List<App>, FlowyError> appsOrFail)? didReceiveApps,
   }) {
@@ -190,7 +188,7 @@ class _$_Initial implements _Initial {
   TResult maybeWhen<TResult extends Object?>({
     TResult Function()? initial,
     TResult Function()? collapse,
-    TResult Function(HomeStackContext<dynamic, dynamic> context)? openPage,
+    TResult Function(Plugin plugin)? openPage,
     TResult Function(String name, String? desc)? createApp,
     TResult Function(Either<List<App>, FlowyError> appsOrFail)? didReceiveApps,
     required TResult orElse(),
@@ -285,8 +283,7 @@ class _$Collapse implements Collapse {
   TResult when<TResult extends Object?>({
     required TResult Function() initial,
     required TResult Function() collapse,
-    required TResult Function(HomeStackContext<dynamic, dynamic> context)
-        openPage,
+    required TResult Function(Plugin plugin) openPage,
     required TResult Function(String name, String? desc) createApp,
     required TResult Function(Either<List<App>, FlowyError> appsOrFail)
         didReceiveApps,
@@ -299,7 +296,7 @@ class _$Collapse implements Collapse {
   TResult? whenOrNull<TResult extends Object?>({
     TResult Function()? initial,
     TResult Function()? collapse,
-    TResult Function(HomeStackContext<dynamic, dynamic> context)? openPage,
+    TResult Function(Plugin plugin)? openPage,
     TResult Function(String name, String? desc)? createApp,
     TResult Function(Either<List<App>, FlowyError> appsOrFail)? didReceiveApps,
   }) {
@@ -311,7 +308,7 @@ class _$Collapse implements Collapse {
   TResult maybeWhen<TResult extends Object?>({
     TResult Function()? initial,
     TResult Function()? collapse,
-    TResult Function(HomeStackContext<dynamic, dynamic> context)? openPage,
+    TResult Function(Plugin plugin)? openPage,
     TResult Function(String name, String? desc)? createApp,
     TResult Function(Either<List<App>, FlowyError> appsOrFail)? didReceiveApps,
     required TResult orElse(),
@@ -371,7 +368,7 @@ abstract class Collapse implements MenuEvent {
 abstract class $OpenPageCopyWith<$Res> {
   factory $OpenPageCopyWith(OpenPage value, $Res Function(OpenPage) then) =
       _$OpenPageCopyWithImpl<$Res>;
-  $Res call({HomeStackContext<dynamic, dynamic> context});
+  $Res call({Plugin plugin});
 }
 
 /// @nodoc
@@ -385,13 +382,13 @@ class _$OpenPageCopyWithImpl<$Res> extends _$MenuEventCopyWithImpl<$Res>
 
   @override
   $Res call({
-    Object? context = freezed,
+    Object? plugin = freezed,
   }) {
     return _then(OpenPage(
-      context == freezed
-          ? _value.context
-          : context // ignore: cast_nullable_to_non_nullable
-              as HomeStackContext<dynamic, dynamic>,
+      plugin == freezed
+          ? _value.plugin
+          : plugin // ignore: cast_nullable_to_non_nullable
+              as Plugin,
     ));
   }
 }
@@ -399,27 +396,27 @@ class _$OpenPageCopyWithImpl<$Res> extends _$MenuEventCopyWithImpl<$Res>
 /// @nodoc
 
 class _$OpenPage implements OpenPage {
-  const _$OpenPage(this.context);
+  const _$OpenPage(this.plugin);
 
   @override
-  final HomeStackContext<dynamic, dynamic> context;
+  final Plugin plugin;
 
   @override
   String toString() {
-    return 'MenuEvent.openPage(context: $context)';
+    return 'MenuEvent.openPage(plugin: $plugin)';
   }
 
   @override
   bool operator ==(dynamic other) {
     return identical(this, other) ||
         (other is OpenPage &&
-            (identical(other.context, context) ||
-                const DeepCollectionEquality().equals(other.context, context)));
+            (identical(other.plugin, plugin) ||
+                const DeepCollectionEquality().equals(other.plugin, plugin)));
   }
 
   @override
   int get hashCode =>
-      runtimeType.hashCode ^ const DeepCollectionEquality().hash(context);
+      runtimeType.hashCode ^ const DeepCollectionEquality().hash(plugin);
 
   @JsonKey(ignore: true)
   @override
@@ -431,13 +428,12 @@ class _$OpenPage implements OpenPage {
   TResult when<TResult extends Object?>({
     required TResult Function() initial,
     required TResult Function() collapse,
-    required TResult Function(HomeStackContext<dynamic, dynamic> context)
-        openPage,
+    required TResult Function(Plugin plugin) openPage,
     required TResult Function(String name, String? desc) createApp,
     required TResult Function(Either<List<App>, FlowyError> appsOrFail)
         didReceiveApps,
   }) {
-    return openPage(context);
+    return openPage(plugin);
   }
 
   @override
@@ -445,11 +441,11 @@ class _$OpenPage implements OpenPage {
   TResult? whenOrNull<TResult extends Object?>({
     TResult Function()? initial,
     TResult Function()? collapse,
-    TResult Function(HomeStackContext<dynamic, dynamic> context)? openPage,
+    TResult Function(Plugin plugin)? openPage,
     TResult Function(String name, String? desc)? createApp,
     TResult Function(Either<List<App>, FlowyError> appsOrFail)? didReceiveApps,
   }) {
-    return openPage?.call(context);
+    return openPage?.call(plugin);
   }
 
   @override
@@ -457,13 +453,13 @@ class _$OpenPage implements OpenPage {
   TResult maybeWhen<TResult extends Object?>({
     TResult Function()? initial,
     TResult Function()? collapse,
-    TResult Function(HomeStackContext<dynamic, dynamic> context)? openPage,
+    TResult Function(Plugin plugin)? openPage,
     TResult Function(String name, String? desc)? createApp,
     TResult Function(Either<List<App>, FlowyError> appsOrFail)? didReceiveApps,
     required TResult orElse(),
   }) {
     if (openPage != null) {
-      return openPage(context);
+      return openPage(plugin);
     }
     return orElse();
   }
@@ -510,11 +506,9 @@ class _$OpenPage implements OpenPage {
 }
 
 abstract class OpenPage implements MenuEvent {
-  const factory OpenPage(HomeStackContext<dynamic, dynamic> context) =
-      _$OpenPage;
+  const factory OpenPage(Plugin plugin) = _$OpenPage;
 
-  HomeStackContext<dynamic, dynamic> get context =>
-      throw _privateConstructorUsedError;
+  Plugin get plugin => throw _privateConstructorUsedError;
   @JsonKey(ignore: true)
   $OpenPageCopyWith<OpenPage> get copyWith =>
       throw _privateConstructorUsedError;
@@ -595,8 +589,7 @@ class _$CreateApp implements CreateApp {
   TResult when<TResult extends Object?>({
     required TResult Function() initial,
     required TResult Function() collapse,
-    required TResult Function(HomeStackContext<dynamic, dynamic> context)
-        openPage,
+    required TResult Function(Plugin plugin) openPage,
     required TResult Function(String name, String? desc) createApp,
     required TResult Function(Either<List<App>, FlowyError> appsOrFail)
         didReceiveApps,
@@ -609,7 +602,7 @@ class _$CreateApp implements CreateApp {
   TResult? whenOrNull<TResult extends Object?>({
     TResult Function()? initial,
     TResult Function()? collapse,
-    TResult Function(HomeStackContext<dynamic, dynamic> context)? openPage,
+    TResult Function(Plugin plugin)? openPage,
     TResult Function(String name, String? desc)? createApp,
     TResult Function(Either<List<App>, FlowyError> appsOrFail)? didReceiveApps,
   }) {
@@ -621,7 +614,7 @@ class _$CreateApp implements CreateApp {
   TResult maybeWhen<TResult extends Object?>({
     TResult Function()? initial,
     TResult Function()? collapse,
-    TResult Function(HomeStackContext<dynamic, dynamic> context)? openPage,
+    TResult Function(Plugin plugin)? openPage,
     TResult Function(String name, String? desc)? createApp,
     TResult Function(Either<List<App>, FlowyError> appsOrFail)? didReceiveApps,
     required TResult orElse(),
@@ -750,8 +743,7 @@ class _$ReceiveApps implements ReceiveApps {
   TResult when<TResult extends Object?>({
     required TResult Function() initial,
     required TResult Function() collapse,
-    required TResult Function(HomeStackContext<dynamic, dynamic> context)
-        openPage,
+    required TResult Function(Plugin plugin) openPage,
     required TResult Function(String name, String? desc) createApp,
     required TResult Function(Either<List<App>, FlowyError> appsOrFail)
         didReceiveApps,
@@ -764,7 +756,7 @@ class _$ReceiveApps implements ReceiveApps {
   TResult? whenOrNull<TResult extends Object?>({
     TResult Function()? initial,
     TResult Function()? collapse,
-    TResult Function(HomeStackContext<dynamic, dynamic> context)? openPage,
+    TResult Function(Plugin plugin)? openPage,
     TResult Function(String name, String? desc)? createApp,
     TResult Function(Either<List<App>, FlowyError> appsOrFail)? didReceiveApps,
   }) {
@@ -776,7 +768,7 @@ class _$ReceiveApps implements ReceiveApps {
   TResult maybeWhen<TResult extends Object?>({
     TResult Function()? initial,
     TResult Function()? collapse,
-    TResult Function(HomeStackContext<dynamic, dynamic> context)? openPage,
+    TResult Function(Plugin plugin)? openPage,
     TResult Function(String name, String? desc)? createApp,
     TResult Function(Either<List<App>, FlowyError> appsOrFail)? didReceiveApps,
     required TResult orElse(),
@@ -847,12 +839,12 @@ class _$MenuStateTearOff {
       {required bool isCollapse,
       required Option<List<App>> apps,
       required Either<Unit, FlowyError> successOrFailure,
-      required HomeStackContext<dynamic, dynamic> stackContext}) {
+      required Plugin plugin}) {
     return _MenuState(
       isCollapse: isCollapse,
       apps: apps,
       successOrFailure: successOrFailure,
-      stackContext: stackContext,
+      plugin: plugin,
     );
   }
 }
@@ -866,8 +858,7 @@ mixin _$MenuState {
   Option<List<App>> get apps => throw _privateConstructorUsedError;
   Either<Unit, FlowyError> get successOrFailure =>
       throw _privateConstructorUsedError;
-  HomeStackContext<dynamic, dynamic> get stackContext =>
-      throw _privateConstructorUsedError;
+  Plugin get plugin => throw _privateConstructorUsedError;
 
   @JsonKey(ignore: true)
   $MenuStateCopyWith<MenuState> get copyWith =>
@@ -882,7 +873,7 @@ abstract class $MenuStateCopyWith<$Res> {
       {bool isCollapse,
       Option<List<App>> apps,
       Either<Unit, FlowyError> successOrFailure,
-      HomeStackContext<dynamic, dynamic> stackContext});
+      Plugin plugin});
 }
 
 /// @nodoc
@@ -898,7 +889,7 @@ class _$MenuStateCopyWithImpl<$Res> implements $MenuStateCopyWith<$Res> {
     Object? isCollapse = freezed,
     Object? apps = freezed,
     Object? successOrFailure = freezed,
-    Object? stackContext = freezed,
+    Object? plugin = freezed,
   }) {
     return _then(_value.copyWith(
       isCollapse: isCollapse == freezed
@@ -913,10 +904,10 @@ class _$MenuStateCopyWithImpl<$Res> implements $MenuStateCopyWith<$Res> {
           ? _value.successOrFailure
           : successOrFailure // ignore: cast_nullable_to_non_nullable
               as Either<Unit, FlowyError>,
-      stackContext: stackContext == freezed
-          ? _value.stackContext
-          : stackContext // ignore: cast_nullable_to_non_nullable
-              as HomeStackContext<dynamic, dynamic>,
+      plugin: plugin == freezed
+          ? _value.plugin
+          : plugin // ignore: cast_nullable_to_non_nullable
+              as Plugin,
     ));
   }
 }
@@ -931,7 +922,7 @@ abstract class _$MenuStateCopyWith<$Res> implements $MenuStateCopyWith<$Res> {
       {bool isCollapse,
       Option<List<App>> apps,
       Either<Unit, FlowyError> successOrFailure,
-      HomeStackContext<dynamic, dynamic> stackContext});
+      Plugin plugin});
 }
 
 /// @nodoc
@@ -948,7 +939,7 @@ class __$MenuStateCopyWithImpl<$Res> extends _$MenuStateCopyWithImpl<$Res>
     Object? isCollapse = freezed,
     Object? apps = freezed,
     Object? successOrFailure = freezed,
-    Object? stackContext = freezed,
+    Object? plugin = freezed,
   }) {
     return _then(_MenuState(
       isCollapse: isCollapse == freezed
@@ -963,10 +954,10 @@ class __$MenuStateCopyWithImpl<$Res> extends _$MenuStateCopyWithImpl<$Res>
           ? _value.successOrFailure
           : successOrFailure // ignore: cast_nullable_to_non_nullable
               as Either<Unit, FlowyError>,
-      stackContext: stackContext == freezed
-          ? _value.stackContext
-          : stackContext // ignore: cast_nullable_to_non_nullable
-              as HomeStackContext<dynamic, dynamic>,
+      plugin: plugin == freezed
+          ? _value.plugin
+          : plugin // ignore: cast_nullable_to_non_nullable
+              as Plugin,
     ));
   }
 }
@@ -978,7 +969,7 @@ class _$_MenuState implements _MenuState {
       {required this.isCollapse,
       required this.apps,
       required this.successOrFailure,
-      required this.stackContext});
+      required this.plugin});
 
   @override
   final bool isCollapse;
@@ -987,11 +978,11 @@ class _$_MenuState implements _MenuState {
   @override
   final Either<Unit, FlowyError> successOrFailure;
   @override
-  final HomeStackContext<dynamic, dynamic> stackContext;
+  final Plugin plugin;
 
   @override
   String toString() {
-    return 'MenuState(isCollapse: $isCollapse, apps: $apps, successOrFailure: $successOrFailure, stackContext: $stackContext)';
+    return 'MenuState(isCollapse: $isCollapse, apps: $apps, successOrFailure: $successOrFailure, plugin: $plugin)';
   }
 
   @override
@@ -1006,9 +997,8 @@ class _$_MenuState implements _MenuState {
             (identical(other.successOrFailure, successOrFailure) ||
                 const DeepCollectionEquality()
                     .equals(other.successOrFailure, successOrFailure)) &&
-            (identical(other.stackContext, stackContext) ||
-                const DeepCollectionEquality()
-                    .equals(other.stackContext, stackContext)));
+            (identical(other.plugin, plugin) ||
+                const DeepCollectionEquality().equals(other.plugin, plugin)));
   }
 
   @override
@@ -1017,7 +1007,7 @@ class _$_MenuState implements _MenuState {
       const DeepCollectionEquality().hash(isCollapse) ^
       const DeepCollectionEquality().hash(apps) ^
       const DeepCollectionEquality().hash(successOrFailure) ^
-      const DeepCollectionEquality().hash(stackContext);
+      const DeepCollectionEquality().hash(plugin);
 
   @JsonKey(ignore: true)
   @override
@@ -1030,7 +1020,7 @@ abstract class _MenuState implements MenuState {
       {required bool isCollapse,
       required Option<List<App>> apps,
       required Either<Unit, FlowyError> successOrFailure,
-      required HomeStackContext<dynamic, dynamic> stackContext}) = _$_MenuState;
+      required Plugin plugin}) = _$_MenuState;
 
   @override
   bool get isCollapse => throw _privateConstructorUsedError;
@@ -1040,8 +1030,7 @@ abstract class _MenuState implements MenuState {
   Either<Unit, FlowyError> get successOrFailure =>
       throw _privateConstructorUsedError;
   @override
-  HomeStackContext<dynamic, dynamic> get stackContext =>
-      throw _privateConstructorUsedError;
+  Plugin get plugin => throw _privateConstructorUsedError;
   @override
   @JsonKey(ignore: true)
   _$MenuStateCopyWith<_MenuState> get copyWith =>

+ 0 - 1
frontend/app_flowy/lib/workspace/application/view/view_bloc.dart

@@ -9,7 +9,6 @@ part 'view_bloc.freezed.dart';
 
 class ViewMenuBloc extends Bloc<ViewEvent, ViewState> {
   final ViewRepository repo;
-
   final ViewListener listener;
 
   ViewMenuBloc({

+ 0 - 27
frontend/app_flowy/lib/workspace/domain/image.dart

@@ -1,27 +0,0 @@
-import 'package:flowy_sdk/protobuf/flowy-folder-data-model/view.pb.dart';
-import 'package:flutter/material.dart';
-import 'package:flowy_infra/image.dart';
-
-AssetImage assetImageForViewType(ViewType type) {
-  final imageName = _imageNameForViewType(type);
-  return AssetImage('assets/images/$imageName');
-}
-
-extension SvgViewType on View {
-  Widget thumbnail({Color? iconColor}) {
-    final imageName = _imageNameForViewType(viewType);
-    final Widget widget = svg(imageName, color: iconColor);
-    return widget;
-  }
-}
-
-String _imageNameForViewType(ViewType type) {
-  switch (type) {
-    case ViewType.RichText:
-      return "file_icon";
-    case ViewType.Plugin:
-      return "file_icon";
-    default:
-      return "file_icon";
-  }
-}

+ 20 - 33
frontend/app_flowy/lib/workspace/domain/page_stack/page_stack.dart

@@ -1,3 +1,5 @@
+import 'package:app_flowy/plugin/plugin.dart';
+import 'package:app_flowy/startup/tasks/load_plugin.dart';
 import 'package:flowy_infra/notifier.dart';
 import 'package:flutter/material.dart';
 import 'package:provider/provider.dart';
@@ -10,23 +12,13 @@ typedef NavigationCallback = void Function(String id);
 abstract class NavigationItem {
   Widget get leftBarItem;
   Widget? get rightBarItem => null;
-  String get identifier;
 
   NavigationCallback get action => (id) {
         getIt<HomeStackManager>().setStackWithId(id);
       };
 }
 
-enum HomeStackType {
-  blank,
-  document,
-  kanban,
-  trash,
-}
-
-List<HomeStackType> pages = HomeStackType.values.toList();
-
-abstract class HomeStackContext<T, S> with NavigationItem {
+abstract class HomeStackContext<T> with NavigationItem {
   List<NavigationItem> get navigationItems;
 
   @override
@@ -35,40 +27,35 @@ abstract class HomeStackContext<T, S> with NavigationItem {
   @override
   Widget? get rightBarItem;
 
-  @override
-  String get identifier;
-
   ValueNotifier<T> get isUpdated;
 
-  HomeStackType get type;
-
   Widget buildWidget();
 
   void dispose();
 }
 
 class HomeStackNotifier extends ChangeNotifier {
-  HomeStackContext stackContext;
+  Plugin _plugin;
   PublishNotifier<bool> collapsedNotifier = PublishNotifier();
 
-  Widget get titleWidget => stackContext.leftBarItem;
+  Widget get titleWidget => _plugin.display.leftBarItem;
 
-  HomeStackNotifier({HomeStackContext? context}) : stackContext = context ?? BlankStackContext();
+  HomeStackNotifier({Plugin? plugin}) : _plugin = plugin ?? makePlugin(pluginType: DefaultPluginEnum.blank.type());
 
-  set context(HomeStackContext context) {
-    if (stackContext.identifier == context.identifier) {
+  set plugin(Plugin newPlugin) {
+    if (newPlugin.pluginId == _plugin.pluginId) {
       return;
     }
 
-    stackContext.isUpdated.removeListener(notifyListeners);
-    stackContext.dispose();
+    // stackContext.isUpdated.removeListener(notifyListeners);
+    _plugin.dispose();
 
-    stackContext = context;
-    stackContext.isUpdated.addListener(notifyListeners);
+    _plugin = newPlugin;
+    // stackContext.isUpdated.addListener(notifyListeners);
     notifyListeners();
   }
 
-  HomeStackContext get context => stackContext;
+  Plugin get plugin => _plugin;
 }
 
 // HomeStack is initialized as singleton to controll the page stack.
@@ -77,13 +64,13 @@ class HomeStackManager {
   HomeStackManager();
 
   Widget title() {
-    return _notifier.context.leftBarItem;
+    return _notifier.plugin.display.leftBarItem;
   }
 
   PublishNotifier<bool> get collapsedNotifier => _notifier.collapsedNotifier;
 
-  void setStack(HomeStackContext context) {
-    _notifier.context = context;
+  void setPlugin(Plugin newPlugin) {
+    _notifier.plugin = newPlugin;
   }
 
   void setStackWithId(String id) {}
@@ -109,10 +96,10 @@ class HomeStackManager {
       ],
       child: Consumer(builder: (ctx, HomeStackNotifier notifier, child) {
         return FadingIndexedStack(
-          index: pages.indexOf(notifier.context.type),
-          children: HomeStackType.values.map((viewType) {
-            if (viewType == notifier.context.type) {
-              return notifier.context.buildWidget();
+          index: getIt<PluginSandbox>().indexOf(notifier.plugin.pluginType),
+          children: getIt<PluginSandbox>().supportPluginTypes.map((pluginType) {
+            if (pluginType == notifier.plugin.pluginType) {
+              return notifier.plugin.display.buildWidget();
             } else {
               return const BlankStackPage();
             }

+ 22 - 32
frontend/app_flowy/lib/workspace/domain/view_ext.dart

@@ -1,40 +1,18 @@
-import 'package:app_flowy/workspace/domain/page_stack/page_stack.dart';
-import 'package:app_flowy/workspace/presentation/stack_page/blank/blank_page.dart';
-import 'package:app_flowy/workspace/presentation/stack_page/doc/doc_stack_page.dart';
+import 'package:flowy_infra/image.dart';
 import 'package:flowy_sdk/protobuf/flowy-folder-data-model/view.pb.dart';
+import 'package:flutter/material.dart';
 
-extension ToHomeStackContext on View {
-  HomeStackContext stackContext() {
-    switch (viewType) {
-      case ViewType.RichText:
-        return DocumentStackContext(view: this);
-      case ViewType.Plugin:
-        return DocumentStackContext(view: this);
-      default:
-        return BlankStackContext();
-    }
-  }
+enum FlowyPlugin {
+  editor,
+  kanban,
 }
 
-extension ToHomeStackType on View {
-  HomeStackType stackType() {
-    switch (viewType) {
-      case ViewType.RichText:
-        return HomeStackType.document;
-      case ViewType.PlainText:
-        return HomeStackType.kanban;
-      default:
-        return HomeStackType.blank;
-    }
-  }
-}
-
-extension ViewTypeExtension on ViewType {
+extension FlowyPluginExtension on FlowyPlugin {
   String displayName() {
     switch (this) {
-      case ViewType.RichText:
+      case FlowyPlugin.editor:
         return "Doc";
-      case ViewType.Plugin:
+      case FlowyPlugin.kanban:
         return "Kanban";
       default:
         return "";
@@ -43,12 +21,24 @@ extension ViewTypeExtension on ViewType {
 
   bool enable() {
     switch (this) {
-      case ViewType.RichText:
+      case FlowyPlugin.editor:
         return true;
-      case ViewType.Plugin:
+      case FlowyPlugin.kanban:
         return false;
       default:
         return false;
     }
   }
 }
+
+extension ViewExtension on View {
+  Widget renderThumbnail({Color? iconColor}) {
+    String thumbnail = this.thumbnail;
+    if (thumbnail.isEmpty) {
+      thumbnail = "file_icon";
+    }
+
+    final Widget widget = svg(thumbnail, color: iconColor);
+    return widget;
+  }
+}

+ 3 - 2
frontend/app_flowy/lib/workspace/infrastructure/repos/app_repo.dart

@@ -1,5 +1,6 @@
 import 'dart:async';
 import 'dart:typed_data';
+import 'package:app_flowy/plugin/plugin.dart';
 import 'package:dartz/dartz.dart';
 import 'package:flowy_sdk/log.dart';
 import 'package:flowy_sdk/dispatch/dispatch.dart';
@@ -26,13 +27,13 @@ class AppRepository {
   Future<Either<View, FlowyError>> createView({
     required String name,
     required String desc,
-    required ViewType viewType,
+    required PluginDataType dataType,
   }) {
     final request = CreateViewPayload.create()
       ..belongToId = appId
       ..name = name
       ..desc = desc
-      ..viewType = viewType;
+      ..dataType = dataType;
 
     return FolderEventCreateView(request).send();
   }

+ 3 - 1
frontend/app_flowy/lib/workspace/presentation/home/home_screen.dart

@@ -1,3 +1,4 @@
+import 'package:app_flowy/plugin/plugin.dart';
 import 'package:app_flowy/workspace/application/home/home_bloc.dart';
 import 'package:app_flowy/workspace/application/home/home_listen_bloc.dart';
 import 'package:app_flowy/workspace/domain/page_stack/page_stack.dart';
@@ -109,7 +110,8 @@ class _HomeScreenState extends State<HomeScreen> {
   Widget _buildHomeMenu({required HomeLayout layout, required BuildContext context}) {
     if (initialView == null && widget.workspaceSetting.hasLatestView()) {
       initialView = widget.workspaceSetting.latestView;
-      getIt<HomeStackManager>().setStack(initialView!.stackContext());
+      final plugin = makePlugin(pluginType: "RichText", data: initialView);
+      getIt<HomeStackManager>().setPlugin(plugin);
     }
 
     HomeMenu homeMenu = HomeMenu(

+ 3 - 3
frontend/app_flowy/lib/workspace/presentation/home/navigation.dart

@@ -17,8 +17,8 @@ class NavigationNotifier with ChangeNotifier {
 
   void update(HomeStackNotifier notifier) {
     bool shouldNotify = false;
-    if (navigationItems != notifier.context.navigationItems) {
-      navigationItems = notifier.context.navigationItems;
+    if (navigationItems != notifier.plugin.display.navigationItems) {
+      navigationItems = notifier.plugin.display.navigationItems;
       shouldNotify = true;
     }
 
@@ -59,7 +59,7 @@ class FlowyNavigation extends StatelessWidget {
       create: (_) {
         final notifier = Provider.of<HomeStackNotifier>(context, listen: false);
         return NavigationNotifier(
-          navigationItems: notifier.context.navigationItems,
+          navigationItems: notifier.plugin.display.navigationItems,
           collapasedNotifier: notifier.collapsedNotifier,
         );
       },

+ 43 - 13
frontend/app_flowy/lib/workspace/presentation/stack_page/blank/blank_page.dart

@@ -1,37 +1,67 @@
-import 'package:app_flowy/workspace/domain/page_stack/page_stack.dart';
+import 'package:app_flowy/startup/tasks/load_plugin.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flowy_infra_ui/style_widget/text.dart';
+import 'package:flowy_sdk/protobuf/flowy-folder-data-model/view.pbenum.dart';
 import 'package:flutter/material.dart';
+
 import 'package:app_flowy/generated/locale_keys.g.dart';
+import 'package:app_flowy/plugin/plugin.dart';
+import 'package:app_flowy/workspace/domain/page_stack/page_stack.dart';
 
-class BlankStackContext extends HomeStackContext {
-  final ValueNotifier<bool> _isUpdated = ValueNotifier<bool>(false);
+class BlankPluginBuilder implements PluginBuilder {
+  @override
+  Plugin build(dynamic data) {
+    return BlankPagePlugin(pluginType: pluginType);
+  }
 
   @override
-  String get identifier => "1";
+  String get pluginName => "Blank";
 
   @override
-  Widget get leftBarItem => FlowyText.medium(LocaleKeys.blankPageTitle.tr(), fontSize: 12);
+  PluginType get pluginType => DefaultPluginEnum.blank.type();
 
   @override
-  Widget? get rightBarItem => null;
+  ViewDataType get dataType => ViewDataType.PlainText;
+}
+
+class BlankPagePlugin implements Plugin {
+  late PluginType _pluginType;
+  BlankPagePlugin({
+    required String pluginType,
+  }) {
+    _pluginType = pluginType;
+  }
 
   @override
-  HomeStackType get type => HomeStackType.blank;
+  void dispose() {}
 
   @override
-  Widget buildWidget() {
-    return const BlankStackPage();
-  }
+  PluginDisplay get display => BlankPagePluginDisplay();
 
   @override
-  List<NavigationItem> get navigationItems => [this];
+  bool get enable => true;
 
   @override
-  ValueNotifier<bool> get isUpdated => _isUpdated;
+  String get pluginId => "BlankStack";
 
   @override
-  void dispose() {}
+  PluginType get pluginType => _pluginType;
+}
+
+class BlankPagePluginDisplay extends PluginDisplay {
+  @override
+  Widget get leftBarItem => FlowyText.medium(LocaleKeys.blankPageTitle.tr(), fontSize: 12);
+
+  @override
+  Widget? get rightBarItem => null;
+
+  @override
+  Widget buildWidget() {
+    return const BlankStackPage();
+  }
+
+  @override
+  List<NavigationItem> get navigationItems => [this];
 }
 
 class BlankStackPage extends StatefulWidget {

+ 46 - 15
frontend/app_flowy/lib/workspace/presentation/stack_page/doc/doc_stack_page.dart

@@ -1,8 +1,8 @@
+import 'package:app_flowy/plugin/plugin.dart';
 import 'package:app_flowy/startup/startup.dart';
 import 'package:app_flowy/workspace/application/appearance.dart';
 import 'package:app_flowy/workspace/application/doc/share_bloc.dart';
 import 'package:app_flowy/workspace/domain/page_stack/page_stack.dart';
-import 'package:app_flowy/workspace/domain/view_ext.dart';
 import 'package:app_flowy/workspace/infrastructure/repos/view_repo.dart';
 import 'package:app_flowy/workspace/presentation/widgets/dialogs.dart';
 import 'package:app_flowy/workspace/presentation/widgets/pop_up_action.dart';
@@ -24,12 +24,34 @@ import 'package:provider/provider.dart';
 
 import 'document_page.dart';
 
-class DocumentStackContext extends HomeStackContext<int, ShareActionWrapper> {
-  View _view;
+class DocumentPluginBuilder implements PluginBuilder {
+  @override
+  Plugin build(dynamic data) {
+    if (data is View) {
+      return DocumentPlugin(pluginType: pluginType, view: data);
+    } else {
+      throw FlowyPluginException.invalidData;
+    }
+  }
+
+  @override
+  String get pluginName => "Doc";
+
+  @override
+  PluginType get pluginType => "RichText";
+
+  @override
+  ViewDataType get dataType => ViewDataType.RichText;
+}
+
+class DocumentPlugin implements Plugin {
+  late View _view;
   late ViewListener _listener;
   final ValueNotifier<int> _isUpdated = ValueNotifier<int>(0);
+  late PluginType _pluginType;
 
-  DocumentStackContext({required View view, Key? key}) : _view = view {
+  DocumentPlugin({required PluginType pluginType, required View view, Key? key}) : _view = view {
+    _pluginType = pluginType;
     _listener = getIt<ViewListener>(param1: view);
     _listener.updatedNotifier.addPublishListener((result) {
       result.fold(
@@ -44,36 +66,45 @@ class DocumentStackContext extends HomeStackContext<int, ShareActionWrapper> {
   }
 
   @override
-  Widget get leftBarItem => DocumentLeftBarItem(view: _view);
+  void dispose() {
+    _listener.close();
+  }
 
   @override
-  Widget? get rightBarItem => DocumentShareButton(view: _view);
+  PluginDisplay get display => DocumentPluginDisplay(view: _view);
 
   @override
-  String get identifier => _view.id;
+  bool get enable => true;
 
   @override
-  HomeStackType get type => _view.stackType();
+  PluginType get pluginType => _pluginType;
+
+  @override
+  String get pluginId => _view.id;
+}
+
+class DocumentPluginDisplay extends PluginDisplay {
+  final View _view;
+
+  DocumentPluginDisplay({required View view, Key? key}) : _view = view;
 
   @override
   Widget buildWidget() => DocumentPage(view: _view, key: ValueKey(_view.id));
 
   @override
-  List<NavigationItem> get navigationItems => _makeNavigationItems();
+  Widget get leftBarItem => DocumentLeftBarItem(view: _view);
+
+  @override
+  Widget? get rightBarItem => DocumentShareButton(view: _view);
 
   @override
-  ValueNotifier<int> get isUpdated => _isUpdated;
+  List<NavigationItem> get navigationItems => _makeNavigationItems();
 
   List<NavigationItem> _makeNavigationItems() {
     return [
       this,
     ];
   }
-
-  @override
-  void dispose() {
-    _listener.close();
-  }
 }
 
 class DocumentLeftBarItem extends StatefulWidget {

+ 0 - 11
frontend/app_flowy/lib/workspace/presentation/stack_page/home_stack.dart

@@ -10,17 +10,6 @@ import 'package:fluttertoast/fluttertoast.dart';
 
 late FToast fToast;
 
-// [[diagram: HomeStack's widget structure]]
-//
-//                                                               ┌──────────────────┐   ┌───────────────┐
-//                                                            ┌──│BlankStackContext │──▶│BlankStackPage │
-// ┌──────────┐  ┌───────────────────┐   ┌─────────────────┐  │  └──────────────────┘   └───────────────┘
-// │HomeStack │─▶│ HomeStackManager  │──▶│HomeStackContext │◀─┤
-// └──────────┘  └───────────────────┘   └─────────────────┘  │  ┌─────────────────┐    ┌────────────┐
-//                                                            └──│ DocStackContext │───▶│DocStackPage│
-//                                                               └─────────────────┘    └────────────┘
-//
-//
 class HomeStack extends StatelessWidget {
   static GlobalKey<ScaffoldState> scaffoldKey = GlobalKey();
   // final Size size;

+ 38 - 10
frontend/app_flowy/lib/workspace/presentation/stack_page/trash/trash_page.dart

@@ -1,4 +1,6 @@
+import 'package:app_flowy/plugin/plugin.dart';
 import 'package:app_flowy/startup/startup.dart';
+import 'package:app_flowy/startup/tasks/load_plugin.dart';
 import 'package:app_flowy/workspace/application/trash/trash_bloc.dart';
 import 'package:app_flowy/workspace/domain/page_stack/page_stack.dart';
 import 'package:app_flowy/workspace/presentation/stack_page/trash/widget/sizes.dart';
@@ -12,6 +14,7 @@ import 'package:flowy_infra_ui/style_widget/scrolling/styled_scrollview.dart';
 import 'package:flowy_infra_ui/style_widget/text.dart';
 import 'package:flowy_infra_ui/style_widget/button.dart';
 import 'package:flowy_infra_ui/widget/spacing.dart';
+import 'package:flowy_sdk/protobuf/flowy-folder-data-model/view.pbenum.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:styled_widget/styled_widget.dart';
@@ -19,32 +22,57 @@ import 'package:app_flowy/generated/locale_keys.g.dart';
 
 import 'widget/trash_header.dart';
 
-class TrashStackContext extends HomeStackContext {
-  final ValueNotifier<bool> _isUpdated = ValueNotifier<bool>(false);
+class TrashPluginBuilder implements PluginBuilder {
+  @override
+  Plugin build(dynamic data) {
+    return TrashPlugin(pluginType: pluginType);
+  }
 
   @override
-  String get identifier => "TrashStackContext";
+  String get pluginName => "Trash";
 
   @override
-  Widget get leftBarItem => FlowyText.medium(LocaleKeys.trash_text.tr(), fontSize: 12);
+  PluginType get pluginType => DefaultPluginEnum.trash.type();
 
   @override
-  Widget? get rightBarItem => null;
+  ViewDataType get dataType => ViewDataType.PlainText;
+}
+
+class TrashPlugin implements Plugin {
+  late PluginType _pluginType;
+
+  TrashPlugin({required PluginType pluginType}) {
+    _pluginType = pluginType;
+  }
 
   @override
-  HomeStackType get type => HomeStackType.trash;
+  void dispose() {}
 
   @override
-  Widget buildWidget() => const TrashStackPage(key: ValueKey('TrashStackPage'));
+  PluginDisplay get display => TrashPluginDisplay();
 
   @override
-  List<NavigationItem> get navigationItems => [this];
+  bool get enable => true;
 
   @override
-  ValueNotifier<bool> get isUpdated => _isUpdated;
+  String get pluginId => "TrashStack";
 
   @override
-  void dispose() {}
+  PluginType get pluginType => _pluginType;
+}
+
+class TrashPluginDisplay extends PluginDisplay {
+  @override
+  Widget get leftBarItem => FlowyText.medium(LocaleKeys.trash_text.tr(), fontSize: 12);
+
+  @override
+  Widget? get rightBarItem => null;
+
+  @override
+  Widget buildWidget() => const TrashStackPage(key: ValueKey('TrashStackPage'));
+
+  @override
+  List<NavigationItem> get navigationItems => [this];
 }
 
 class TrashStackPage extends StatefulWidget {

+ 1 - 28
frontend/app_flowy/lib/workspace/presentation/widgets/home_top_bar.dart

@@ -1,13 +1,10 @@
-import 'package:app_flowy/workspace/domain/image.dart';
 import 'package:app_flowy/workspace/domain/page_stack/page_stack.dart';
 import 'package:app_flowy/workspace/presentation/home/home_sizes.dart';
 import 'package:app_flowy/workspace/presentation/home/navigation.dart';
 import 'package:flowy_infra/theme.dart';
 import 'package:flowy_infra_ui/widget/spacing.dart';
-import 'package:flowy_sdk/protobuf/flowy-folder-data-model/view.pb.dart';
 import 'package:flutter/material.dart';
 import 'package:flowy_infra_ui/style_widget/extension.dart';
-import 'package:flowy_infra_ui/style_widget/text.dart';
 import 'package:provider/provider.dart';
 
 class HomeTopBar extends StatelessWidget {
@@ -28,7 +25,7 @@ class HomeTopBar extends StatelessWidget {
             value: Provider.of<HomeStackNotifier>(context, listen: false),
             child: Consumer(
               builder: (BuildContext context, HomeStackNotifier notifier, Widget? child) {
-                return notifier.stackContext.rightBarItem ?? const SizedBox();
+                return notifier.plugin.display.rightBarItem ?? const SizedBox();
               },
             ),
           ) // _renderMoreButton(),
@@ -41,27 +38,3 @@ class HomeTopBar extends StatelessWidget {
     );
   }
 }
-
-class HomeTitle extends StatelessWidget {
-  final String title;
-  final ViewType type;
-
-  const HomeTitle({
-    Key? key,
-    required this.title,
-    required this.type,
-  }) : super(key: key);
-
-  @override
-  Widget build(BuildContext context) {
-    return Flexible(
-      child: Row(
-        children: [
-          Image(fit: BoxFit.scaleDown, width: 15, height: 15, image: assetImageForViewType(type)),
-          const HSpace(6),
-          FlowyText(title, fontSize: 16),
-        ],
-      ),
-    );
-  }
-}

+ 2 - 2
frontend/app_flowy/lib/workspace/presentation/widgets/menu/menu.dart

@@ -70,9 +70,9 @@ class HomeMenu extends StatelessWidget {
       child: MultiBlocListener(
         listeners: [
           BlocListener<MenuBloc, MenuState>(
-            listenWhen: (p, c) => p.stackContext != c.stackContext,
+            listenWhen: (p, c) => p.plugin.pluginId != c.plugin.pluginId,
             listener: (context, state) {
-              getIt<HomeStackManager>().setStack(state.stackContext);
+              getIt<HomeStackManager>().setPlugin(state.plugin);
             },
           ),
           BlocListener<MenuBloc, MenuState>(

+ 25 - 15
frontend/app_flowy/lib/workspace/presentation/widgets/menu/widget/app/header/add_button.dart

@@ -1,3 +1,6 @@
+import 'package:app_flowy/plugin/plugin.dart';
+import 'package:app_flowy/startup/startup.dart';
+import 'package:app_flowy/startup/tasks/load_plugin.dart';
 import 'package:app_flowy/workspace/domain/view_ext.dart';
 import 'package:flowy_infra/image.dart';
 import 'package:flowy_infra/theme.dart';
@@ -5,13 +8,12 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart';
 import 'package:flowy_infra_ui/style_widget/hover.dart';
 import 'package:flowy_infra_ui/style_widget/icon_button.dart';
 import 'package:flowy_infra_ui/style_widget/text.dart';
-import 'package:flowy_sdk/protobuf/flowy-folder-data-model/view.pb.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:styled_widget/styled_widget.dart';
 
 class AddButton extends StatelessWidget {
-  final Function(ViewType) onSelected;
+  final Function(PluginBuilder) onSelected;
   const AddButton({
     Key? key,
     required this.onSelected,
@@ -35,21 +37,29 @@ class AddButton extends StatelessWidget {
 }
 
 class ActionList {
-  final Function(ViewType) onSelected;
+  final Function(PluginBuilder) onSelected;
   final BuildContext anchorContext;
   final String _identifier = 'DisclosureButtonActionList';
 
   const ActionList({required this.anchorContext, required this.onSelected});
 
   void show(BuildContext buildContext) {
-    final items = ViewType.values.where((element) => element.enable()).map((ty) {
-      return CreateItem(
-          viewType: ty,
-          onSelected: (viewType) {
+    final items = getIt<PluginSandbox>()
+        .builders
+        .where(
+          (builder) => !isDefaultPlugin(builder.pluginType),
+        )
+        .map(
+      (pluginBuilder) {
+        return CreateItem(
+          pluginBuilder: pluginBuilder,
+          onSelected: (builder) {
             FlowyOverlay.of(buildContext).remove(_identifier);
-            onSelected(viewType);
-          });
-    }).toList();
+            onSelected(builder);
+          },
+        );
+      },
+    ).toList();
 
     ListOverlay.showWithAnchor(
       buildContext,
@@ -65,11 +75,11 @@ class ActionList {
 }
 
 class CreateItem extends StatelessWidget {
-  final ViewType viewType;
-  final Function(ViewType) onSelected;
+  final PluginBuilder pluginBuilder;
+  final Function(PluginBuilder) onSelected;
   const CreateItem({
     Key? key,
-    required this.viewType,
+    required this.pluginBuilder,
     required this.onSelected,
   }) : super(key: key);
 
@@ -82,9 +92,9 @@ class CreateItem extends StatelessWidget {
       config: config,
       builder: (context, onHover) {
         return GestureDetector(
-          onTap: () => onSelected(viewType),
+          onTap: () => onSelected(pluginBuilder),
           child: FlowyText.medium(
-            viewType.displayName(),
+            pluginBuilder.pluginName,
             color: theme.textColor,
             fontSize: 12,
           ).padding(horizontal: 10, vertical: 6),

+ 6 - 4
frontend/app_flowy/lib/workspace/presentation/widgets/menu/widget/app/header/header.dart

@@ -102,10 +102,12 @@ class MenuAppHeader extends StatelessWidget {
     return Tooltip(
       message: LocaleKeys.menuAppHeader_addPageTooltip.tr(),
       child: AddButton(
-        onSelected: (viewType) {
-          context
-              .read<AppBloc>()
-              .add(AppEvent.createView(LocaleKeys.menuAppHeader_defaultNewPageName.tr(), "", viewType));
+        onSelected: (pluginBuilder) {
+          context.read<AppBloc>().add(AppEvent.createView(
+                LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
+                "",
+                pluginBuilder.dataType,
+              ));
         },
       ).padding(right: MenuAppSizes.headerPadding),
     );

+ 2 - 2
frontend/app_flowy/lib/workspace/presentation/widgets/menu/widget/app/section/item.dart

@@ -1,6 +1,7 @@
 import 'package:app_flowy/startup/startup.dart';
 import 'package:app_flowy/workspace/application/view/view_bloc.dart';
 import 'package:app_flowy/workspace/domain/edit_action/view_edit.dart';
+import 'package:app_flowy/workspace/domain/view_ext.dart';
 import 'package:app_flowy/workspace/presentation/widgets/dialogs.dart';
 import 'package:dartz/dartz.dart' as dartz;
 import 'package:easy_localization/easy_localization.dart';
@@ -12,7 +13,6 @@ import 'package:flowy_sdk/protobuf/flowy-folder-data-model/view.pb.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:styled_widget/styled_widget.dart';
-import 'package:app_flowy/workspace/domain/image.dart';
 import 'package:app_flowy/workspace/presentation/widgets/menu/widget/app/menu_app.dart';
 import 'package:app_flowy/generated/locale_keys.g.dart';
 
@@ -55,7 +55,7 @@ class ViewSectionItem extends StatelessWidget {
 
   Widget _render(BuildContext context, bool onHover, ViewState state, Color iconColor) {
     List<Widget> children = [
-      SizedBox(width: 16, height: 16, child: state.view.thumbnail(iconColor: iconColor)),
+      SizedBox(width: 16, height: 16, child: state.view.renderThumbnail(iconColor: iconColor)),
       const HSpace(2),
       Expanded(child: FlowyText.regular(state.view.name, fontSize: 12, overflow: TextOverflow.clip)),
     ];

+ 1 - 1
frontend/app_flowy/lib/workspace/presentation/widgets/menu/widget/app/section/section.dart

@@ -106,7 +106,7 @@ class ViewSectionNotifier with ChangeNotifier {
 
     if (view != null) {
       WidgetsBinding.instance?.addPostFrameCallback((_) {
-        getIt<HomeStackManager>().setStack(view.stackContext());
+        getIt<HomeStackManager>().setPlugin(view.stackContext());
       });
     } else {
       // do nothing

+ 3 - 2
frontend/app_flowy/lib/workspace/presentation/widgets/menu/widget/menu_trash.dart

@@ -1,7 +1,8 @@
+import 'package:app_flowy/plugin/plugin.dart';
 import 'package:app_flowy/startup/startup.dart';
+import 'package:app_flowy/startup/tasks/load_plugin.dart';
 import 'package:app_flowy/workspace/application/appearance.dart';
 import 'package:app_flowy/workspace/domain/page_stack/page_stack.dart';
-import 'package:app_flowy/workspace/presentation/stack_page/trash/trash_page.dart';
 import 'package:app_flowy/workspace/presentation/widgets/menu/menu.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flowy_infra/image.dart';
@@ -22,7 +23,7 @@ class MenuTrash extends StatelessWidget {
       child: InkWell(
         onTap: () {
           Provider.of<MenuSharedState>(context, listen: false).selectedView.value = null;
-          getIt<HomeStackManager>().setStack(TrashStackContext());
+          getIt<HomeStackManager>().setPlugin(makePlugin(pluginType: DefaultPluginEnum.trash.type()));
         },
         child: _render(context),
       ),

+ 82 - 40
frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-folder-data-model/view.pb.dart

@@ -20,11 +20,13 @@ class View extends $pb.GeneratedMessage {
     ..aOS(2, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'belongToId')
     ..aOS(3, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'name')
     ..aOS(4, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'desc')
-    ..e<ViewType>(5, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'viewType', $pb.PbFieldType.OE, defaultOrMaker: ViewType.RichText, valueOf: ViewType.valueOf, enumValues: ViewType.values)
+    ..e<ViewDataType>(5, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'dataType', $pb.PbFieldType.OE, defaultOrMaker: ViewDataType.RichText, valueOf: ViewDataType.valueOf, enumValues: ViewDataType.values)
     ..aInt64(6, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'version')
     ..aOM<RepeatedView>(7, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'belongings', subBuilder: RepeatedView.create)
     ..aInt64(8, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'modifiedTime')
     ..aInt64(9, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'createTime')
+    ..aOS(10, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'extData')
+    ..aOS(11, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'thumbnail')
     ..hasRequiredFields = false
   ;
 
@@ -34,11 +36,13 @@ class View extends $pb.GeneratedMessage {
     $core.String? belongToId,
     $core.String? name,
     $core.String? desc,
-    ViewType? viewType,
+    ViewDataType? dataType,
     $fixnum.Int64? version,
     RepeatedView? belongings,
     $fixnum.Int64? modifiedTime,
     $fixnum.Int64? createTime,
+    $core.String? extData,
+    $core.String? thumbnail,
   }) {
     final _result = create();
     if (id != null) {
@@ -53,8 +57,8 @@ class View extends $pb.GeneratedMessage {
     if (desc != null) {
       _result.desc = desc;
     }
-    if (viewType != null) {
-      _result.viewType = viewType;
+    if (dataType != null) {
+      _result.dataType = dataType;
     }
     if (version != null) {
       _result.version = version;
@@ -68,6 +72,12 @@ class View extends $pb.GeneratedMessage {
     if (createTime != null) {
       _result.createTime = createTime;
     }
+    if (extData != null) {
+      _result.extData = extData;
+    }
+    if (thumbnail != null) {
+      _result.thumbnail = thumbnail;
+    }
     return _result;
   }
   factory View.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
@@ -128,13 +138,13 @@ class View extends $pb.GeneratedMessage {
   void clearDesc() => clearField(4);
 
   @$pb.TagNumber(5)
-  ViewType get viewType => $_getN(4);
+  ViewDataType get dataType => $_getN(4);
   @$pb.TagNumber(5)
-  set viewType(ViewType v) { setField(5, v); }
+  set dataType(ViewDataType v) { setField(5, v); }
   @$pb.TagNumber(5)
-  $core.bool hasViewType() => $_has(4);
+  $core.bool hasDataType() => $_has(4);
   @$pb.TagNumber(5)
-  void clearViewType() => clearField(5);
+  void clearDataType() => clearField(5);
 
   @$pb.TagNumber(6)
   $fixnum.Int64 get version => $_getI64(5);
@@ -173,6 +183,24 @@ class View extends $pb.GeneratedMessage {
   $core.bool hasCreateTime() => $_has(8);
   @$pb.TagNumber(9)
   void clearCreateTime() => clearField(9);
+
+  @$pb.TagNumber(10)
+  $core.String get extData => $_getSZ(9);
+  @$pb.TagNumber(10)
+  set extData($core.String v) { $_setString(9, v); }
+  @$pb.TagNumber(10)
+  $core.bool hasExtData() => $_has(9);
+  @$pb.TagNumber(10)
+  void clearExtData() => clearField(10);
+
+  @$pb.TagNumber(11)
+  $core.String get thumbnail => $_getSZ(10);
+  @$pb.TagNumber(11)
+  set thumbnail($core.String v) { $_setString(10, v); }
+  @$pb.TagNumber(11)
+  $core.bool hasThumbnail() => $_has(10);
+  @$pb.TagNumber(11)
+  void clearThumbnail() => clearField(11);
 }
 
 class RepeatedView extends $pb.GeneratedMessage {
@@ -232,8 +260,8 @@ class CreateViewPayload extends $pb.GeneratedMessage {
     ..aOS(2, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'name')
     ..aOS(3, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'desc')
     ..aOS(4, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'thumbnail')
-    ..e<ViewType>(5, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'viewType', $pb.PbFieldType.OE, defaultOrMaker: ViewType.RichText, valueOf: ViewType.valueOf, enumValues: ViewType.values)
-    ..aOS(6, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'ext')
+    ..e<ViewDataType>(5, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'dataType', $pb.PbFieldType.OE, defaultOrMaker: ViewDataType.RichText, valueOf: ViewDataType.valueOf, enumValues: ViewDataType.values)
+    ..aOS(6, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'extData')
     ..hasRequiredFields = false
   ;
 
@@ -243,8 +271,8 @@ class CreateViewPayload extends $pb.GeneratedMessage {
     $core.String? name,
     $core.String? desc,
     $core.String? thumbnail,
-    ViewType? viewType,
-    $core.String? ext,
+    ViewDataType? dataType,
+    $core.String? extData,
   }) {
     final _result = create();
     if (belongToId != null) {
@@ -259,11 +287,11 @@ class CreateViewPayload extends $pb.GeneratedMessage {
     if (thumbnail != null) {
       _result.thumbnail = thumbnail;
     }
-    if (viewType != null) {
-      _result.viewType = viewType;
+    if (dataType != null) {
+      _result.dataType = dataType;
     }
-    if (ext != null) {
-      _result.ext = ext;
+    if (extData != null) {
+      _result.extData = extData;
     }
     return _result;
   }
@@ -328,22 +356,22 @@ class CreateViewPayload extends $pb.GeneratedMessage {
   void clearThumbnail() => clearField(4);
 
   @$pb.TagNumber(5)
-  ViewType get viewType => $_getN(4);
+  ViewDataType get dataType => $_getN(4);
   @$pb.TagNumber(5)
-  set viewType(ViewType v) { setField(5, v); }
+  set dataType(ViewDataType v) { setField(5, v); }
   @$pb.TagNumber(5)
-  $core.bool hasViewType() => $_has(4);
+  $core.bool hasDataType() => $_has(4);
   @$pb.TagNumber(5)
-  void clearViewType() => clearField(5);
+  void clearDataType() => clearField(5);
 
   @$pb.TagNumber(6)
-  $core.String get ext => $_getSZ(5);
+  $core.String get extData => $_getSZ(5);
   @$pb.TagNumber(6)
-  set ext($core.String v) { $_setString(5, v); }
+  set extData($core.String v) { $_setString(5, v); }
   @$pb.TagNumber(6)
-  $core.bool hasExt() => $_has(5);
+  $core.bool hasExtData() => $_has(5);
   @$pb.TagNumber(6)
-  void clearExt() => clearField(6);
+  void clearExtData() => clearField(6);
 }
 
 class CreateViewParams extends $pb.GeneratedMessage {
@@ -352,9 +380,10 @@ class CreateViewParams extends $pb.GeneratedMessage {
     ..aOS(2, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'name')
     ..aOS(3, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'desc')
     ..aOS(4, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'thumbnail')
-    ..e<ViewType>(5, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'viewType', $pb.PbFieldType.OE, defaultOrMaker: ViewType.RichText, valueOf: ViewType.valueOf, enumValues: ViewType.values)
-    ..aOS(6, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'ext')
+    ..e<ViewDataType>(5, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'dataType', $pb.PbFieldType.OE, defaultOrMaker: ViewDataType.RichText, valueOf: ViewDataType.valueOf, enumValues: ViewDataType.values)
+    ..aOS(6, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'extData')
     ..aOS(7, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'viewId')
+    ..aOS(8, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'data')
     ..hasRequiredFields = false
   ;
 
@@ -364,9 +393,10 @@ class CreateViewParams extends $pb.GeneratedMessage {
     $core.String? name,
     $core.String? desc,
     $core.String? thumbnail,
-    ViewType? viewType,
-    $core.String? ext,
+    ViewDataType? dataType,
+    $core.String? extData,
     $core.String? viewId,
+    $core.String? data,
   }) {
     final _result = create();
     if (belongToId != null) {
@@ -381,15 +411,18 @@ class CreateViewParams extends $pb.GeneratedMessage {
     if (thumbnail != null) {
       _result.thumbnail = thumbnail;
     }
-    if (viewType != null) {
-      _result.viewType = viewType;
+    if (dataType != null) {
+      _result.dataType = dataType;
     }
-    if (ext != null) {
-      _result.ext = ext;
+    if (extData != null) {
+      _result.extData = extData;
     }
     if (viewId != null) {
       _result.viewId = viewId;
     }
+    if (data != null) {
+      _result.data = data;
+    }
     return _result;
   }
   factory CreateViewParams.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
@@ -450,22 +483,22 @@ class CreateViewParams extends $pb.GeneratedMessage {
   void clearThumbnail() => clearField(4);
 
   @$pb.TagNumber(5)
-  ViewType get viewType => $_getN(4);
+  ViewDataType get dataType => $_getN(4);
   @$pb.TagNumber(5)
-  set viewType(ViewType v) { setField(5, v); }
+  set dataType(ViewDataType v) { setField(5, v); }
   @$pb.TagNumber(5)
-  $core.bool hasViewType() => $_has(4);
+  $core.bool hasDataType() => $_has(4);
   @$pb.TagNumber(5)
-  void clearViewType() => clearField(5);
+  void clearDataType() => clearField(5);
 
   @$pb.TagNumber(6)
-  $core.String get ext => $_getSZ(5);
+  $core.String get extData => $_getSZ(5);
   @$pb.TagNumber(6)
-  set ext($core.String v) { $_setString(5, v); }
+  set extData($core.String v) { $_setString(5, v); }
   @$pb.TagNumber(6)
-  $core.bool hasExt() => $_has(5);
+  $core.bool hasExtData() => $_has(5);
   @$pb.TagNumber(6)
-  void clearExt() => clearField(6);
+  void clearExtData() => clearField(6);
 
   @$pb.TagNumber(7)
   $core.String get viewId => $_getSZ(6);
@@ -475,6 +508,15 @@ class CreateViewParams extends $pb.GeneratedMessage {
   $core.bool hasViewId() => $_has(6);
   @$pb.TagNumber(7)
   void clearViewId() => clearField(7);
+
+  @$pb.TagNumber(8)
+  $core.String get data => $_getSZ(7);
+  @$pb.TagNumber(8)
+  set data($core.String v) { $_setString(7, v); }
+  @$pb.TagNumber(8)
+  $core.bool hasData() => $_has(7);
+  @$pb.TagNumber(8)
+  void clearData() => clearField(8);
 }
 
 class ViewId extends $pb.GeneratedMessage {

+ 7 - 7
frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-folder-data-model/view.pbenum.dart

@@ -9,18 +9,18 @@
 import 'dart:core' as $core;
 import 'package:protobuf/protobuf.dart' as $pb;
 
-class ViewType extends $pb.ProtobufEnum {
-  static const ViewType RichText = ViewType._(0, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'RichText');
-  static const ViewType PlainText = ViewType._(1, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'PlainText');
+class ViewDataType extends $pb.ProtobufEnum {
+  static const ViewDataType RichText = ViewDataType._(0, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'RichText');
+  static const ViewDataType PlainText = ViewDataType._(1, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'PlainText');
 
-  static const $core.List<ViewType> values = <ViewType> [
+  static const $core.List<ViewDataType> values = <ViewDataType> [
     RichText,
     PlainText,
   ];
 
-  static final $core.Map<$core.int, ViewType> _byValue = $pb.ProtobufEnum.initByValue(values);
-  static ViewType? valueOf($core.int value) => _byValue[value];
+  static final $core.Map<$core.int, ViewDataType> _byValue = $pb.ProtobufEnum.initByValue(values);
+  static ViewDataType? valueOf($core.int value) => _byValue[value];
 
-  const ViewType._($core.int v, $core.String n) : super(v, n);
+  const ViewDataType._($core.int v, $core.String n) : super(v, n);
 }
 

+ 16 - 13
frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-folder-data-model/view.pbjson.dart

@@ -8,17 +8,17 @@
 import 'dart:core' as $core;
 import 'dart:convert' as $convert;
 import 'dart:typed_data' as $typed_data;
-@$core.Deprecated('Use viewTypeDescriptor instead')
-const ViewType$json = const {
-  '1': 'ViewType',
+@$core.Deprecated('Use viewDataTypeDescriptor instead')
+const ViewDataType$json = const {
+  '1': 'ViewDataType',
   '2': const [
     const {'1': 'RichText', '2': 0},
     const {'1': 'PlainText', '2': 1},
   ],
 };
 
-/// Descriptor for `ViewType`. Decode as a `google.protobuf.EnumDescriptorProto`.
-final $typed_data.Uint8List viewTypeDescriptor = $convert.base64Decode('CghWaWV3VHlwZRIMCghSaWNoVGV4dBAAEg0KCVBsYWluVGV4dBAB');
+/// Descriptor for `ViewDataType`. Decode as a `google.protobuf.EnumDescriptorProto`.
+final $typed_data.Uint8List viewDataTypeDescriptor = $convert.base64Decode('CgxWaWV3RGF0YVR5cGUSDAoIUmljaFRleHQQABINCglQbGFpblRleHQQAQ==');
 @$core.Deprecated('Use viewDescriptor instead')
 const View$json = const {
   '1': 'View',
@@ -27,16 +27,18 @@ const View$json = const {
     const {'1': 'belong_to_id', '3': 2, '4': 1, '5': 9, '10': 'belongToId'},
     const {'1': 'name', '3': 3, '4': 1, '5': 9, '10': 'name'},
     const {'1': 'desc', '3': 4, '4': 1, '5': 9, '10': 'desc'},
-    const {'1': 'view_type', '3': 5, '4': 1, '5': 14, '6': '.ViewType', '10': 'viewType'},
+    const {'1': 'data_type', '3': 5, '4': 1, '5': 14, '6': '.ViewDataType', '10': 'dataType'},
     const {'1': 'version', '3': 6, '4': 1, '5': 3, '10': 'version'},
     const {'1': 'belongings', '3': 7, '4': 1, '5': 11, '6': '.RepeatedView', '10': 'belongings'},
     const {'1': 'modified_time', '3': 8, '4': 1, '5': 3, '10': 'modifiedTime'},
     const {'1': 'create_time', '3': 9, '4': 1, '5': 3, '10': 'createTime'},
+    const {'1': 'ext_data', '3': 10, '4': 1, '5': 9, '10': 'extData'},
+    const {'1': 'thumbnail', '3': 11, '4': 1, '5': 9, '10': 'thumbnail'},
   ],
 };
 
 /// Descriptor for `View`. Decode as a `google.protobuf.DescriptorProto`.
-final $typed_data.Uint8List viewDescriptor = $convert.base64Decode('CgRWaWV3Eg4KAmlkGAEgASgJUgJpZBIgCgxiZWxvbmdfdG9faWQYAiABKAlSCmJlbG9uZ1RvSWQSEgoEbmFtZRgDIAEoCVIEbmFtZRISCgRkZXNjGAQgASgJUgRkZXNjEiYKCXZpZXdfdHlwZRgFIAEoDjIJLlZpZXdUeXBlUgh2aWV3VHlwZRIYCgd2ZXJzaW9uGAYgASgDUgd2ZXJzaW9uEi0KCmJlbG9uZ2luZ3MYByABKAsyDS5SZXBlYXRlZFZpZXdSCmJlbG9uZ2luZ3MSIwoNbW9kaWZpZWRfdGltZRgIIAEoA1IMbW9kaWZpZWRUaW1lEh8KC2NyZWF0ZV90aW1lGAkgASgDUgpjcmVhdGVUaW1l');
+final $typed_data.Uint8List viewDescriptor = $convert.base64Decode('CgRWaWV3Eg4KAmlkGAEgASgJUgJpZBIgCgxiZWxvbmdfdG9faWQYAiABKAlSCmJlbG9uZ1RvSWQSEgoEbmFtZRgDIAEoCVIEbmFtZRISCgRkZXNjGAQgASgJUgRkZXNjEioKCWRhdGFfdHlwZRgFIAEoDjINLlZpZXdEYXRhVHlwZVIIZGF0YVR5cGUSGAoHdmVyc2lvbhgGIAEoA1IHdmVyc2lvbhItCgpiZWxvbmdpbmdzGAcgASgLMg0uUmVwZWF0ZWRWaWV3UgpiZWxvbmdpbmdzEiMKDW1vZGlmaWVkX3RpbWUYCCABKANSDG1vZGlmaWVkVGltZRIfCgtjcmVhdGVfdGltZRgJIAEoA1IKY3JlYXRlVGltZRIZCghleHRfZGF0YRgKIAEoCVIHZXh0RGF0YRIcCgl0aHVtYm5haWwYCyABKAlSCXRodW1ibmFpbA==');
 @$core.Deprecated('Use repeatedViewDescriptor instead')
 const RepeatedView$json = const {
   '1': 'RepeatedView',
@@ -55,8 +57,8 @@ const CreateViewPayload$json = const {
     const {'1': 'name', '3': 2, '4': 1, '5': 9, '10': 'name'},
     const {'1': 'desc', '3': 3, '4': 1, '5': 9, '10': 'desc'},
     const {'1': 'thumbnail', '3': 4, '4': 1, '5': 9, '9': 0, '10': 'thumbnail'},
-    const {'1': 'view_type', '3': 5, '4': 1, '5': 14, '6': '.ViewType', '10': 'viewType'},
-    const {'1': 'ext', '3': 6, '4': 1, '5': 9, '10': 'ext'},
+    const {'1': 'data_type', '3': 5, '4': 1, '5': 14, '6': '.ViewDataType', '10': 'dataType'},
+    const {'1': 'ext_data', '3': 6, '4': 1, '5': 9, '10': 'extData'},
   ],
   '8': const [
     const {'1': 'one_of_thumbnail'},
@@ -64,7 +66,7 @@ const CreateViewPayload$json = const {
 };
 
 /// Descriptor for `CreateViewPayload`. Decode as a `google.protobuf.DescriptorProto`.
-final $typed_data.Uint8List createViewPayloadDescriptor = $convert.base64Decode('ChFDcmVhdGVWaWV3UGF5bG9hZBIgCgxiZWxvbmdfdG9faWQYASABKAlSCmJlbG9uZ1RvSWQSEgoEbmFtZRgCIAEoCVIEbmFtZRISCgRkZXNjGAMgASgJUgRkZXNjEh4KCXRodW1ibmFpbBgEIAEoCUgAUgl0aHVtYm5haWwSJgoJdmlld190eXBlGAUgASgOMgkuVmlld1R5cGVSCHZpZXdUeXBlEhAKA2V4dBgGIAEoCVIDZXh0QhIKEG9uZV9vZl90aHVtYm5haWw=');
+final $typed_data.Uint8List createViewPayloadDescriptor = $convert.base64Decode('ChFDcmVhdGVWaWV3UGF5bG9hZBIgCgxiZWxvbmdfdG9faWQYASABKAlSCmJlbG9uZ1RvSWQSEgoEbmFtZRgCIAEoCVIEbmFtZRISCgRkZXNjGAMgASgJUgRkZXNjEh4KCXRodW1ibmFpbBgEIAEoCUgAUgl0aHVtYm5haWwSKgoJZGF0YV90eXBlGAUgASgOMg0uVmlld0RhdGFUeXBlUghkYXRhVHlwZRIZCghleHRfZGF0YRgGIAEoCVIHZXh0RGF0YUISChBvbmVfb2ZfdGh1bWJuYWls');
 @$core.Deprecated('Use createViewParamsDescriptor instead')
 const CreateViewParams$json = const {
   '1': 'CreateViewParams',
@@ -73,14 +75,15 @@ const CreateViewParams$json = const {
     const {'1': 'name', '3': 2, '4': 1, '5': 9, '10': 'name'},
     const {'1': 'desc', '3': 3, '4': 1, '5': 9, '10': 'desc'},
     const {'1': 'thumbnail', '3': 4, '4': 1, '5': 9, '10': 'thumbnail'},
-    const {'1': 'view_type', '3': 5, '4': 1, '5': 14, '6': '.ViewType', '10': 'viewType'},
-    const {'1': 'ext', '3': 6, '4': 1, '5': 9, '10': 'ext'},
+    const {'1': 'data_type', '3': 5, '4': 1, '5': 14, '6': '.ViewDataType', '10': 'dataType'},
+    const {'1': 'ext_data', '3': 6, '4': 1, '5': 9, '10': 'extData'},
     const {'1': 'view_id', '3': 7, '4': 1, '5': 9, '10': 'viewId'},
+    const {'1': 'data', '3': 8, '4': 1, '5': 9, '10': 'data'},
   ],
 };
 
 /// Descriptor for `CreateViewParams`. Decode as a `google.protobuf.DescriptorProto`.
-final $typed_data.Uint8List createViewParamsDescriptor = $convert.base64Decode('ChBDcmVhdGVWaWV3UGFyYW1zEiAKDGJlbG9uZ190b19pZBgBIAEoCVIKYmVsb25nVG9JZBISCgRuYW1lGAIgASgJUgRuYW1lEhIKBGRlc2MYAyABKAlSBGRlc2MSHAoJdGh1bWJuYWlsGAQgASgJUgl0aHVtYm5haWwSJgoJdmlld190eXBlGAUgASgOMgkuVmlld1R5cGVSCHZpZXdUeXBlEhAKA2V4dBgGIAEoCVIDZXh0EhcKB3ZpZXdfaWQYByABKAlSBnZpZXdJZA==');
+final $typed_data.Uint8List createViewParamsDescriptor = $convert.base64Decode('ChBDcmVhdGVWaWV3UGFyYW1zEiAKDGJlbG9uZ190b19pZBgBIAEoCVIKYmVsb25nVG9JZBISCgRuYW1lGAIgASgJUgRuYW1lEhIKBGRlc2MYAyABKAlSBGRlc2MSHAoJdGh1bWJuYWlsGAQgASgJUgl0aHVtYm5haWwSKgoJZGF0YV90eXBlGAUgASgOMg0uVmlld0RhdGFUeXBlUghkYXRhVHlwZRIZCghleHRfZGF0YRgGIAEoCVIHZXh0RGF0YRIXCgd2aWV3X2lkGAcgASgJUgZ2aWV3SWQSEgoEZGF0YRgIIAEoCVIEZGF0YQ==');
 @$core.Deprecated('Use viewIdDescriptor instead')
 const ViewId$json = const {
   '1': 'ViewId',

+ 3 - 3
frontend/rust-lib/flowy-document/src/queue.rs

@@ -8,7 +8,7 @@ use flowy_collaboration::{
     errors::CollaborateError,
 };
 use flowy_error::{FlowyError, FlowyResult};
-use flowy_sync::{DeltaMD5, RevisionCompact, RevisionManager, TransformDeltas};
+use flowy_sync::{DeltaMD5, RevisionCompact, RevisionManager, RichTextTransformDeltas, TransformDeltas};
 use futures::stream::StreamExt;
 use lib_ot::{
     core::{Interval, OperationTransformable},
@@ -102,7 +102,7 @@ impl EditBlockQueue {
                         server_prime = Some(s_prime);
                     }
                     drop(read_guard);
-                    Ok::<TransformDeltas<RichTextAttributes>, CollaborateError>(TransformDeltas {
+                    Ok::<RichTextTransformDeltas, CollaborateError>(TransformDeltas {
                         client_prime,
                         server_prime,
                     })
@@ -232,7 +232,7 @@ pub(crate) enum EditorCommand {
     },
     TransformDelta {
         delta: RichTextDelta,
-        ret: Ret<TransformDeltas<RichTextAttributes>>,
+        ret: Ret<RichTextTransformDeltas>,
     },
     Insert {
         index: usize,

+ 11 - 14
frontend/rust-lib/flowy-document/src/web_socket.rs

@@ -10,7 +10,8 @@ use flowy_collaboration::{
 use flowy_error::{internal_error, FlowyError};
 use flowy_sync::*;
 use lib_infra::future::{BoxResultFuture, FutureResult};
-use lib_ot::{core::Delta, rich_text::RichTextAttributes};
+use lib_ot::rich_text::RichTextAttributes;
+use lib_ot::rich_text::RichTextDelta;
 use lib_ws::WSConnectState;
 use std::{sync::Arc, time::Duration};
 use tokio::sync::{
@@ -31,12 +32,8 @@ pub(crate) async fn make_block_ws_manager(
 ) -> Arc<RevisionWebSocketManager> {
     let ws_data_provider = Arc::new(WSDataProvider::new(&doc_id, Arc::new(rev_manager.clone())));
     let resolver = Arc::new(BlockConflictResolver { edit_cmd_tx });
-    let conflict_controller = ConflictController::<RichTextAttributes>::new(
-        &user_id,
-        resolver,
-        Arc::new(ws_data_provider.clone()),
-        rev_manager,
-    );
+    let conflict_controller =
+        RichTextConflictController::new(&user_id, resolver, Arc::new(ws_data_provider.clone()), rev_manager);
     let ws_data_stream = Arc::new(BlockRevisionWSDataStream::new(conflict_controller));
     let ws_data_sink = Arc::new(BlockWSDataSink(ws_data_provider));
     let ping_duration = Duration::from_millis(DOCUMENT_SYNC_INTERVAL_IN_MILLIS);
@@ -66,11 +63,11 @@ fn listen_document_ws_state(_user_id: &str, _doc_id: &str, mut subscriber: broad
 }
 
 pub(crate) struct BlockRevisionWSDataStream {
-    conflict_controller: Arc<ConflictController<RichTextAttributes>>,
+    conflict_controller: Arc<RichTextConflictController>,
 }
 
 impl BlockRevisionWSDataStream {
-    pub fn new(conflict_controller: ConflictController<RichTextAttributes>) -> Self {
+    pub fn new(conflict_controller: RichTextConflictController) -> Self {
         Self {
             conflict_controller: Arc::new(conflict_controller),
         }
@@ -112,7 +109,7 @@ struct BlockConflictResolver {
 }
 
 impl ConflictResolver<RichTextAttributes> for BlockConflictResolver {
-    fn compose_delta(&self, delta: Delta<RichTextAttributes>) -> BoxResultFuture<DeltaMD5, FlowyError> {
+    fn compose_delta(&self, delta: RichTextDelta) -> BoxResultFuture<DeltaMD5, FlowyError> {
         let tx = self.edit_cmd_tx.clone();
         Box::pin(async move {
             let (ret, rx) = oneshot::channel();
@@ -131,11 +128,11 @@ impl ConflictResolver<RichTextAttributes> for BlockConflictResolver {
 
     fn transform_delta(
         &self,
-        delta: Delta<RichTextAttributes>,
-    ) -> BoxResultFuture<flowy_sync::TransformDeltas<RichTextAttributes>, FlowyError> {
+        delta: RichTextDelta,
+    ) -> BoxResultFuture<flowy_sync::RichTextTransformDeltas, FlowyError> {
         let tx = self.edit_cmd_tx.clone();
         Box::pin(async move {
-            let (ret, rx) = oneshot::channel::<CollaborateResult<TransformDeltas<RichTextAttributes>>>();
+            let (ret, rx) = oneshot::channel::<CollaborateResult<RichTextTransformDeltas>>();
             tx.send(EditorCommand::TransformDelta { delta, ret })
                 .await
                 .map_err(internal_error)?;
@@ -146,7 +143,7 @@ impl ConflictResolver<RichTextAttributes> for BlockConflictResolver {
         })
     }
 
-    fn reset_delta(&self, delta: Delta<RichTextAttributes>) -> BoxResultFuture<DeltaMD5, FlowyError> {
+    fn reset_delta(&self, delta: RichTextDelta) -> BoxResultFuture<DeltaMD5, FlowyError> {
         let tx = self.edit_cmd_tx.clone();
         Box::pin(async move {
             let (ret, rx) = oneshot::channel();

+ 2 - 2
frontend/rust-lib/flowy-folder/src/services/folder_editor.rs

@@ -12,7 +12,7 @@ use flowy_sync::{
     RevisionWebSocket, RevisionWebSocketManager,
 };
 use lib_infra::future::FutureResult;
-use lib_ot::core::PlainAttributes;
+use lib_ot::core::PlainTextAttributes;
 use lib_sqlite::ConnectionPool;
 use parking_lot::RwLock;
 use std::sync::Arc;
@@ -144,7 +144,7 @@ impl RevisionCompact for FolderRevisionCompact {
 
         let (base_rev_id, rev_id) = first_revision.pair_rev_id();
         let md5 = last_revision.md5.clone();
-        let delta = make_delta_from_revisions::<PlainAttributes>(revisions)?;
+        let delta = make_delta_from_revisions::<PlainTextAttributes>(revisions)?;
         let delta_data = delta.to_bytes();
         Ok(Revision::new(object_id, base_rev_id, rev_id, delta_data, user_id, md5))
     }

+ 23 - 22
frontend/rust-lib/flowy-folder/src/services/persistence/version_1/view_sql.rs

@@ -1,7 +1,7 @@
 use crate::{
     entities::{
         trash::{Trash, TrashType},
-        view::{RepeatedView, UpdateViewParams, View, ViewType},
+        view::{RepeatedView, UpdateViewParams, View, ViewDataType},
     },
     errors::FlowyError,
     services::persistence::version_1::app_sql::AppTable,
@@ -119,16 +119,16 @@ pub(crate) struct ViewTable {
     pub modified_time: i64,
     pub create_time: i64,
     pub thumbnail: String,
-    pub view_type: ViewTableType,
+    pub view_type: SqlViewDataType,
     pub version: i64,
     pub is_trash: bool,
 }
 
 impl ViewTable {
     pub fn new(view: View) -> Self {
-        let view_type = match view.view_type {
-            ViewType::RichText => ViewTableType::RichText,
-            ViewType::PlainText => ViewTableType::Text,
+        let data_type = match view.data_type {
+            ViewDataType::RichText => SqlViewDataType::RichText,
+            ViewDataType::PlainText => SqlViewDataType::PlainText,
         };
 
         ViewTable {
@@ -138,9 +138,8 @@ impl ViewTable {
             desc: view.desc,
             modified_time: view.modified_time,
             create_time: view.create_time,
-            // TODO: thumbnail
-            thumbnail: "".to_owned(),
-            view_type,
+            thumbnail: view.thumbnail,
+            view_type: data_type,
             version: 0,
             is_trash: false,
         }
@@ -149,9 +148,9 @@ impl ViewTable {
 
 impl std::convert::From<ViewTable> for View {
     fn from(table: ViewTable) -> Self {
-        let view_type = match table.view_type {
-            ViewTableType::RichText => ViewType::RichText,
-            ViewTableType::Text => ViewType::PlainText,
+        let data_type = match table.view_type {
+            SqlViewDataType::RichText => ViewDataType::RichText,
+            SqlViewDataType::PlainText => ViewDataType::PlainText,
         };
 
         View {
@@ -159,11 +158,13 @@ impl std::convert::From<ViewTable> for View {
             belong_to_id: table.belong_to_id,
             name: table.name,
             desc: table.desc,
-            view_type,
+            data_type,
             belongings: RepeatedView::default(),
             modified_time: table.modified_time,
             version: table.version,
             create_time: table.create_time,
+            ext_data: "".to_string(),
+            thumbnail: table.thumbnail,
         }
     }
 }
@@ -215,34 +216,34 @@ impl ViewChangeset {
 #[derive(Clone, Copy, PartialEq, Eq, Debug, Hash, FromSqlRow, AsExpression)]
 #[repr(i32)]
 #[sql_type = "Integer"]
-pub enum ViewTableType {
+pub enum SqlViewDataType {
     RichText = 0,
-    Text = 1,
+    PlainText = 1,
 }
 
-impl std::default::Default for ViewTableType {
+impl std::default::Default for SqlViewDataType {
     fn default() -> Self {
-        ViewTableType::RichText
+        SqlViewDataType::RichText
     }
 }
 
-impl std::convert::From<i32> for ViewTableType {
+impl std::convert::From<i32> for SqlViewDataType {
     fn from(value: i32) -> Self {
         match value {
-            0 => ViewTableType::RichText,
-            1 => ViewTableType::Text,
+            0 => SqlViewDataType::RichText,
+            1 => SqlViewDataType::PlainText,
             o => {
                 log::error!("Unsupported view type {}, fallback to ViewType::Docs", o);
-                ViewTableType::Text
+                SqlViewDataType::PlainText
             }
         }
     }
 }
 
-impl ViewTableType {
+impl SqlViewDataType {
     pub fn value(&self) -> i32 {
         *self as i32
     }
 }
 
-impl_sql_integer_expression!(ViewTableType);
+impl_sql_integer_expression!(SqlViewDataType);

+ 9 - 9
frontend/rust-lib/flowy-folder/src/services/view/controller.rs

@@ -24,7 +24,7 @@ use crate::{
 use flowy_database::kv::KV;
 use flowy_document::BlockManager;
 use flowy_folder_data_model::entities::share::{ExportData, ExportParams};
-use flowy_folder_data_model::entities::view::ViewType;
+
 use lib_infra::uuid_string;
 
 const LATEST_VIEW_ID: &str = "latest_view_id";
@@ -62,10 +62,10 @@ impl ViewController {
 
     #[tracing::instrument(level = "trace", skip(self, params), fields(name = %params.name), err)]
     pub(crate) async fn create_view_from_params(&self, params: CreateViewParams) -> Result<View, FlowyError> {
-        let view_data = if params.ext.is_empty() {
+        let view_data = if params.data.is_empty() {
             initial_delta_string()
         } else {
-            params.ext.clone()
+            params.data.clone()
         };
 
         let delta_data = Bytes::from(view_data);
@@ -78,12 +78,11 @@ impl ViewController {
         Ok(view)
     }
 
-    #[tracing::instrument(level = "debug", skip(self, view_id, view_type, repeated_revision), err)]
+    #[tracing::instrument(level = "debug", skip(self, view_id, repeated_revision), err)]
     pub(crate) async fn create_view(
         &self,
         view_id: &str,
         repeated_revision: RepeatedRevision,
-        view_type: ViewType,
     ) -> Result<(), FlowyError> {
         if repeated_revision.is_empty() {
             return Err(FlowyError::internal().context("The content of the view should not be empty"));
@@ -173,11 +172,12 @@ impl ViewController {
         let duplicate_params = CreateViewParams {
             belong_to_id: view.belong_to_id.clone(),
             name: format!("{} (copy)", &view.name),
-            desc: view.desc.clone(),
-            thumbnail: "".to_owned(),
-            view_type: view.view_type.clone(),
-            ext: document_json,
+            desc: view.desc,
+            thumbnail: view.thumbnail,
+            data_type: view.data_type,
+            data: document_json,
             view_id: uuid_string(),
+            ext_data: view.ext_data,
         };
 
         let _ = self.create_view_from_params(duplicate_params).await?;

+ 16 - 12
frontend/rust-lib/flowy-folder/src/services/web_socket.rs

@@ -10,7 +10,7 @@ use flowy_collaboration::{
 use flowy_error::FlowyError;
 use flowy_sync::*;
 use lib_infra::future::{BoxResultFuture, FutureResult};
-use lib_ot::core::{Delta, OperationTransformable, PlainAttributes, PlainDelta};
+use lib_ot::core::{OperationTransformable, PlainTextAttributes, PlainTextDelta};
 use parking_lot::RwLock;
 use std::{sync::Arc, time::Duration};
 
@@ -23,8 +23,12 @@ pub(crate) async fn make_folder_ws_manager(
 ) -> Arc<RevisionWebSocketManager> {
     let ws_data_provider = Arc::new(WSDataProvider::new(folder_id, Arc::new(rev_manager.clone())));
     let resolver = Arc::new(FolderConflictResolver { folder_pad });
-    let conflict_controller =
-        ConflictController::<PlainAttributes>::new(user_id, resolver, Arc::new(ws_data_provider.clone()), rev_manager);
+    let conflict_controller = ConflictController::<PlainTextAttributes>::new(
+        user_id,
+        resolver,
+        Arc::new(ws_data_provider.clone()),
+        rev_manager,
+    );
     let ws_data_stream = Arc::new(FolderRevisionWSDataStream::new(conflict_controller));
     let ws_data_sink = Arc::new(FolderWSDataSink(ws_data_provider));
     let ping_duration = Duration::from_millis(FOLDER_SYNC_INTERVAL_IN_MILLIS);
@@ -50,8 +54,8 @@ struct FolderConflictResolver {
     folder_pad: Arc<RwLock<FolderPad>>,
 }
 
-impl ConflictResolver<PlainAttributes> for FolderConflictResolver {
-    fn compose_delta(&self, delta: Delta<PlainAttributes>) -> BoxResultFuture<DeltaMD5, FlowyError> {
+impl ConflictResolver<PlainTextAttributes> for FolderConflictResolver {
+    fn compose_delta(&self, delta: PlainTextDelta) -> BoxResultFuture<DeltaMD5, FlowyError> {
         let folder_pad = self.folder_pad.clone();
         Box::pin(async move {
             let md5 = folder_pad.write().compose_remote_delta(delta)?;
@@ -61,13 +65,13 @@ impl ConflictResolver<PlainAttributes> for FolderConflictResolver {
 
     fn transform_delta(
         &self,
-        delta: Delta<PlainAttributes>,
-    ) -> BoxResultFuture<TransformDeltas<PlainAttributes>, FlowyError> {
+        delta: PlainTextDelta,
+    ) -> BoxResultFuture<TransformDeltas<PlainTextAttributes>, FlowyError> {
         let folder_pad = self.folder_pad.clone();
         Box::pin(async move {
             let read_guard = folder_pad.read();
-            let mut server_prime: Option<PlainDelta> = None;
-            let client_prime: PlainDelta;
+            let mut server_prime: Option<PlainTextDelta> = None;
+            let client_prime: PlainTextDelta;
             if read_guard.is_empty() {
                 // Do nothing
                 client_prime = delta;
@@ -84,7 +88,7 @@ impl ConflictResolver<PlainAttributes> for FolderConflictResolver {
         })
     }
 
-    fn reset_delta(&self, delta: Delta<PlainAttributes>) -> BoxResultFuture<DeltaMD5, FlowyError> {
+    fn reset_delta(&self, delta: PlainTextDelta) -> BoxResultFuture<DeltaMD5, FlowyError> {
         let folder_pad = self.folder_pad.clone();
         Box::pin(async move {
             let md5 = folder_pad.write().reset_folder(delta)?;
@@ -94,11 +98,11 @@ impl ConflictResolver<PlainAttributes> for FolderConflictResolver {
 }
 
 struct FolderRevisionWSDataStream {
-    conflict_controller: Arc<ConflictController<PlainAttributes>>,
+    conflict_controller: Arc<PlainTextConflictController>,
 }
 
 impl FolderRevisionWSDataStream {
-    pub fn new(conflict_controller: ConflictController<PlainAttributes>) -> Self {
+    pub fn new(conflict_controller: PlainTextConflictController) -> Self {
         Self {
             conflict_controller: Arc::new(conflict_controller),
         }

+ 4 - 3
frontend/rust-lib/flowy-folder/tests/workspace/helper.rs

@@ -5,7 +5,7 @@ use flowy_folder_data_model::entities::workspace::WorkspaceId;
 use flowy_folder_data_model::entities::{
     app::{App, AppId, CreateAppPayload, UpdateAppPayload},
     trash::{RepeatedTrash, TrashId, TrashType},
-    view::{CreateViewPayload, UpdateViewPayload, View, ViewType},
+    view::{CreateViewPayload, UpdateViewPayload, View, ViewDataType},
     workspace::{CreateWorkspacePayload, RepeatedWorkspace, Workspace},
 };
 use flowy_test::{event_builder::*, FlowySDKTest};
@@ -109,13 +109,14 @@ pub async fn delete_app(sdk: &FlowySDKTest, app_id: &str) {
         .await;
 }
 
-pub async fn create_view(sdk: &FlowySDKTest, app_id: &str, name: &str, desc: &str, view_type: ViewType) -> View {
+pub async fn create_view(sdk: &FlowySDKTest, app_id: &str, name: &str, desc: &str, view_type: ViewDataType) -> View {
     let request = CreateViewPayload {
         belong_to_id: app_id.to_string(),
         name: name.to_string(),
         desc: desc.to_string(),
         thumbnail: None,
-        view_type,
+        data_type: view_type,
+        ext_data: "".to_string(),
     };
     let view = FolderEventBuilder::new(sdk.clone())
         .event(CreateView)

+ 3 - 3
frontend/rust-lib/flowy-folder/tests/workspace/script.rs

@@ -4,7 +4,7 @@ use flowy_folder::{errors::ErrorCode, services::folder_editor::ClientFolderEdito
 use flowy_folder_data_model::entities::{
     app::{App, RepeatedApp},
     trash::Trash,
-    view::{RepeatedView, View, ViewType},
+    view::{RepeatedView, View, ViewDataType},
     workspace::Workspace,
 };
 use flowy_sync::REVISION_WRITE_INTERVAL_IN_MILLIS;
@@ -68,7 +68,7 @@ impl FolderTest {
         let _ = sdk.init_user().await;
         let mut workspace = create_workspace(&sdk, "FolderWorkspace", "Folder test workspace").await;
         let mut app = create_app(&sdk, &workspace.id, "Folder App", "Folder test app").await;
-        let view = create_view(&sdk, &app.id, "Folder View", "Folder test view", ViewType::RichText).await;
+        let view = create_view(&sdk, &app.id, "Folder View", "Folder test view", ViewDataType::RichText).await;
         app.belongings = RepeatedView {
             items: vec![view.clone()],
         };
@@ -146,7 +146,7 @@ impl FolderTest {
             }
 
             FolderScript::CreateView { name, desc } => {
-                let view = create_view(sdk, &self.app.id, &name, &desc, ViewType::RichText).await;
+                let view = create_view(sdk, &self.app.id, &name, &desc, ViewDataType::RichText).await;
                 self.view = view;
             }
             FolderScript::AssertView(view) => {

+ 3 - 1
frontend/rust-lib/flowy-net/src/local_server/server.rs

@@ -300,11 +300,13 @@ impl FolderCouldServiceV1 for LocalServer {
             belong_to_id: params.belong_to_id,
             name: params.name,
             desc: params.desc,
-            view_type: params.view_type,
+            data_type: params.data_type,
             version: 0,
             belongings: RepeatedView::default(),
             modified_time: time,
             create_time: time,
+            ext_data: params.ext_data,
+            thumbnail: params.thumbnail,
         };
         FutureResult::new(async { Ok(view) })
     }

+ 7 - 1
frontend/rust-lib/flowy-sync/src/conflict_resolve.rs

@@ -9,7 +9,8 @@ use flowy_collaboration::{
 };
 use flowy_error::{FlowyError, FlowyResult};
 use lib_infra::future::BoxResultFuture;
-use lib_ot::core::{Attributes, Delta};
+use lib_ot::core::{Attributes, Delta, PlainTextAttributes};
+use lib_ot::rich_text::RichTextAttributes;
 use serde::de::DeserializeOwned;
 use std::{convert::TryFrom, sync::Arc};
 
@@ -29,6 +30,9 @@ pub trait ConflictRevisionSink: Send + Sync + 'static {
     fn ack(&self, rev_id: String, ty: ServerRevisionWSDataType) -> BoxResultFuture<(), FlowyError>;
 }
 
+pub type RichTextConflictController = ConflictController<RichTextAttributes>;
+pub type PlainTextConflictController = ConflictController<PlainTextAttributes>;
+
 pub struct ConflictController<T>
 where
     T: Attributes + Send + Sync,
@@ -171,6 +175,8 @@ where
     }
 }
 
+pub type RichTextTransformDeltas = TransformDeltas<RichTextAttributes>;
+
 pub struct TransformDeltas<T>
 where
     T: Attributes,

+ 2 - 1
frontend/rust-lib/flowy-test/src/helper.rs

@@ -88,7 +88,8 @@ async fn create_view(sdk: &FlowySDKTest, app_id: &str) -> View {
         name: "View A".to_string(),
         desc: "".to_string(),
         thumbnail: Some("http://1.png".to_string()),
-        view_type: ViewType::RichText,
+        data_type: ViewDataType::RichText,
+        ext_data: "".to_string(),
     };
 
     let view = FolderEventBuilder::new(sdk.clone())

+ 4 - 4
shared-lib/flowy-collaboration/src/client_folder/builder.rs

@@ -6,7 +6,7 @@ use crate::{
     errors::{CollaborateError, CollaborateResult},
 };
 use flowy_folder_data_model::entities::{trash::Trash, workspace::Workspace};
-use lib_ot::core::{PlainAttributes, PlainDelta, PlainDeltaBuilder};
+use lib_ot::core::{PlainTextAttributes, PlainTextDelta, PlainTextDeltaBuilder};
 use serde::{Deserialize, Serialize};
 use std::sync::Arc;
 
@@ -34,7 +34,7 @@ impl FolderPadBuilder {
         self
     }
 
-    pub(crate) fn build_with_delta(self, mut delta: PlainDelta) -> CollaborateResult<FolderPad> {
+    pub(crate) fn build_with_delta(self, mut delta: PlainTextDelta) -> CollaborateResult<FolderPad> {
         if delta.is_empty() {
             delta = default_folder_delta();
         }
@@ -47,7 +47,7 @@ impl FolderPadBuilder {
     }
 
     pub(crate) fn build_with_revisions(self, revisions: Vec<Revision>) -> CollaborateResult<FolderPad> {
-        let folder_delta: FolderDelta = make_delta_from_revisions::<PlainAttributes>(revisions)?;
+        let folder_delta: FolderDelta = make_delta_from_revisions::<PlainTextAttributes>(revisions)?;
         self.build_with_delta(folder_delta)
     }
 
@@ -57,7 +57,7 @@ impl FolderPadBuilder {
         Ok(FolderPad {
             workspaces: self.workspaces,
             trash: self.trash,
-            root: PlainDeltaBuilder::new().insert(&json).build(),
+            root: PlainTextDeltaBuilder::new().insert(&json).build(),
         })
     }
 }

+ 8 - 8
shared-lib/flowy-collaboration/src/client_folder/folder_pad.rs

@@ -8,7 +8,7 @@ use crate::{
 };
 use dissimilar::*;
 use flowy_folder_data_model::entities::{app::App, trash::Trash, view::View, workspace::Workspace};
-use lib_ot::core::{Delta, FlowyStr, OperationTransformable, PlainAttributes, PlainDeltaBuilder};
+use lib_ot::core::{FlowyStr, OperationTransformable, PlainTextDelta, PlainTextDeltaBuilder};
 use serde::{Deserialize, Serialize};
 use std::sync::Arc;
 
@@ -21,7 +21,7 @@ pub struct FolderPad {
 }
 
 pub fn default_folder_delta() -> FolderDelta {
-    PlainDeltaBuilder::new()
+    PlainTextDeltaBuilder::new()
         .insert(r#"{"workspaces":[],"trash":[]}"#)
         .build()
 }
@@ -385,9 +385,9 @@ impl FolderPad {
     }
 }
 
-fn cal_diff(old: String, new: String) -> Delta<PlainAttributes> {
+fn cal_diff(old: String, new: String) -> PlainTextDelta {
     let chunks = dissimilar::diff(&old, &new);
-    let mut delta_builder = PlainDeltaBuilder::new();
+    let mut delta_builder = PlainTextDeltaBuilder::new();
     for chunk in &chunks {
         match chunk {
             Chunk::Equal(s) => {
@@ -410,7 +410,7 @@ mod tests {
     use crate::{client_folder::folder_pad::FolderPad, entities::folder_info::FolderDelta};
     use chrono::Utc;
     use flowy_folder_data_model::entities::{app::App, trash::Trash, view::View, workspace::Workspace};
-    use lib_ot::core::{OperationTransformable, PlainDelta, PlainDeltaBuilder};
+    use lib_ot::core::{OperationTransformable, PlainTextDelta, PlainTextDeltaBuilder};
 
     #[test]
     fn folder_add_workspace() {
@@ -725,7 +725,7 @@ mod tests {
     fn test_folder() -> (FolderPad, FolderDelta, Workspace) {
         let mut folder = FolderPad::default();
         let folder_json = serde_json::to_string(&folder).unwrap();
-        let mut delta = PlainDeltaBuilder::new().insert(&folder_json).build();
+        let mut delta = PlainTextDeltaBuilder::new().insert(&folder_json).build();
 
         let mut workspace = Workspace::default();
         workspace.name = "😁 my first workspace".to_owned();
@@ -767,7 +767,7 @@ mod tests {
     fn test_trash() -> (FolderPad, FolderDelta, Trash) {
         let mut folder = FolderPad::default();
         let folder_json = serde_json::to_string(&folder).unwrap();
-        let mut delta = PlainDeltaBuilder::new().insert(&folder_json).build();
+        let mut delta = PlainTextDeltaBuilder::new().insert(&folder_json).build();
 
         let mut trash = Trash::default();
         trash.name = "🚽 my first trash".to_owned();
@@ -780,7 +780,7 @@ mod tests {
         (folder, delta, trash)
     }
 
-    fn make_folder_from_delta(mut initial_delta: FolderDelta, deltas: Vec<PlainDelta>) -> FolderPad {
+    fn make_folder_from_delta(mut initial_delta: FolderDelta, deltas: Vec<PlainTextDelta>) -> FolderPad {
         for delta in deltas {
             initial_delta = initial_delta.compose(&delta).unwrap();
         }

+ 2 - 2
shared-lib/flowy-collaboration/src/entities/folder_info.rs

@@ -1,7 +1,7 @@
 use flowy_derive::ProtoBuf;
-use lib_ot::core::PlainDelta;
+use lib_ot::core::PlainTextDelta;
 
-pub type FolderDelta = PlainDelta;
+pub type FolderDelta = PlainTextDelta;
 
 #[derive(ProtoBuf, Default, Debug, Clone, Eq, PartialEq)]
 pub struct FolderInfo {

+ 2 - 2
shared-lib/flowy-collaboration/src/server_folder/folder_manager.rs

@@ -12,7 +12,7 @@ use crate::{
 use async_stream::stream;
 use futures::stream::StreamExt;
 use lib_infra::future::BoxResultFuture;
-use lib_ot::core::PlainAttributes;
+use lib_ot::core::PlainTextAttributes;
 use std::{collections::HashMap, fmt::Debug, sync::Arc};
 use tokio::{
     sync::{mpsc, oneshot, RwLock},
@@ -187,7 +187,7 @@ impl ServerFolderManager {
     }
 }
 
-type FolderRevisionSynchronizer = RevisionSynchronizer<PlainAttributes>;
+type FolderRevisionSynchronizer = RevisionSynchronizer<PlainTextAttributes>;
 
 struct OpenFolderHandler {
     folder_id: String,

+ 5 - 8
shared-lib/flowy-collaboration/src/server_folder/folder_pad.rs

@@ -1,5 +1,5 @@
 use crate::{entities::folder_info::FolderDelta, errors::CollaborateError, synchronizer::RevisionSyncObject};
-use lib_ot::core::{Delta, OperationTransformable, PlainAttributes};
+use lib_ot::core::{OperationTransformable, PlainTextAttributes, PlainTextDelta};
 
 pub struct ServerFolder {
     folder_id: String,
@@ -15,21 +15,18 @@ impl ServerFolder {
     }
 }
 
-impl RevisionSyncObject<PlainAttributes> for ServerFolder {
+impl RevisionSyncObject<PlainTextAttributes> for ServerFolder {
     fn id(&self) -> &str {
         &self.folder_id
     }
 
-    fn compose(&mut self, other: &Delta<PlainAttributes>) -> Result<(), CollaborateError> {
+    fn compose(&mut self, other: &PlainTextDelta) -> Result<(), CollaborateError> {
         let new_delta = self.delta.compose(other)?;
         self.delta = new_delta;
         Ok(())
     }
 
-    fn transform(
-        &self,
-        other: &Delta<PlainAttributes>,
-    ) -> Result<(Delta<PlainAttributes>, Delta<PlainAttributes>), CollaborateError> {
+    fn transform(&self, other: &PlainTextDelta) -> Result<(PlainTextDelta, PlainTextDelta), CollaborateError> {
         let value = self.delta.transform(other)?;
         Ok(value)
     }
@@ -38,7 +35,7 @@ impl RevisionSyncObject<PlainAttributes> for ServerFolder {
         self.delta.to_json()
     }
 
-    fn set_delta(&mut self, new_delta: Delta<PlainAttributes>) {
+    fn set_delta(&mut self, new_delta: PlainTextDelta) {
         self.delta = new_delta;
     }
 }

+ 32 - 43
shared-lib/flowy-folder-data-model/src/entities/view.rs

@@ -28,7 +28,8 @@ pub struct View {
     pub desc: String,
 
     #[pb(index = 5)]
-    pub view_type: ViewType,
+    #[serde(default)]
+    pub data_type: ViewDataType,
 
     #[pb(index = 6)]
     pub version: i64,
@@ -41,6 +42,12 @@ pub struct View {
 
     #[pb(index = 9)]
     pub create_time: i64,
+
+    #[pb(index = 10)]
+    pub ext_data: String,
+
+    #[pb(index = 11)]
+    pub thumbnail: String,
 }
 
 #[derive(Eq, PartialEq, Debug, Default, ProtoBuf, Clone, Serialize, Deserialize)]
@@ -65,25 +72,25 @@ impl std::convert::From<View> for Trash {
 }
 
 #[derive(Eq, PartialEq, Debug, ProtoBuf_Enum, Clone, Serialize)]
-pub enum ViewType {
+pub enum ViewDataType {
     RichText = 0,
     PlainText = 1,
 }
 
-impl std::default::Default for ViewType {
+impl std::default::Default for ViewDataType {
     fn default() -> Self {
-        ViewType::PlainText
+        ViewDataType::PlainText
     }
 }
 
-impl std::convert::From<i32> for ViewType {
+impl std::convert::From<i32> for ViewDataType {
     fn from(val: i32) -> Self {
         match val {
-            0 => ViewType::RichText,
-            1 => ViewType::PlainText,
+            0 => ViewDataType::RichText,
+            1 => ViewDataType::PlainText,
             _ => {
                 log::error!("Invalid view type: {}", val);
-                ViewType::PlainText
+                ViewDataType::PlainText
             }
         }
     }
@@ -104,10 +111,10 @@ pub struct CreateViewPayload {
     pub thumbnail: Option<String>,
 
     #[pb(index = 5)]
-    pub view_type: ViewType,
+    pub data_type: ViewDataType,
 
     #[pb(index = 6)]
-    pub ext: String,
+    pub ext_data: String,
 }
 
 #[derive(Default, ProtoBuf, Debug, Clone)]
@@ -125,35 +132,16 @@ pub struct CreateViewParams {
     pub thumbnail: String,
 
     #[pb(index = 5)]
-    pub view_type: ViewType,
+    pub data_type: ViewDataType,
 
     #[pb(index = 6)]
-    pub ext: String,
+    pub ext_data: String,
 
     #[pb(index = 7)]
     pub view_id: String,
-}
 
-impl CreateViewParams {
-    pub fn new(
-        belong_to_id: String,
-        name: String,
-        desc: String,
-        view_type: ViewType,
-        thumbnail: String,
-        ext: String,
-        view_id: String,
-    ) -> Self {
-        Self {
-            belong_to_id,
-            name,
-            desc,
-            thumbnail,
-            view_type,
-            ext,
-            view_id,
-        }
-    }
+    #[pb(index = 8)]
+    pub data: String,
 }
 
 impl TryInto<CreateViewParams> for CreateViewPayload {
@@ -163,21 +151,22 @@ impl TryInto<CreateViewParams> for CreateViewPayload {
         let name = ViewName::parse(self.name)?.0;
         let belong_to_id = AppIdentify::parse(self.belong_to_id)?.0;
         let view_id = uuid::Uuid::new_v4().to_string();
-        let ext = ViewExtensionData::parse(self.ext)?.0;
+        let ext_data = ViewExtensionData::parse(self.ext_data)?.0;
         let thumbnail = match self.thumbnail {
             None => "".to_string(),
             Some(thumbnail) => ViewThumbnail::parse(thumbnail)?.0,
         };
 
-        Ok(CreateViewParams::new(
+        Ok(CreateViewParams {
             belong_to_id,
             name,
-            self.desc,
-            self.view_type,
+            desc: self.desc,
+            data_type: self.data_type,
             thumbnail,
-            ext,
+            ext_data,
             view_id,
-        ))
+            data: "".to_string(),
+        })
     }
 }
 
@@ -280,7 +269,7 @@ impl TryInto<UpdateViewParams> for UpdateViewPayload {
     }
 }
 
-impl<'de> Deserialize<'de> for ViewType {
+impl<'de> Deserialize<'de> for ViewDataType {
     fn deserialize<D>(deserializer: D) -> Result<Self, <D as Deserializer<'de>>::Error>
     where
         D: Deserializer<'de>,
@@ -288,7 +277,7 @@ impl<'de> Deserialize<'de> for ViewType {
         struct ViewTypeVisitor();
 
         impl<'de> Visitor<'de> for ViewTypeVisitor {
-            type Value = ViewType;
+            type Value = ViewDataType;
             fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
                 formatter.write_str("Plugin, RichText")
             }
@@ -301,10 +290,10 @@ impl<'de> Deserialize<'de> for ViewType {
                 match s {
                     "Doc" | "RichText" => {
                         // Rename ViewType::Doc to ViewType::RichText, So we need to migrate the ViewType manually.
-                        view_type = ViewType::RichText;
+                        view_type = ViewDataType::RichText;
                     }
                     "Plugin" => {
-                        view_type = ViewType::PlainText;
+                        view_type = ViewDataType::PlainText;
                     }
                     unknown => {
                         return Err(de::Error::invalid_value(Unexpected::Str(unknown), &self));

+ 267 - 138
shared-lib/flowy-folder-data-model/src/protobuf/model/view.rs

@@ -30,11 +30,13 @@ pub struct View {
     pub belong_to_id: ::std::string::String,
     pub name: ::std::string::String,
     pub desc: ::std::string::String,
-    pub view_type: ViewType,
+    pub data_type: ViewDataType,
     pub version: i64,
     pub belongings: ::protobuf::SingularPtrField<RepeatedView>,
     pub modified_time: i64,
     pub create_time: i64,
+    pub ext_data: ::std::string::String,
+    pub thumbnail: ::std::string::String,
     // special fields
     pub unknown_fields: ::protobuf::UnknownFields,
     pub cached_size: ::protobuf::CachedSize,
@@ -155,19 +157,19 @@ impl View {
         ::std::mem::replace(&mut self.desc, ::std::string::String::new())
     }
 
-    // .ViewType view_type = 5;
+    // .ViewDataType data_type = 5;
 
 
-    pub fn get_view_type(&self) -> ViewType {
-        self.view_type
+    pub fn get_data_type(&self) -> ViewDataType {
+        self.data_type
     }
-    pub fn clear_view_type(&mut self) {
-        self.view_type = ViewType::RichText;
+    pub fn clear_data_type(&mut self) {
+        self.data_type = ViewDataType::RichText;
     }
 
     // Param is passed by value, moved
-    pub fn set_view_type(&mut self, v: ViewType) {
-        self.view_type = v;
+    pub fn set_data_type(&mut self, v: ViewDataType) {
+        self.data_type = v;
     }
 
     // int64 version = 6;
@@ -247,6 +249,58 @@ impl View {
     pub fn set_create_time(&mut self, v: i64) {
         self.create_time = v;
     }
+
+    // string ext_data = 10;
+
+
+    pub fn get_ext_data(&self) -> &str {
+        &self.ext_data
+    }
+    pub fn clear_ext_data(&mut self) {
+        self.ext_data.clear();
+    }
+
+    // Param is passed by value, moved
+    pub fn set_ext_data(&mut self, v: ::std::string::String) {
+        self.ext_data = v;
+    }
+
+    // Mutable pointer to the field.
+    // If field is not initialized, it is initialized with default value first.
+    pub fn mut_ext_data(&mut self) -> &mut ::std::string::String {
+        &mut self.ext_data
+    }
+
+    // Take field
+    pub fn take_ext_data(&mut self) -> ::std::string::String {
+        ::std::mem::replace(&mut self.ext_data, ::std::string::String::new())
+    }
+
+    // string thumbnail = 11;
+
+
+    pub fn get_thumbnail(&self) -> &str {
+        &self.thumbnail
+    }
+    pub fn clear_thumbnail(&mut self) {
+        self.thumbnail.clear();
+    }
+
+    // Param is passed by value, moved
+    pub fn set_thumbnail(&mut self, v: ::std::string::String) {
+        self.thumbnail = v;
+    }
+
+    // Mutable pointer to the field.
+    // If field is not initialized, it is initialized with default value first.
+    pub fn mut_thumbnail(&mut self) -> &mut ::std::string::String {
+        &mut self.thumbnail
+    }
+
+    // Take field
+    pub fn take_thumbnail(&mut self) -> ::std::string::String {
+        ::std::mem::replace(&mut self.thumbnail, ::std::string::String::new())
+    }
 }
 
 impl ::protobuf::Message for View {
@@ -276,7 +330,7 @@ impl ::protobuf::Message for View {
                     ::protobuf::rt::read_singular_proto3_string_into(wire_type, is, &mut self.desc)?;
                 },
                 5 => {
-                    ::protobuf::rt::read_proto3_enum_with_unknown_fields_into(wire_type, is, &mut self.view_type, 5, &mut self.unknown_fields)?
+                    ::protobuf::rt::read_proto3_enum_with_unknown_fields_into(wire_type, is, &mut self.data_type, 5, &mut self.unknown_fields)?
                 },
                 6 => {
                     if wire_type != ::protobuf::wire_format::WireTypeVarint {
@@ -302,6 +356,12 @@ impl ::protobuf::Message for View {
                     let tmp = is.read_int64()?;
                     self.create_time = tmp;
                 },
+                10 => {
+                    ::protobuf::rt::read_singular_proto3_string_into(wire_type, is, &mut self.ext_data)?;
+                },
+                11 => {
+                    ::protobuf::rt::read_singular_proto3_string_into(wire_type, is, &mut self.thumbnail)?;
+                },
                 _ => {
                     ::protobuf::rt::read_unknown_or_skip_group(field_number, wire_type, is, self.mut_unknown_fields())?;
                 },
@@ -326,8 +386,8 @@ impl ::protobuf::Message for View {
         if !self.desc.is_empty() {
             my_size += ::protobuf::rt::string_size(4, &self.desc);
         }
-        if self.view_type != ViewType::RichText {
-            my_size += ::protobuf::rt::enum_size(5, self.view_type);
+        if self.data_type != ViewDataType::RichText {
+            my_size += ::protobuf::rt::enum_size(5, self.data_type);
         }
         if self.version != 0 {
             my_size += ::protobuf::rt::value_size(6, self.version, ::protobuf::wire_format::WireTypeVarint);
@@ -342,6 +402,12 @@ impl ::protobuf::Message for View {
         if self.create_time != 0 {
             my_size += ::protobuf::rt::value_size(9, self.create_time, ::protobuf::wire_format::WireTypeVarint);
         }
+        if !self.ext_data.is_empty() {
+            my_size += ::protobuf::rt::string_size(10, &self.ext_data);
+        }
+        if !self.thumbnail.is_empty() {
+            my_size += ::protobuf::rt::string_size(11, &self.thumbnail);
+        }
         my_size += ::protobuf::rt::unknown_fields_size(self.get_unknown_fields());
         self.cached_size.set(my_size);
         my_size
@@ -360,8 +426,8 @@ impl ::protobuf::Message for View {
         if !self.desc.is_empty() {
             os.write_string(4, &self.desc)?;
         }
-        if self.view_type != ViewType::RichText {
-            os.write_enum(5, ::protobuf::ProtobufEnum::value(&self.view_type))?;
+        if self.data_type != ViewDataType::RichText {
+            os.write_enum(5, ::protobuf::ProtobufEnum::value(&self.data_type))?;
         }
         if self.version != 0 {
             os.write_int64(6, self.version)?;
@@ -377,6 +443,12 @@ impl ::protobuf::Message for View {
         if self.create_time != 0 {
             os.write_int64(9, self.create_time)?;
         }
+        if !self.ext_data.is_empty() {
+            os.write_string(10, &self.ext_data)?;
+        }
+        if !self.thumbnail.is_empty() {
+            os.write_string(11, &self.thumbnail)?;
+        }
         os.write_unknown_fields(self.get_unknown_fields())?;
         ::std::result::Result::Ok(())
     }
@@ -435,10 +507,10 @@ impl ::protobuf::Message for View {
                 |m: &View| { &m.desc },
                 |m: &mut View| { &mut m.desc },
             ));
-            fields.push(::protobuf::reflect::accessor::make_simple_field_accessor::<_, ::protobuf::types::ProtobufTypeEnum<ViewType>>(
-                "view_type",
-                |m: &View| { &m.view_type },
-                |m: &mut View| { &mut m.view_type },
+            fields.push(::protobuf::reflect::accessor::make_simple_field_accessor::<_, ::protobuf::types::ProtobufTypeEnum<ViewDataType>>(
+                "data_type",
+                |m: &View| { &m.data_type },
+                |m: &mut View| { &mut m.data_type },
             ));
             fields.push(::protobuf::reflect::accessor::make_simple_field_accessor::<_, ::protobuf::types::ProtobufTypeInt64>(
                 "version",
@@ -460,6 +532,16 @@ impl ::protobuf::Message for View {
                 |m: &View| { &m.create_time },
                 |m: &mut View| { &mut m.create_time },
             ));
+            fields.push(::protobuf::reflect::accessor::make_simple_field_accessor::<_, ::protobuf::types::ProtobufTypeString>(
+                "ext_data",
+                |m: &View| { &m.ext_data },
+                |m: &mut View| { &mut m.ext_data },
+            ));
+            fields.push(::protobuf::reflect::accessor::make_simple_field_accessor::<_, ::protobuf::types::ProtobufTypeString>(
+                "thumbnail",
+                |m: &View| { &m.thumbnail },
+                |m: &mut View| { &mut m.thumbnail },
+            ));
             ::protobuf::reflect::MessageDescriptor::new_pb_name::<View>(
                 "View",
                 fields,
@@ -480,11 +562,13 @@ impl ::protobuf::Clear for View {
         self.belong_to_id.clear();
         self.name.clear();
         self.desc.clear();
-        self.view_type = ViewType::RichText;
+        self.data_type = ViewDataType::RichText;
         self.version = 0;
         self.belongings.clear();
         self.modified_time = 0;
         self.create_time = 0;
+        self.ext_data.clear();
+        self.thumbnail.clear();
         self.unknown_fields.clear();
     }
 }
@@ -673,8 +757,8 @@ pub struct CreateViewPayload {
     pub belong_to_id: ::std::string::String,
     pub name: ::std::string::String,
     pub desc: ::std::string::String,
-    pub view_type: ViewType,
-    pub ext: ::std::string::String,
+    pub data_type: ViewDataType,
+    pub ext_data: ::std::string::String,
     // message oneof groups
     pub one_of_thumbnail: ::std::option::Option<CreateViewPayload_oneof_one_of_thumbnail>,
     // special fields
@@ -825,45 +909,45 @@ impl CreateViewPayload {
         }
     }
 
-    // .ViewType view_type = 5;
+    // .ViewDataType data_type = 5;
 
 
-    pub fn get_view_type(&self) -> ViewType {
-        self.view_type
+    pub fn get_data_type(&self) -> ViewDataType {
+        self.data_type
     }
-    pub fn clear_view_type(&mut self) {
-        self.view_type = ViewType::RichText;
+    pub fn clear_data_type(&mut self) {
+        self.data_type = ViewDataType::RichText;
     }
 
     // Param is passed by value, moved
-    pub fn set_view_type(&mut self, v: ViewType) {
-        self.view_type = v;
+    pub fn set_data_type(&mut self, v: ViewDataType) {
+        self.data_type = v;
     }
 
-    // string ext = 6;
+    // string ext_data = 6;
 
 
-    pub fn get_ext(&self) -> &str {
-        &self.ext
+    pub fn get_ext_data(&self) -> &str {
+        &self.ext_data
     }
-    pub fn clear_ext(&mut self) {
-        self.ext.clear();
+    pub fn clear_ext_data(&mut self) {
+        self.ext_data.clear();
     }
 
     // Param is passed by value, moved
-    pub fn set_ext(&mut self, v: ::std::string::String) {
-        self.ext = v;
+    pub fn set_ext_data(&mut self, v: ::std::string::String) {
+        self.ext_data = v;
     }
 
     // Mutable pointer to the field.
     // If field is not initialized, it is initialized with default value first.
-    pub fn mut_ext(&mut self) -> &mut ::std::string::String {
-        &mut self.ext
+    pub fn mut_ext_data(&mut self) -> &mut ::std::string::String {
+        &mut self.ext_data
     }
 
     // Take field
-    pub fn take_ext(&mut self) -> ::std::string::String {
-        ::std::mem::replace(&mut self.ext, ::std::string::String::new())
+    pub fn take_ext_data(&mut self) -> ::std::string::String {
+        ::std::mem::replace(&mut self.ext_data, ::std::string::String::new())
     }
 }
 
@@ -892,10 +976,10 @@ impl ::protobuf::Message for CreateViewPayload {
                     self.one_of_thumbnail = ::std::option::Option::Some(CreateViewPayload_oneof_one_of_thumbnail::thumbnail(is.read_string()?));
                 },
                 5 => {
-                    ::protobuf::rt::read_proto3_enum_with_unknown_fields_into(wire_type, is, &mut self.view_type, 5, &mut self.unknown_fields)?
+                    ::protobuf::rt::read_proto3_enum_with_unknown_fields_into(wire_type, is, &mut self.data_type, 5, &mut self.unknown_fields)?
                 },
                 6 => {
-                    ::protobuf::rt::read_singular_proto3_string_into(wire_type, is, &mut self.ext)?;
+                    ::protobuf::rt::read_singular_proto3_string_into(wire_type, is, &mut self.ext_data)?;
                 },
                 _ => {
                     ::protobuf::rt::read_unknown_or_skip_group(field_number, wire_type, is, self.mut_unknown_fields())?;
@@ -918,11 +1002,11 @@ impl ::protobuf::Message for CreateViewPayload {
         if !self.desc.is_empty() {
             my_size += ::protobuf::rt::string_size(3, &self.desc);
         }
-        if self.view_type != ViewType::RichText {
-            my_size += ::protobuf::rt::enum_size(5, self.view_type);
+        if self.data_type != ViewDataType::RichText {
+            my_size += ::protobuf::rt::enum_size(5, self.data_type);
         }
-        if !self.ext.is_empty() {
-            my_size += ::protobuf::rt::string_size(6, &self.ext);
+        if !self.ext_data.is_empty() {
+            my_size += ::protobuf::rt::string_size(6, &self.ext_data);
         }
         if let ::std::option::Option::Some(ref v) = self.one_of_thumbnail {
             match v {
@@ -946,11 +1030,11 @@ impl ::protobuf::Message for CreateViewPayload {
         if !self.desc.is_empty() {
             os.write_string(3, &self.desc)?;
         }
-        if self.view_type != ViewType::RichText {
-            os.write_enum(5, ::protobuf::ProtobufEnum::value(&self.view_type))?;
+        if self.data_type != ViewDataType::RichText {
+            os.write_enum(5, ::protobuf::ProtobufEnum::value(&self.data_type))?;
         }
-        if !self.ext.is_empty() {
-            os.write_string(6, &self.ext)?;
+        if !self.ext_data.is_empty() {
+            os.write_string(6, &self.ext_data)?;
         }
         if let ::std::option::Option::Some(ref v) = self.one_of_thumbnail {
             match v {
@@ -1017,15 +1101,15 @@ impl ::protobuf::Message for CreateViewPayload {
                 CreateViewPayload::has_thumbnail,
                 CreateViewPayload::get_thumbnail,
             ));
-            fields.push(::protobuf::reflect::accessor::make_simple_field_accessor::<_, ::protobuf::types::ProtobufTypeEnum<ViewType>>(
-                "view_type",
-                |m: &CreateViewPayload| { &m.view_type },
-                |m: &mut CreateViewPayload| { &mut m.view_type },
+            fields.push(::protobuf::reflect::accessor::make_simple_field_accessor::<_, ::protobuf::types::ProtobufTypeEnum<ViewDataType>>(
+                "data_type",
+                |m: &CreateViewPayload| { &m.data_type },
+                |m: &mut CreateViewPayload| { &mut m.data_type },
             ));
             fields.push(::protobuf::reflect::accessor::make_simple_field_accessor::<_, ::protobuf::types::ProtobufTypeString>(
-                "ext",
-                |m: &CreateViewPayload| { &m.ext },
-                |m: &mut CreateViewPayload| { &mut m.ext },
+                "ext_data",
+                |m: &CreateViewPayload| { &m.ext_data },
+                |m: &mut CreateViewPayload| { &mut m.ext_data },
             ));
             ::protobuf::reflect::MessageDescriptor::new_pb_name::<CreateViewPayload>(
                 "CreateViewPayload",
@@ -1047,8 +1131,8 @@ impl ::protobuf::Clear for CreateViewPayload {
         self.name.clear();
         self.desc.clear();
         self.one_of_thumbnail = ::std::option::Option::None;
-        self.view_type = ViewType::RichText;
-        self.ext.clear();
+        self.data_type = ViewDataType::RichText;
+        self.ext_data.clear();
         self.unknown_fields.clear();
     }
 }
@@ -1072,9 +1156,10 @@ pub struct CreateViewParams {
     pub name: ::std::string::String,
     pub desc: ::std::string::String,
     pub thumbnail: ::std::string::String,
-    pub view_type: ViewType,
-    pub ext: ::std::string::String,
+    pub data_type: ViewDataType,
+    pub ext_data: ::std::string::String,
     pub view_id: ::std::string::String,
+    pub data: ::std::string::String,
     // special fields
     pub unknown_fields: ::protobuf::UnknownFields,
     pub cached_size: ::protobuf::CachedSize,
@@ -1195,45 +1280,45 @@ impl CreateViewParams {
         ::std::mem::replace(&mut self.thumbnail, ::std::string::String::new())
     }
 
-    // .ViewType view_type = 5;
+    // .ViewDataType data_type = 5;
 
 
-    pub fn get_view_type(&self) -> ViewType {
-        self.view_type
+    pub fn get_data_type(&self) -> ViewDataType {
+        self.data_type
     }
-    pub fn clear_view_type(&mut self) {
-        self.view_type = ViewType::RichText;
+    pub fn clear_data_type(&mut self) {
+        self.data_type = ViewDataType::RichText;
     }
 
     // Param is passed by value, moved
-    pub fn set_view_type(&mut self, v: ViewType) {
-        self.view_type = v;
+    pub fn set_data_type(&mut self, v: ViewDataType) {
+        self.data_type = v;
     }
 
-    // string ext = 6;
+    // string ext_data = 6;
 
 
-    pub fn get_ext(&self) -> &str {
-        &self.ext
+    pub fn get_ext_data(&self) -> &str {
+        &self.ext_data
     }
-    pub fn clear_ext(&mut self) {
-        self.ext.clear();
+    pub fn clear_ext_data(&mut self) {
+        self.ext_data.clear();
     }
 
     // Param is passed by value, moved
-    pub fn set_ext(&mut self, v: ::std::string::String) {
-        self.ext = v;
+    pub fn set_ext_data(&mut self, v: ::std::string::String) {
+        self.ext_data = v;
     }
 
     // Mutable pointer to the field.
     // If field is not initialized, it is initialized with default value first.
-    pub fn mut_ext(&mut self) -> &mut ::std::string::String {
-        &mut self.ext
+    pub fn mut_ext_data(&mut self) -> &mut ::std::string::String {
+        &mut self.ext_data
     }
 
     // Take field
-    pub fn take_ext(&mut self) -> ::std::string::String {
-        ::std::mem::replace(&mut self.ext, ::std::string::String::new())
+    pub fn take_ext_data(&mut self) -> ::std::string::String {
+        ::std::mem::replace(&mut self.ext_data, ::std::string::String::new())
     }
 
     // string view_id = 7;
@@ -1261,6 +1346,32 @@ impl CreateViewParams {
     pub fn take_view_id(&mut self) -> ::std::string::String {
         ::std::mem::replace(&mut self.view_id, ::std::string::String::new())
     }
+
+    // string data = 8;
+
+
+    pub fn get_data(&self) -> &str {
+        &self.data
+    }
+    pub fn clear_data(&mut self) {
+        self.data.clear();
+    }
+
+    // Param is passed by value, moved
+    pub fn set_data(&mut self, v: ::std::string::String) {
+        self.data = v;
+    }
+
+    // Mutable pointer to the field.
+    // If field is not initialized, it is initialized with default value first.
+    pub fn mut_data(&mut self) -> &mut ::std::string::String {
+        &mut self.data
+    }
+
+    // Take field
+    pub fn take_data(&mut self) -> ::std::string::String {
+        ::std::mem::replace(&mut self.data, ::std::string::String::new())
+    }
 }
 
 impl ::protobuf::Message for CreateViewParams {
@@ -1285,14 +1396,17 @@ impl ::protobuf::Message for CreateViewParams {
                     ::protobuf::rt::read_singular_proto3_string_into(wire_type, is, &mut self.thumbnail)?;
                 },
                 5 => {
-                    ::protobuf::rt::read_proto3_enum_with_unknown_fields_into(wire_type, is, &mut self.view_type, 5, &mut self.unknown_fields)?
+                    ::protobuf::rt::read_proto3_enum_with_unknown_fields_into(wire_type, is, &mut self.data_type, 5, &mut self.unknown_fields)?
                 },
                 6 => {
-                    ::protobuf::rt::read_singular_proto3_string_into(wire_type, is, &mut self.ext)?;
+                    ::protobuf::rt::read_singular_proto3_string_into(wire_type, is, &mut self.ext_data)?;
                 },
                 7 => {
                     ::protobuf::rt::read_singular_proto3_string_into(wire_type, is, &mut self.view_id)?;
                 },
+                8 => {
+                    ::protobuf::rt::read_singular_proto3_string_into(wire_type, is, &mut self.data)?;
+                },
                 _ => {
                     ::protobuf::rt::read_unknown_or_skip_group(field_number, wire_type, is, self.mut_unknown_fields())?;
                 },
@@ -1317,15 +1431,18 @@ impl ::protobuf::Message for CreateViewParams {
         if !self.thumbnail.is_empty() {
             my_size += ::protobuf::rt::string_size(4, &self.thumbnail);
         }
-        if self.view_type != ViewType::RichText {
-            my_size += ::protobuf::rt::enum_size(5, self.view_type);
+        if self.data_type != ViewDataType::RichText {
+            my_size += ::protobuf::rt::enum_size(5, self.data_type);
         }
-        if !self.ext.is_empty() {
-            my_size += ::protobuf::rt::string_size(6, &self.ext);
+        if !self.ext_data.is_empty() {
+            my_size += ::protobuf::rt::string_size(6, &self.ext_data);
         }
         if !self.view_id.is_empty() {
             my_size += ::protobuf::rt::string_size(7, &self.view_id);
         }
+        if !self.data.is_empty() {
+            my_size += ::protobuf::rt::string_size(8, &self.data);
+        }
         my_size += ::protobuf::rt::unknown_fields_size(self.get_unknown_fields());
         self.cached_size.set(my_size);
         my_size
@@ -1344,15 +1461,18 @@ impl ::protobuf::Message for CreateViewParams {
         if !self.thumbnail.is_empty() {
             os.write_string(4, &self.thumbnail)?;
         }
-        if self.view_type != ViewType::RichText {
-            os.write_enum(5, ::protobuf::ProtobufEnum::value(&self.view_type))?;
+        if self.data_type != ViewDataType::RichText {
+            os.write_enum(5, ::protobuf::ProtobufEnum::value(&self.data_type))?;
         }
-        if !self.ext.is_empty() {
-            os.write_string(6, &self.ext)?;
+        if !self.ext_data.is_empty() {
+            os.write_string(6, &self.ext_data)?;
         }
         if !self.view_id.is_empty() {
             os.write_string(7, &self.view_id)?;
         }
+        if !self.data.is_empty() {
+            os.write_string(8, &self.data)?;
+        }
         os.write_unknown_fields(self.get_unknown_fields())?;
         ::std::result::Result::Ok(())
     }
@@ -1411,21 +1531,26 @@ impl ::protobuf::Message for CreateViewParams {
                 |m: &CreateViewParams| { &m.thumbnail },
                 |m: &mut CreateViewParams| { &mut m.thumbnail },
             ));
-            fields.push(::protobuf::reflect::accessor::make_simple_field_accessor::<_, ::protobuf::types::ProtobufTypeEnum<ViewType>>(
-                "view_type",
-                |m: &CreateViewParams| { &m.view_type },
-                |m: &mut CreateViewParams| { &mut m.view_type },
+            fields.push(::protobuf::reflect::accessor::make_simple_field_accessor::<_, ::protobuf::types::ProtobufTypeEnum<ViewDataType>>(
+                "data_type",
+                |m: &CreateViewParams| { &m.data_type },
+                |m: &mut CreateViewParams| { &mut m.data_type },
             ));
             fields.push(::protobuf::reflect::accessor::make_simple_field_accessor::<_, ::protobuf::types::ProtobufTypeString>(
-                "ext",
-                |m: &CreateViewParams| { &m.ext },
-                |m: &mut CreateViewParams| { &mut m.ext },
+                "ext_data",
+                |m: &CreateViewParams| { &m.ext_data },
+                |m: &mut CreateViewParams| { &mut m.ext_data },
             ));
             fields.push(::protobuf::reflect::accessor::make_simple_field_accessor::<_, ::protobuf::types::ProtobufTypeString>(
                 "view_id",
                 |m: &CreateViewParams| { &m.view_id },
                 |m: &mut CreateViewParams| { &mut m.view_id },
             ));
+            fields.push(::protobuf::reflect::accessor::make_simple_field_accessor::<_, ::protobuf::types::ProtobufTypeString>(
+                "data",
+                |m: &CreateViewParams| { &m.data },
+                |m: &mut CreateViewParams| { &mut m.data },
+            ));
             ::protobuf::reflect::MessageDescriptor::new_pb_name::<CreateViewParams>(
                 "CreateViewParams",
                 fields,
@@ -1446,9 +1571,10 @@ impl ::protobuf::Clear for CreateViewParams {
         self.name.clear();
         self.desc.clear();
         self.thumbnail.clear();
-        self.view_type = ViewType::RichText;
-        self.ext.clear();
+        self.data_type = ViewDataType::RichText;
+        self.ext_data.clear();
         self.view_id.clear();
+        self.data.clear();
         self.unknown_fields.clear();
     }
 }
@@ -2589,28 +2715,28 @@ impl ::protobuf::reflect::ProtobufValue for UpdateViewParams {
 }
 
 #[derive(Clone,PartialEq,Eq,Debug,Hash)]
-pub enum ViewType {
+pub enum ViewDataType {
     RichText = 0,
     PlainText = 1,
 }
 
-impl ::protobuf::ProtobufEnum for ViewType {
+impl ::protobuf::ProtobufEnum for ViewDataType {
     fn value(&self) -> i32 {
         *self as i32
     }
 
-    fn from_i32(value: i32) -> ::std::option::Option<ViewType> {
+    fn from_i32(value: i32) -> ::std::option::Option<ViewDataType> {
         match value {
-            0 => ::std::option::Option::Some(ViewType::RichText),
-            1 => ::std::option::Option::Some(ViewType::PlainText),
+            0 => ::std::option::Option::Some(ViewDataType::RichText),
+            1 => ::std::option::Option::Some(ViewDataType::PlainText),
             _ => ::std::option::Option::None
         }
     }
 
     fn values() -> &'static [Self] {
-        static values: &'static [ViewType] = &[
-            ViewType::RichText,
-            ViewType::PlainText,
+        static values: &'static [ViewDataType] = &[
+            ViewDataType::RichText,
+            ViewDataType::PlainText,
         ];
         values
     }
@@ -2618,58 +2744,61 @@ impl ::protobuf::ProtobufEnum for ViewType {
     fn enum_descriptor_static() -> &'static ::protobuf::reflect::EnumDescriptor {
         static descriptor: ::protobuf::rt::LazyV2<::protobuf::reflect::EnumDescriptor> = ::protobuf::rt::LazyV2::INIT;
         descriptor.get(|| {
-            ::protobuf::reflect::EnumDescriptor::new_pb_name::<ViewType>("ViewType", file_descriptor_proto())
+            ::protobuf::reflect::EnumDescriptor::new_pb_name::<ViewDataType>("ViewDataType", file_descriptor_proto())
         })
     }
 }
 
-impl ::std::marker::Copy for ViewType {
+impl ::std::marker::Copy for ViewDataType {
 }
 
-impl ::std::default::Default for ViewType {
+impl ::std::default::Default for ViewDataType {
     fn default() -> Self {
-        ViewType::RichText
+        ViewDataType::RichText
     }
 }
 
-impl ::protobuf::reflect::ProtobufValue for ViewType {
+impl ::protobuf::reflect::ProtobufValue for ViewDataType {
     fn as_ref(&self) -> ::protobuf::reflect::ReflectValueRef {
         ::protobuf::reflect::ReflectValueRef::Enum(::protobuf::ProtobufEnum::descriptor(self))
     }
 }
 
 static file_descriptor_proto_data: &'static [u8] = b"\
-    \n\nview.proto\"\x97\x02\n\x04View\x12\x0e\n\x02id\x18\x01\x20\x01(\tR\
+    \n\nview.proto\"\xd4\x02\n\x04View\x12\x0e\n\x02id\x18\x01\x20\x01(\tR\
     \x02id\x12\x20\n\x0cbelong_to_id\x18\x02\x20\x01(\tR\nbelongToId\x12\x12\
     \n\x04name\x18\x03\x20\x01(\tR\x04name\x12\x12\n\x04desc\x18\x04\x20\x01\
-    (\tR\x04desc\x12&\n\tview_type\x18\x05\x20\x01(\x0e2\t.ViewTypeR\x08view\
-    Type\x12\x18\n\x07version\x18\x06\x20\x01(\x03R\x07version\x12-\n\nbelon\
-    gings\x18\x07\x20\x01(\x0b2\r.RepeatedViewR\nbelongings\x12#\n\rmodified\
-    _time\x18\x08\x20\x01(\x03R\x0cmodifiedTime\x12\x1f\n\x0bcreate_time\x18\
-    \t\x20\x01(\x03R\ncreateTime\"+\n\x0cRepeatedView\x12\x1b\n\x05items\x18\
-    \x01\x20\x03(\x0b2\x05.ViewR\x05items\"\xcb\x01\n\x11CreateViewPayload\
-    \x12\x20\n\x0cbelong_to_id\x18\x01\x20\x01(\tR\nbelongToId\x12\x12\n\x04\
-    name\x18\x02\x20\x01(\tR\x04name\x12\x12\n\x04desc\x18\x03\x20\x01(\tR\
-    \x04desc\x12\x1e\n\tthumbnail\x18\x04\x20\x01(\tH\0R\tthumbnail\x12&\n\t\
-    view_type\x18\x05\x20\x01(\x0e2\t.ViewTypeR\x08viewType\x12\x10\n\x03ext\
-    \x18\x06\x20\x01(\tR\x03extB\x12\n\x10one_of_thumbnail\"\xcd\x01\n\x10Cr\
-    eateViewParams\x12\x20\n\x0cbelong_to_id\x18\x01\x20\x01(\tR\nbelongToId\
-    \x12\x12\n\x04name\x18\x02\x20\x01(\tR\x04name\x12\x12\n\x04desc\x18\x03\
-    \x20\x01(\tR\x04desc\x12\x1c\n\tthumbnail\x18\x04\x20\x01(\tR\tthumbnail\
-    \x12&\n\tview_type\x18\x05\x20\x01(\x0e2\t.ViewTypeR\x08viewType\x12\x10\
-    \n\x03ext\x18\x06\x20\x01(\tR\x03ext\x12\x17\n\x07view_id\x18\x07\x20\
-    \x01(\tR\x06viewId\"\x1e\n\x06ViewId\x12\x14\n\x05value\x18\x01\x20\x01(\
-    \tR\x05value\"&\n\x0eRepeatedViewId\x12\x14\n\x05items\x18\x01\x20\x03(\
-    \tR\x05items\"\xaa\x01\n\x11UpdateViewPayload\x12\x17\n\x07view_id\x18\
-    \x01\x20\x01(\tR\x06viewId\x12\x14\n\x04name\x18\x02\x20\x01(\tH\0R\x04n\
-    ame\x12\x14\n\x04desc\x18\x03\x20\x01(\tH\x01R\x04desc\x12\x1e\n\tthumbn\
-    ail\x18\x04\x20\x01(\tH\x02R\tthumbnailB\r\n\x0bone_of_nameB\r\n\x0bone_\
-    of_descB\x12\n\x10one_of_thumbnail\"\xa9\x01\n\x10UpdateViewParams\x12\
-    \x17\n\x07view_id\x18\x01\x20\x01(\tR\x06viewId\x12\x14\n\x04name\x18\
-    \x02\x20\x01(\tH\0R\x04name\x12\x14\n\x04desc\x18\x03\x20\x01(\tH\x01R\
-    \x04desc\x12\x1e\n\tthumbnail\x18\x04\x20\x01(\tH\x02R\tthumbnailB\r\n\
-    \x0bone_of_nameB\r\n\x0bone_of_descB\x12\n\x10one_of_thumbnail*'\n\x08Vi\
-    ewType\x12\x0c\n\x08RichText\x10\0\x12\r\n\tPlainText\x10\x01b\x06proto3\
+    (\tR\x04desc\x12*\n\tdata_type\x18\x05\x20\x01(\x0e2\r.ViewDataTypeR\x08\
+    dataType\x12\x18\n\x07version\x18\x06\x20\x01(\x03R\x07version\x12-\n\nb\
+    elongings\x18\x07\x20\x01(\x0b2\r.RepeatedViewR\nbelongings\x12#\n\rmodi\
+    fied_time\x18\x08\x20\x01(\x03R\x0cmodifiedTime\x12\x1f\n\x0bcreate_time\
+    \x18\t\x20\x01(\x03R\ncreateTime\x12\x19\n\x08ext_data\x18\n\x20\x01(\tR\
+    \x07extData\x12\x1c\n\tthumbnail\x18\x0b\x20\x01(\tR\tthumbnail\"+\n\x0c\
+    RepeatedView\x12\x1b\n\x05items\x18\x01\x20\x03(\x0b2\x05.ViewR\x05items\
+    \"\xd8\x01\n\x11CreateViewPayload\x12\x20\n\x0cbelong_to_id\x18\x01\x20\
+    \x01(\tR\nbelongToId\x12\x12\n\x04name\x18\x02\x20\x01(\tR\x04name\x12\
+    \x12\n\x04desc\x18\x03\x20\x01(\tR\x04desc\x12\x1e\n\tthumbnail\x18\x04\
+    \x20\x01(\tH\0R\tthumbnail\x12*\n\tdata_type\x18\x05\x20\x01(\x0e2\r.Vie\
+    wDataTypeR\x08dataType\x12\x19\n\x08ext_data\x18\x06\x20\x01(\tR\x07extD\
+    ataB\x12\n\x10one_of_thumbnail\"\xee\x01\n\x10CreateViewParams\x12\x20\n\
+    \x0cbelong_to_id\x18\x01\x20\x01(\tR\nbelongToId\x12\x12\n\x04name\x18\
+    \x02\x20\x01(\tR\x04name\x12\x12\n\x04desc\x18\x03\x20\x01(\tR\x04desc\
+    \x12\x1c\n\tthumbnail\x18\x04\x20\x01(\tR\tthumbnail\x12*\n\tdata_type\
+    \x18\x05\x20\x01(\x0e2\r.ViewDataTypeR\x08dataType\x12\x19\n\x08ext_data\
+    \x18\x06\x20\x01(\tR\x07extData\x12\x17\n\x07view_id\x18\x07\x20\x01(\tR\
+    \x06viewId\x12\x12\n\x04data\x18\x08\x20\x01(\tR\x04data\"\x1e\n\x06View\
+    Id\x12\x14\n\x05value\x18\x01\x20\x01(\tR\x05value\"&\n\x0eRepeatedViewI\
+    d\x12\x14\n\x05items\x18\x01\x20\x03(\tR\x05items\"\xaa\x01\n\x11UpdateV\
+    iewPayload\x12\x17\n\x07view_id\x18\x01\x20\x01(\tR\x06viewId\x12\x14\n\
+    \x04name\x18\x02\x20\x01(\tH\0R\x04name\x12\x14\n\x04desc\x18\x03\x20\
+    \x01(\tH\x01R\x04desc\x12\x1e\n\tthumbnail\x18\x04\x20\x01(\tH\x02R\tthu\
+    mbnailB\r\n\x0bone_of_nameB\r\n\x0bone_of_descB\x12\n\x10one_of_thumbnai\
+    l\"\xa9\x01\n\x10UpdateViewParams\x12\x17\n\x07view_id\x18\x01\x20\x01(\
+    \tR\x06viewId\x12\x14\n\x04name\x18\x02\x20\x01(\tH\0R\x04name\x12\x14\n\
+    \x04desc\x18\x03\x20\x01(\tH\x01R\x04desc\x12\x1e\n\tthumbnail\x18\x04\
+    \x20\x01(\tH\x02R\tthumbnailB\r\n\x0bone_of_nameB\r\n\x0bone_of_descB\
+    \x12\n\x10one_of_thumbnail*+\n\x0cViewDataType\x12\x0c\n\x08RichText\x10\
+    \0\x12\r\n\tPlainText\x10\x01b\x06proto3\
 ";
 
 static file_descriptor_proto_lazy: ::protobuf::rt::LazyV2<::protobuf::descriptor::FileDescriptorProto> = ::protobuf::rt::LazyV2::INIT;

+ 9 - 6
shared-lib/flowy-folder-data-model/src/protobuf/proto/view.proto

@@ -5,11 +5,13 @@ message View {
     string belong_to_id = 2;
     string name = 3;
     string desc = 4;
-    ViewType view_type = 5;
+    ViewDataType data_type = 5;
     int64 version = 6;
     RepeatedView belongings = 7;
     int64 modified_time = 8;
     int64 create_time = 9;
+    string ext_data = 10;
+    string thumbnail = 11;
 }
 message RepeatedView {
     repeated View items = 1;
@@ -19,17 +21,18 @@ message CreateViewPayload {
     string name = 2;
     string desc = 3;
     oneof one_of_thumbnail { string thumbnail = 4; };
-    ViewType view_type = 5;
-    string ext = 6;
+    ViewDataType data_type = 5;
+    string ext_data = 6;
 }
 message CreateViewParams {
     string belong_to_id = 1;
     string name = 2;
     string desc = 3;
     string thumbnail = 4;
-    ViewType view_type = 5;
-    string ext = 6;
+    ViewDataType data_type = 5;
+    string ext_data = 6;
     string view_id = 7;
+    string data = 8;
 }
 message ViewId {
     string value = 1;
@@ -49,7 +52,7 @@ message UpdateViewParams {
     oneof one_of_desc { string desc = 3; };
     oneof one_of_thumbnail { string thumbnail = 4; };
 }
-enum ViewType {
+enum ViewDataType {
     RichText = 0;
     PlainText = 1;
 }

+ 5 - 3
shared-lib/flowy-folder-data-model/src/user_default.rs

@@ -1,6 +1,6 @@
 use crate::entities::{
     app::{App, RepeatedApp},
-    view::{RepeatedView, View, ViewType},
+    view::{RepeatedView, View, ViewDataType},
     workspace::Workspace,
 };
 use chrono::Utc;
@@ -49,17 +49,19 @@ fn create_default_view(app_id: String, time: chrono::DateTime<Utc>) -> View {
     let view_id = uuid::Uuid::new_v4();
     let name = "Read me".to_string();
     let desc = "".to_string();
-    let view_type = ViewType::RichText;
+    let data_type = ViewDataType::RichText;
 
     View {
         id: view_id.to_string(),
         belong_to_id: app_id,
         name,
         desc,
-        view_type,
+        data_type,
         version: 0,
         belongings: Default::default(),
         modified_time: time.timestamp(),
         create_time: time.timestamp(),
+        ext_data: "".to_string(),
+        thumbnail: "".to_string(),
     }
 }

+ 2 - 2
shared-lib/lib-ot/src/core/delta/builder.rs

@@ -1,6 +1,6 @@
-use crate::core::{trim, Attributes, Delta, PlainAttributes};
+use crate::core::{trim, Attributes, Delta, PlainTextAttributes};
 
-pub type PlainDeltaBuilder = DeltaBuilder<PlainAttributes>;
+pub type PlainTextDeltaBuilder = DeltaBuilder<PlainTextAttributes>;
 
 pub struct DeltaBuilder<T: Attributes> {
     delta: Delta<T>,

+ 1 - 1
shared-lib/lib-ot/src/core/delta/delta.rs

@@ -13,7 +13,7 @@ use std::{
     str::FromStr,
 };
 
-pub type PlainDelta = Delta<PlainAttributes>;
+pub type PlainTextDelta = Delta<PlainTextAttributes>;
 
 // TODO: optimize the memory usage with Arc::make_mut or Cow
 #[derive(Clone, Debug, PartialEq, Eq)]

+ 2 - 2
shared-lib/lib-ot/src/core/operation/builder.rs

@@ -1,10 +1,10 @@
 use crate::{
-    core::{Attributes, Operation, PlainAttributes},
+    core::{Attributes, Operation, PlainTextAttributes},
     rich_text::RichTextAttributes,
 };
 
 pub type RichTextOpBuilder = OpBuilder<RichTextAttributes>;
-pub type PlainTextOpBuilder = OpBuilder<PlainAttributes>;
+pub type PlainTextOpBuilder = OpBuilder<PlainTextAttributes>;
 
 pub struct OpBuilder<T: Attributes> {
     ty: Operation<T>,

+ 4 - 4
shared-lib/lib-ot/src/core/operation/operation.rs

@@ -339,14 +339,14 @@ where
 }
 
 #[derive(Debug, Clone, Eq, PartialEq, Default, Serialize, Deserialize)]
-pub struct PlainAttributes();
-impl fmt::Display for PlainAttributes {
+pub struct PlainTextAttributes();
+impl fmt::Display for PlainTextAttributes {
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
         f.write_str("PlainAttributes")
     }
 }
 
-impl Attributes for PlainAttributes {
+impl Attributes for PlainTextAttributes {
     fn is_empty(&self) -> bool {
         true
     }
@@ -356,7 +356,7 @@ impl Attributes for PlainAttributes {
     fn extend_other(&mut self, _other: Self) {}
 }
 
-impl OperationTransformable for PlainAttributes {
+impl OperationTransformable for PlainTextAttributes {
     fn compose(&self, _other: &Self) -> Result<Self, OTError> {
         Ok(self.clone())
     }

+ 2 - 1
shared-lib/lib-ot/src/rich_text/attributes.rs

@@ -40,6 +40,7 @@ impl fmt::Display for RichTextAttributes {
     }
 }
 
+#[inline(always)]
 pub fn plain_attributes() -> RichTextAttributes {
     RichTextAttributes::default()
 }
@@ -58,7 +59,7 @@ impl RichTextAttributes {
         self.inner.insert(key, value);
     }
 
-    pub fn add_kv(&mut self, key: RichTextAttributeKey, value: RichTextAttributeValue) {
+    pub fn insert(&mut self, key: RichTextAttributeKey, value: RichTextAttributeValue) {
         self.inner.insert(key, value);
     }
 

+ 1 - 1
shared-lib/lib-ot/src/rich_text/attributes_serde.rs

@@ -101,7 +101,7 @@ impl<'de> Deserialize<'de> for RichTextAttributes {
                 let mut attributes = RichTextAttributes::new();
                 while let Some(key) = map.next_key::<RichTextAttributeKey>()? {
                     let value = map.next_value::<RichTextAttributeValue>()?;
-                    attributes.add_kv(key, value);
+                    attributes.insert(key, value);
                 }
 
                 Ok(attributes)