mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
* 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:
parent
592b918175
commit
2f803959e7
@ -44,7 +44,8 @@
|
||||
"small": "small",
|
||||
"medium": "medium",
|
||||
"large": "large",
|
||||
"fontSize": "Font Size"
|
||||
"fontSize": "Font Size",
|
||||
"import": "Import"
|
||||
},
|
||||
"disclosureAction": {
|
||||
"rename": "Rename",
|
||||
|
@ -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 ?? []);
|
||||
}
|
||||
}
|
||||
|
@ -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.');
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -35,7 +35,9 @@ class AppService {
|
||||
..desc = desc ?? ""
|
||||
..dataFormat = dataFormatType
|
||||
..layout = layoutType
|
||||
..initialData = utf8.encode(initialData ?? "");
|
||||
..initialData = utf8.encode(
|
||||
initialData ?? "",
|
||||
);
|
||||
|
||||
return FolderEventCreateView(payload).send();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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()) : '',
|
||||
),
|
||||
);
|
||||
},
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -156,8 +156,8 @@ class EditorState {
|
||||
_applyRules(ruleCount);
|
||||
if (withUpdateCursor) {
|
||||
await updateCursorSelection(transaction.afterSelection);
|
||||
completer.complete();
|
||||
}
|
||||
completer.complete();
|
||||
});
|
||||
|
||||
if (options.recordUndo) {
|
||||
|
@ -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) {
|
||||
|
@ -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));
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user