Browse Source

feat: #1832 Support to import data from Markdown or Text to Document … (#1840)

* feat: #1832 Support to import data from Markdown or Text to Document page

* feat: #1832 Support to import data from Markdown or Text to Document page
Lucas.Xu 2 years ago
parent
commit
2f803959e7

+ 2 - 1
frontend/app_flowy/assets/translations/en.json

@@ -44,7 +44,8 @@
     "small": "small",
     "medium": "medium",
     "large": "large",
-    "fontSize": "Font Size"
+    "fontSize": "Font Size",
+    "import": "Import"
   },
   "disclosureAction": {
     "rename": "Rename",

+ 27 - 0
frontend/app_flowy/lib/util/file_picker/file_picker_impl.dart

@@ -6,4 +6,31 @@ class FilePicker implements FilePickerService {
   Future<String?> getDirectoryPath({String? title}) {
     return fp.FilePicker.platform.getDirectoryPath();
   }
+
+  @override
+  Future<FilePickerResult?> pickFiles(
+      {String? dialogTitle,
+      String? initialDirectory,
+      fp.FileType type = fp.FileType.any,
+      List<String>? allowedExtensions,
+      Function(fp.FilePickerStatus p1)? onFileLoading,
+      bool allowCompression = true,
+      bool allowMultiple = false,
+      bool withData = false,
+      bool withReadStream = false,
+      bool lockParentWindow = false}) async {
+    final result = await fp.FilePicker.platform.pickFiles(
+      dialogTitle: dialogTitle,
+      initialDirectory: initialDirectory,
+      type: type,
+      allowedExtensions: allowedExtensions,
+      onFileLoading: onFileLoading,
+      allowCompression: allowCompression,
+      allowMultiple: allowMultiple,
+      withData: withData,
+      withReadStream: withReadStream,
+      lockParentWindow: lockParentWindow,
+    );
+    return FilePickerResult(result?.files ?? []);
+  }
 }

+ 14 - 0
frontend/app_flowy/lib/util/file_picker/file_picker_service.dart

@@ -13,4 +13,18 @@ abstract class FilePickerService {
     String? title,
   }) async =>
       throw UnimplementedError('getDirectoryPath() has not been implemented.');
+
+  Future<FilePickerResult?> pickFiles({
+    String? dialogTitle,
+    String? initialDirectory,
+    FileType type = FileType.any,
+    List<String>? allowedExtensions,
+    Function(FilePickerStatus)? onFileLoading,
+    bool allowCompression = true,
+    bool allowMultiple = false,
+    bool withData = false,
+    bool withReadStream = false,
+    bool lockParentWindow = false,
+  }) async =>
+      throw UnimplementedError('pickFiles() has not been implemented.');
 }

+ 5 - 0
frontend/app_flowy/lib/workspace/application/app/app_bloc.dart

