diff --git a/frontend/app_flowy/assets/translations/en.json b/frontend/app_flowy/assets/translations/en.json index cccb344748..a9e18ec02a 100644 --- a/frontend/app_flowy/assets/translations/en.json +++ b/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", diff --git a/frontend/app_flowy/lib/util/file_picker/file_picker_impl.dart b/frontend/app_flowy/lib/util/file_picker/file_picker_impl.dart index 0c7f7e8b02..0b027224fd 100644 --- a/frontend/app_flowy/lib/util/file_picker/file_picker_impl.dart +++ b/frontend/app_flowy/lib/util/file_picker/file_picker_impl.dart @@ -6,4 +6,31 @@ class FilePicker implements FilePickerService { Future getDirectoryPath({String? title}) { return fp.FilePicker.platform.getDirectoryPath(); } + + @override + Future pickFiles( + {String? dialogTitle, + String? initialDirectory, + fp.FileType type = fp.FileType.any, + List? 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 ?? []); + } } diff --git a/frontend/app_flowy/lib/util/file_picker/file_picker_service.dart b/frontend/app_flowy/lib/util/file_picker/file_picker_service.dart index 5d25a9e830..8177d69278 100644 --- a/frontend/app_flowy/lib/util/file_picker/file_picker_service.dart +++ b/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 pickFiles({ + String? dialogTitle, + String? initialDirectory, + FileType type = FileType.any, + List? 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.'); } diff --git a/frontend/app_flowy/lib/workspace/application/app/app_bloc.dart b/frontend/app_flowy/lib/workspace/application/app/app_bloc.dart index 161da5395e..f2851bf1e0 100644 --- a/frontend/app_flowy/lib/workspace/application/app/app_bloc.dart +++ b/frontend/app_flowy/lib/workspace/application/app/app_bloc.dart @@ -102,6 +102,7 @@ class AppBloc extends Bloc { 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; diff --git a/frontend/app_flowy/lib/workspace/application/app/app_service.dart b/frontend/app_flowy/lib/workspace/application/app/app_service.dart index 1ebe8a75e4..1a5826b4a4 100644 --- a/frontend/app_flowy/lib/workspace/application/app/app_service.dart +++ b/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(); } diff --git a/frontend/app_flowy/lib/workspace/presentation/home/menu/app/header/add_button.dart b/frontend/app_flowy/lib/workspace/presentation/home/menu/app/header/add_button.dart index 9c26f80bb0..9f52cc5337 100644 --- a/frontend/app_flowy/lib/workspace/presentation/home/menu/app/header/add_button.dart +++ b/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 actions = []; + + // Plugins actions.addAll( pluginBuilders() .map((pluginBuilder) => @@ -24,6 +35,16 @@ class AddButton extends StatelessWidget { .toList(), ); + // Import + actions.addAll( + getIt() + .builders + .whereType() + .map((pluginBuilder) => + ImportActionWrapper(pluginBuilder: pluginBuilder)) + .toList(), + ); + return PopoverActionList( 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(); +} diff --git a/frontend/app_flowy/lib/workspace/presentation/home/menu/app/header/header.dart b/frontend/app_flowy/lib/workspace/presentation/home/menu/app/header/header.dart index 5a6214d47b..4b670b437e 100644 --- a/frontend/app_flowy/lib/workspace/presentation/home/menu/app/header/header.dart +++ b/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().add( AppEvent.createView( LocaleKeys.menuAppHeader_defaultNewPageName.tr(), pluginBuilder, + initialData: + document != null ? jsonEncode(document.toJson()) : '', ), ); }, diff --git a/frontend/app_flowy/lib/workspace/presentation/home/menu/app/header/import/import_panel.dart b/frontend/app_flowy/lib/workspace/presentation/home/menu/app/header/import/import_panel.dart new file mode 100644 index 0000000000..c1d1498d8f --- /dev/null +++ b/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 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 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 _importFile(_ImportType importType) async { + final result = await getIt().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); + } + } +} diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/editor_state.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/editor_state.dart index efa82472ab..44e4e3d5f5 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/editor_state.dart +++ b/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) { diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_all.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_all.dart index 0f655c1d31..16fd7b9b6b 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_all.dart +++ b/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 LibraryLoader(); Map _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 initializeMessages(String localeName) { +Future 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) { diff --git a/frontend/app_flowy/packages/flowy_infra_ui/lib/style_widget/button.dart b/frontend/app_flowy/packages/flowy_infra_ui/lib/style_widget/button.dart index 579138a2d1..867d31da57 100644 --- a/frontend/app_flowy/packages/flowy_infra_ui/lib/style_widget/button.dart +++ b/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)); }