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
This commit is contained in:
Lucas.Xu 2023-02-13 08:51:24 +07:00 committed by GitHub
parent 592b918175
commit 2f803959e7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 282 additions and 35 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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