@@ -102,6 +102,7 @@ class AppBloc extends Bloc<AppEvent, AppState> {
       dataFormatType: value.pluginBuilder.dataFormatType,
       pluginType: value.pluginBuilder.pluginType,
       layoutType: value.pluginBuilder.layoutType!,
+      initialData: value.initialData,
     );
     result.fold(
       (view) => emit(state.copyWith(
@@ -140,6 +141,10 @@ class AppEvent with _$AppEvent {
     String name,
     PluginBuilder pluginBuilder, {
     String? desc,
+
+    /// The initial data should be the JSON of the doucment
+    /// For example: {"document":{"type":"editor","children":[]}}
+    String? initialData,
   }) = CreateView;
   const factory AppEvent.loadViews() = LoadApp;
   const factory AppEvent.delete() = DeleteApp;

+ 3 - 1
frontend/app_flowy/lib/workspace/application/app/app_service.dart

@@ -35,7 +35,9 @@ class AppService {
       ..desc = desc ?? ""
       ..dataFormat = dataFormatType
       ..layout = layoutType
-      ..initialData = utf8.encode(initialData ?? "");
+      ..initialData = utf8.encode(
+        initialData ?? "",
+      );
 
     return FolderEventCreateView(payload).send();
   }

+ 48 - 3
frontend/app_flowy/lib/workspace/presentation/home/menu/app/header/add_button.dart

@@ -1,13 +1,22 @@
+import 'package:app_flowy/plugins/document/document.dart';
 import 'package:app_flowy/startup/plugin/plugin.dart';
+import 'package:app_flowy/startup/startup.dart';
+import 'package:app_flowy/workspace/presentation/home/menu/app/header/import/import_panel.dart';
 import 'package:app_flowy/workspace/presentation/widgets/pop_up_action.dart';
+import 'package:appflowy_editor/appflowy_editor.dart' show Document;
 import 'package:appflowy_popover/appflowy_popover.dart';
 import 'package:flowy_infra/image.dart';
 import 'package:flowy_infra_ui/style_widget/icon_button.dart';
 import 'package:flutter/material.dart';
 import 'package:styled_widget/styled_widget.dart';
+import 'package:app_flowy/generated/locale_keys.g.dart';
+import 'package:easy_localization/easy_localization.dart';
 
 class AddButton extends StatelessWidget {
-  final Function(PluginBuilder) onSelected;
+  final Function(
+    PluginBuilder,
+    Document? document,
+  ) onSelected;
 
   const AddButton({
     Key? key,
@@ -17,6 +26,8 @@ class AddButton extends StatelessWidget {
   @override
   Widget build(BuildContext context) {
     final List<PopoverAction> actions = [];
+
+    // Plugins
     actions.addAll(
       pluginBuilders()
           .map((pluginBuilder) =>
@@ -24,6 +35,16 @@ class AddButton extends StatelessWidget {
           .toList(),
     );
 
+    // Import
+    actions.addAll(
+      getIt<PluginSandbox>()
+          .builders
+          .whereType<DocumentPluginBuilder>()
+          .map((pluginBuilder) =>
+              ImportActionWrapper(pluginBuilder: pluginBuilder))
+          .toList(),
+    );
+
     return PopoverActionList<PopoverAction>(
       direction: PopoverDirection.bottomWithLeftAligned,
       actions: actions,
@@ -39,9 +60,16 @@ class AddButton extends StatelessWidget {
       },
       onSelected: (action, controller) {
         if (action is AddButtonActionWrapper) {
-          onSelected(action.pluginBuilder);
+          onSelected(action.pluginBuilder, null);
+        }
+        if (action is ImportActionWrapper) {
+          showImportPanel(context, (document) {
+            if (document == null) {
+              return;
+            }
+            onSelected(action.pluginBuilder, document);
+          });
         }
-
         controller.close();
       },
     );
@@ -60,3 +88,20 @@ class AddButtonActionWrapper extends ActionCell {
   @override
   String get name => pluginBuilder.menuName;
 }
+
+class ImportActionWrapper extends ActionCell {
+  final DocumentPluginBuilder pluginBuilder;
+
+  ImportActionWrapper({
+    required this.pluginBuilder,
+  });
+
+  @override
+  Widget? leftIcon(Color iconColor) => svgWidget(
+        'editor/import',
+        color: iconColor,
+      );
+
+  @override
+  String get name => LocaleKeys.moreAction_import.tr();
+}

+ 5 - 1
frontend/app_flowy/lib/workspace/presentation/home/menu/app/header/header.dart

@@ -1,3 +1,5 @@
+import 'dart:convert';
+
 import 'package:app_flowy/workspace/presentation/widgets/dialogs.dart';
 import 'package:app_flowy/workspace/presentation/widgets/pop_up_action.dart';
 import 'package:appflowy_popover/appflowy_popover.dart';
@@ -104,11 +106,13 @@ class MenuAppHeader extends StatelessWidget {
       message: LocaleKeys.menuAppHeader_addPageTooltip.tr(),
       textStyle: AFThemeExtension.of(context).caption.textColor(Colors.white),
       child: AddButton(
-        onSelected: (pluginBuilder) {
+        onSelected: (pluginBuilder, document) {
           context.read<AppBloc>().add(
                 AppEvent.createView(
                   LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
                   pluginBuilder,
+                  initialData:
+                      document != null ? jsonEncode(document.toJson()) : '',
                 ),
               );
         },

+ 144 - 0
frontend/app_flowy/lib/workspace/presentation/home/menu/app/header/import/import_panel.dart

@@ -0,0 +1,144 @@
+import 'dart:io';
+
+import 'package:app_flowy/startup/startup.dart';
+import 'package:app_flowy/util/file_picker/file_picker_service.dart';
+import 'package:appflowy_editor/appflowy_editor.dart';
+import 'package:file_picker/file_picker.dart';
+import 'package:flowy_infra/image.dart';
+import 'package:flowy_infra_ui/flowy_infra_ui.dart';
+import 'package:flowy_infra_ui/style_widget/button.dart';
+import 'package:flowy_infra_ui/style_widget/container.dart';
+import 'package:flutter/material.dart';
+import 'package:app_flowy/generated/locale_keys.g.dart';
+import 'package:easy_localization/easy_localization.dart';
+
+typedef ImportCallback = void Function(Document? document);
+
+Future<void> showImportPanel(
+  BuildContext context,
+  ImportCallback callback,
+) async {
+  await showDialog(
+    context: context,
+    builder: (context) {
+      return AlertDialog(
+        title: FlowyText.semibold(
+          LocaleKeys.moreAction_import.tr(),
+          fontSize: 20,
+        ),
+        content: _ImportPanel(importCallback: callback),
+        contentPadding: const EdgeInsets.symmetric(
+          vertical: 10.0,
+          horizontal: 20.0,
+        ),
+      );
+    },
+  );
+}
+
+enum _ImportType {
+  markdownOrText;
+
+  @override
+  String toString() {
+    switch (this) {
+      case _ImportType.markdownOrText:
+        return 'Text & Markdown';
+      default:
+        assert(false, 'Unsupported Type ${this}');
+        return '';
+    }
+  }
+
+  Widget? get icon {
+    switch (this) {
+      case _ImportType.markdownOrText:
+        return svgWidget('editor/documents');
+      default:
+        assert(false, 'Unsupported Type ${this}');
+        return null;
+    }
+  }
+
+  List<String> get allowedExtensions {
+    switch (this) {
+      case _ImportType.markdownOrText:
+        return ['md', 'txt'];
+      default:
+        assert(false, 'Unsupported Type ${this}');
+        return [];
+    }
+  }
+}
+
+class _ImportPanel extends StatefulWidget {
+  const _ImportPanel({
+    required this.importCallback,
+  });
+
+  final ImportCallback importCallback;
+
+  @override
+  State<_ImportPanel> createState() => _ImportPanelState();
+}
+
+class _ImportPanelState extends State<_ImportPanel> {
+  @override
+  Widget build(BuildContext context) {
+    final width = MediaQuery.of(context).size.width * 0.7;
+    final height = width * 0.5;
+    return FlowyContainer(
+      Theme.of(context).colorScheme.surface,
+      height: height,
+      width: width,
+      child: GridView.count(
+        childAspectRatio: 1 / .2,
+        crossAxisCount: 2,
+        children: _ImportType.values.map(
+          (e) {
+            return Card(
+              child: FlowyButton(
+                leftIcon: e.icon,
+                leftIconSize: const Size.square(20),
+                text: FlowyText.medium(
+                  e.toString(),
+                  fontSize: 15,
+                  overflow: TextOverflow.ellipsis,
+                ),
+                onTap: () async {
+                  await _importFile(e);
+                  if (mounted) {
+                    Navigator.of(context).pop();
+                  }
+                },
+              ),
+            );
+          },
+        ).toList(),
+      ),
+    );
+  }
+
+  Future<void> _importFile(_ImportType importType) async {
+    final result = await getIt<FilePickerService>().pickFiles(
+      allowMultiple: false,
+      type: FileType.custom,
+      allowedExtensions: importType.allowedExtensions,
+    );
+    if (result == null || result.files.isEmpty) {
+      return;
+    }
+    final path = result.files.single.path!;
+    final plainText = await File(path).readAsString();
+
+    switch (importType) {
+      case _ImportType.markdownOrText:
+        final document = markdownToDocument(plainText);
+        widget.importCallback(document);
+        break;
+      default:
+        assert(false, 'Unsupported Type $importType');
+        widget.importCallback(null);
+    }
+  }
+}

+ 1 - 1
frontend/app_flowy/packages/appflowy_editor/lib/src/editor_state.dart

@@ -156,8 +156,8 @@ class EditorState {
       _applyRules(ruleCount);
       if (withUpdateCursor) {
         await updateCursorSelection(transaction.afterSelection);
-        completer.complete();
       }
+      completer.complete();
     });
 
     if (options.recordUndo) {

+ 26 - 27
frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_all.dart

@@ -11,7 +11,6 @@
 
 import 'dart:async';
 
-import 'package:flutter/foundation.dart';
 import 'package:intl/intl.dart';
 import 'package:intl/message_lookup_by_library.dart';
 import 'package:intl/src/intl_helpers.dart';
@@ -41,28 +40,28 @@ import 'messages_zh-TW.dart' as messages_zh_tw;
 
 typedef Future<dynamic> LibraryLoader();
 Map<String, LibraryLoader> _deferredLibraries = {
-  'bn_BN': () => new SynchronousFuture(null),
-  'ca': () => new SynchronousFuture(null),
-  'cs_CZ': () => new SynchronousFuture(null),
-  'de_DE': () => new SynchronousFuture(null),
-  'en': () => new SynchronousFuture(null),
-  'es_VE': () => new SynchronousFuture(null),
-  'fr_CA': () => new SynchronousFuture(null),
-  'fr_FR': () => new SynchronousFuture(null),
-  'hi_IN': () => new SynchronousFuture(null),
-  'hu_HU': () => new SynchronousFuture(null),
-  'id_ID': () => new SynchronousFuture(null),
-  'it_IT': () => new SynchronousFuture(null),
-  'ja_JP': () => new SynchronousFuture(null),
-  'ml_IN': () => new SynchronousFuture(null),
-  'nl_NL': () => new SynchronousFuture(null),
-  'pl_PL': () => new SynchronousFuture(null),
-  'pt_BR': () => new SynchronousFuture(null),
-  'pt_PT': () => new SynchronousFuture(null),
-  'ru_RU': () => new SynchronousFuture(null),
-  'tr_TR': () => new SynchronousFuture(null),
-  'zh_CN': () => new SynchronousFuture(null),
-  'zh_TW': () => new SynchronousFuture(null),
+  'bn_BN': () => new Future.value(null),
+  'ca': () => new Future.value(null),
+  'cs_CZ': () => new Future.value(null),
+  'de_DE': () => new Future.value(null),
+  'en': () => new Future.value(null),
+  'es_VE': () => new Future.value(null),
+  'fr_CA': () => new Future.value(null),
+  'fr_FR': () => new Future.value(null),
+  'hi_IN': () => new Future.value(null),
+  'hu_HU': () => new Future.value(null),
+  'id_ID': () => new Future.value(null),
+  'it_IT': () => new Future.value(null),
+  'ja_JP': () => new Future.value(null),
+  'ml_IN': () => new Future.value(null),
+  'nl_NL': () => new Future.value(null),
+  'pl_PL': () => new Future.value(null),
+  'pt_BR': () => new Future.value(null),
+  'pt_PT': () => new Future.value(null),
+  'ru_RU': () => new Future.value(null),
+  'tr_TR': () => new Future.value(null),
+  'zh_CN': () => new Future.value(null),
+  'zh_TW': () => new Future.value(null),
 };
 
 MessageLookupByLibrary? _findExact(String localeName) {
@@ -117,18 +116,18 @@ MessageLookupByLibrary? _findExact(String localeName) {
 }
 
 /// User programs should call this before using [localeName] for messages.
-Future<bool> initializeMessages(String localeName) {
+Future<bool> initializeMessages(String localeName) async {
   var availableLocale = Intl.verifiedLocale(
       localeName, (locale) => _deferredLibraries[locale] != null,
       onFailure: (_) => null);
   if (availableLocale == null) {
-    return new SynchronousFuture(false);
+    return new Future.value(false);
   }
   var lib = _deferredLibraries[availableLocale];
-  lib == null ? new SynchronousFuture(false) : lib();
+  await (lib == null ? new Future.value(false) : lib());
   initializeInternalMessageLookup(() => new CompositeMessageLookup());
   messageLookup.addLocale(availableLocale, _findGeneratedMessagesFor);
-  return new SynchronousFuture(true);
+  return new Future.value(true);
 }
 
 bool _messagesExistFor(String locale) {

+ 7 - 1
frontend/app_flowy/packages/flowy_infra_ui/lib/style_widget/button.dart

@@ -21,6 +21,7 @@ class FlowyButton extends StatelessWidget {
   final bool useIntrinsicWidth;
   final bool disable;
   final double disableOpacity;
+  final Size? leftIconSize;
 
   const FlowyButton({
     Key? key,
@@ -37,6 +38,7 @@ class FlowyButton extends StatelessWidget {
     this.useIntrinsicWidth = false,
     this.disable = false,
     this.disableOpacity = 0.5,
+    this.leftIconSize = const Size.square(16),
   }) : super(key: key);
 
   @override
@@ -65,7 +67,11 @@ class FlowyButton extends StatelessWidget {
 
     if (leftIcon != null) {
       children.add(
-          SizedBox.fromSize(size: const Size.square(16), child: leftIcon!));
+        SizedBox.fromSize(
+          size: leftIconSize,
+          child: leftIcon!,
+        ),
+      );
       children.add(const HSpace(6));
     }