|  | @@ -1,15 +1,22 @@
 | 
											
												
													
														|  | 
 |  | +import 'dart:convert';
 | 
											
												
													
														|  | 
 |  | +import 'dart:io';
 | 
											
												
													
														|  | 
 |  | +
 | 
											
												
													
														|  | 
 |  | +import 'package:appflowy/plugins/document/application/document_data_pb_extension.dart';
 | 
											
												
													
														|  | 
 |  | +import 'package:appflowy/plugins/document/application/prelude.dart';
 | 
											
												
													
														|  |  import 'package:appflowy/startup/startup.dart';
 |  |  import 'package:appflowy/startup/startup.dart';
 | 
											
												
													
														|  |  import 'package:appflowy/util/file_picker/file_picker_service.dart';
 |  |  import 'package:appflowy/util/file_picker/file_picker_service.dart';
 | 
											
												
													
														|  |  import 'package:appflowy/workspace/application/settings/settings_file_exporter_cubit.dart';
 |  |  import 'package:appflowy/workspace/application/settings/settings_file_exporter_cubit.dart';
 | 
											
												
													
														|  | 
 |  | +import 'package:appflowy_backend/log.dart';
 | 
											
												
													
														|  | 
 |  | +import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
 | 
											
												
													
														|  |  import 'package:dartz/dartz.dart' as dartz;
 |  |  import 'package:dartz/dartz.dart' as dartz;
 | 
											
												
													
														|  |  import 'package:easy_localization/easy_localization.dart';
 |  |  import 'package:easy_localization/easy_localization.dart';
 | 
											
												
													
														|  | -import 'package:flowy_infra_ui/flowy_infra_ui.dart';
 |  | 
 | 
											
												
													
														|  | 
 |  | +import 'package:flowy_infra_ui/flowy_infra_ui.dart' hide WidgetBuilder;
 | 
											
												
													
														|  |  import 'package:appflowy_backend/dispatch/dispatch.dart';
 |  |  import 'package:appflowy_backend/dispatch/dispatch.dart';
 | 
											
												
													
														|  |  import 'package:appflowy_backend/protobuf/flowy-error/errors.pbserver.dart';
 |  |  import 'package:appflowy_backend/protobuf/flowy-error/errors.pbserver.dart';
 | 
											
												
													
														|  |  import 'package:appflowy_backend/protobuf/flowy-folder2/workspace.pb.dart';
 |  |  import 'package:appflowy_backend/protobuf/flowy-folder2/workspace.pb.dart';
 | 
											
												
													
														|  |  import 'package:flutter/material.dart';
 |  |  import 'package:flutter/material.dart';
 | 
											
												
													
														|  |  import 'package:flutter_bloc/flutter_bloc.dart';
 |  |  import 'package:flutter_bloc/flutter_bloc.dart';
 | 
											
												
													
														|  | -
 |  | 
 | 
											
												
													
														|  | 
 |  | +import 'package:path/path.dart' as p;
 | 
											
												
													
														|  |  import '../../../../generated/locale_keys.g.dart';
 |  |  import '../../../../generated/locale_keys.g.dart';
 | 
											
												
													
														|  |  
 |  |  
 | 
											
												
													
														|  |  class FileExporterWidget extends StatefulWidget {
 |  |  class FileExporterWidget extends StatefulWidget {
 | 
											
										
											
												
													
														|  | @@ -22,24 +29,44 @@ class FileExporterWidget extends StatefulWidget {
 | 
											
												
													
														|  |  class _FileExporterWidgetState extends State<FileExporterWidget> {
 |  |  class _FileExporterWidgetState extends State<FileExporterWidget> {
 | 
											
												
													
														|  |    // Map<String, List<String>> _selectedPages = {};
 |  |    // Map<String, List<String>> _selectedPages = {};
 | 
											
												
													
														|  |  
 |  |  
 | 
											
												
													
														|  | 
 |  | +  SettingsFileExporterCubit? cubit;
 | 
											
												
													
														|  | 
 |  | +
 | 
											
												
													
														|  |    @override
 |  |    @override
 | 
											
												
													
														|  |    Widget build(BuildContext context) {
 |  |    Widget build(BuildContext context) {
 | 
											
												
													
														|  | -    return Column(
 |  | 
 | 
											
												
													
														|  | -      crossAxisAlignment: CrossAxisAlignment.start,
 |  | 
 | 
											
												
													
														|  | -      children: [
 |  | 
 | 
											
												
													
														|  | -        FlowyText.medium(
 |  | 
 | 
											
												
													
														|  | -          LocaleKeys.settings_files_selectFiles.tr(),
 |  | 
 | 
											
												
													
														|  | -          fontSize: 16.0,
 |  | 
 | 
											
												
													
														|  | -        ),
 |  | 
 | 
											
												
													
														|  | -        const VSpace(8),
 |  | 
 | 
											
												
													
														|  | -        Expanded(child: _buildFileSelector(context)),
 |  | 
 | 
											
												
													
														|  | -        const VSpace(8),
 |  | 
 | 
											
												
													
														|  | -        _buildButtons(context)
 |  | 
 | 
											
												
													
														|  | -      ],
 |  | 
 | 
											
												
													
														|  | 
 |  | +    return FutureBuilder<dartz.Either<WorkspaceSettingPB, FlowyError>>(
 | 
											
												
													
														|  | 
 |  | +      future: FolderEventReadCurrentWorkspace().send(),
 | 
											
												
													
														|  | 
 |  | +      builder: (context, snapshot) {
 | 
											
												
													
														|  | 
 |  | +        if (snapshot.hasData &&
 | 
											
												
													
														|  | 
 |  | +            snapshot.connectionState == ConnectionState.done) {
 | 
											
												
													
														|  | 
 |  | +          final workspaces = snapshot.data?.getLeftOrNull<WorkspaceSettingPB>();
 | 
											
												
													
														|  | 
 |  | +          if (workspaces != null) {
 | 
											
												
													
														|  | 
 |  | +            final views = workspaces.workspace.views;
 | 
											
												
													
														|  | 
 |  | +            cubit ??= SettingsFileExporterCubit(views: views);
 | 
											
												
													
														|  | 
 |  | +            return BlocProvider<SettingsFileExporterCubit>.value(
 | 
											
												
													
														|  | 
 |  | +              value: cubit!,
 | 
											
												
													
														|  | 
 |  | +              child: Column(
 | 
											
												
													
														|  | 
 |  | +                crossAxisAlignment: CrossAxisAlignment.start,
 | 
											
												
													
														|  | 
 |  | +                mainAxisSize: MainAxisSize.min,
 | 
											
												
													
														|  | 
 |  | +                children: [
 | 
											
												
													
														|  | 
 |  | +                  FlowyText.medium(
 | 
											
												
													
														|  | 
 |  | +                    LocaleKeys.settings_files_selectFiles.tr(),
 | 
											
												
													
														|  | 
 |  | +                    fontSize: 16.0,
 | 
											
												
													
														|  | 
 |  | +                  ),
 | 
											
												
													
														|  | 
 |  | +                  const VSpace(8),
 | 
											
												
													
														|  | 
 |  | +                  const Expanded(child: _ExpandedList()),
 | 
											
												
													
														|  | 
 |  | +                  const VSpace(8),
 | 
											
												
													
														|  | 
 |  | +                  _buildButtons()
 | 
											
												
													
														|  | 
 |  | +                ],
 | 
											
												
													
														|  | 
 |  | +              ),
 | 
											
												
													
														|  | 
 |  | +            );
 | 
											
												
													
														|  | 
 |  | +          }
 | 
											
												
													
														|  | 
 |  | +        }
 | 
											
												
													
														|  | 
 |  | +        return const CircularProgressIndicator();
 | 
											
												
													
														|  | 
 |  | +      },
 | 
											
												
													
														|  |      );
 |  |      );
 | 
											
												
													
														|  |    }
 |  |    }
 | 
											
												
													
														|  |  
 |  |  
 | 
											
												
													
														|  | -  Row _buildButtons(BuildContext context) {
 |  | 
 | 
											
												
													
														|  | 
 |  | +  Widget _buildButtons() {
 | 
											
												
													
														|  |      return Row(
 |  |      return Row(
 | 
											
												
													
														|  |        children: [
 |  |        children: [
 | 
											
												
													
														|  |          const Spacer(),
 |  |          const Spacer(),
 | 
											
										
											
												
													
														|  | @@ -55,8 +82,28 @@ class _FileExporterWidgetState extends State<FileExporterWidget> {
 | 
											
												
													
														|  |            onPressed: () async {
 |  |            onPressed: () async {
 | 
											
												
													
														|  |              await getIt<FilePickerService>()
 |  |              await getIt<FilePickerService>()
 | 
											
												
													
														|  |                  .getDirectoryPath()
 |  |                  .getDirectoryPath()
 | 
											
												
													
														|  | -                .then((exportPath) {
 |  | 
 | 
											
												
													
														|  | -              Navigator.of(context).pop();
 |  | 
 | 
											
												
													
														|  | 
 |  | +                .then((exportPath) async {
 | 
											
												
													
														|  | 
 |  | +              if (exportPath != null && cubit != null) {
 | 
											
												
													
														|  | 
 |  | +                final views = cubit!.state.selectedViews;
 | 
											
												
													
														|  | 
 |  | +                final result =
 | 
											
												
													
														|  | 
 |  | +                    await _AppFlowyFileExporter.exportToPath(exportPath, views);
 | 
											
												
													
														|  | 
 |  | +                if (result.$1) {
 | 
											
												
													
														|  | 
 |  | +                  // success
 | 
											
												
													
														|  | 
 |  | +                  _showToast(LocaleKeys.settings_files_exportFileSuccess.tr());
 | 
											
												
													
														|  | 
 |  | +                } else {
 | 
											
												
													
														|  | 
 |  | +                  _showToast(
 | 
											
												
													
														|  | 
 |  | +                    LocaleKeys.settings_files_exportFileFail.tr() +
 | 
											
												
													
														|  | 
 |  | +                        result.$2.join('\n'),
 | 
											
												
													
														|  | 
 |  | +                  );
 | 
											
												
													
														|  | 
 |  | +                }
 | 
											
												
													
														|  | 
 |  | +              } else {
 | 
											
												
													
														|  | 
 |  | +                _showToast(LocaleKeys.settings_files_exportFileFail.tr());
 | 
											
												
													
														|  | 
 |  | +              }
 | 
											
												
													
														|  | 
 |  | +              if (mounted) {
 | 
											
												
													
														|  | 
 |  | +                Navigator.of(context).popUntil(
 | 
											
												
													
														|  | 
 |  | +                  (router) => router.settings.name == '/',
 | 
											
												
													
														|  | 
 |  | +                );
 | 
											
												
													
														|  | 
 |  | +              }
 | 
											
												
													
														|  |              });
 |  |              });
 | 
											
												
													
														|  |            },
 |  |            },
 | 
											
												
													
														|  |          ),
 |  |          ),
 | 
											
										
											
												
													
														|  | @@ -64,24 +111,14 @@ class _FileExporterWidgetState extends State<FileExporterWidget> {
 | 
											
												
													
														|  |      );
 |  |      );
 | 
											
												
													
														|  |    }
 |  |    }
 | 
											
												
													
														|  |  
 |  |  
 | 
											
												
													
														|  | -  FutureBuilder<dartz.Either<WorkspaceSettingPB, FlowyError>>
 |  | 
 | 
											
												
													
														|  | -      _buildFileSelector(BuildContext context) {
 |  | 
 | 
											
												
													
														|  | -    return FutureBuilder<dartz.Either<WorkspaceSettingPB, FlowyError>>(
 |  | 
 | 
											
												
													
														|  | -      future: FolderEventReadCurrentWorkspace().send(),
 |  | 
 | 
											
												
													
														|  | -      builder: (context, snapshot) {
 |  | 
 | 
											
												
													
														|  | -        if (snapshot.hasData &&
 |  | 
 | 
											
												
													
														|  | -            snapshot.connectionState == ConnectionState.done) {
 |  | 
 | 
											
												
													
														|  | -          final workspaces = snapshot.data?.getLeftOrNull<WorkspaceSettingPB>();
 |  | 
 | 
											
												
													
														|  | -          if (workspaces != null) {
 |  | 
 | 
											
												
													
														|  | -            final views = workspaces.workspace.views;
 |  | 
 | 
											
												
													
														|  | -            return BlocProvider<SettingsFileExporterCubit>(
 |  | 
 | 
											
												
													
														|  | -              create: (_) => SettingsFileExporterCubit(views: views),
 |  | 
 | 
											
												
													
														|  | -              child: const _ExpandedList(),
 |  | 
 | 
											
												
													
														|  | -            );
 |  | 
 | 
											
												
													
														|  | -          }
 |  | 
 | 
											
												
													
														|  | -        }
 |  | 
 | 
											
												
													
														|  | -        return const CircularProgressIndicator();
 |  | 
 | 
											
												
													
														|  | -      },
 |  | 
 | 
											
												
													
														|  | 
 |  | +  void _showToast(String message) {
 | 
											
												
													
														|  | 
 |  | +    ScaffoldMessenger.of(context).showSnackBar(
 | 
											
												
													
														|  | 
 |  | +      SnackBar(
 | 
											
												
													
														|  | 
 |  | +        content: FlowyText(
 | 
											
												
													
														|  | 
 |  | +          message,
 | 
											
												
													
														|  | 
 |  | +          color: Theme.of(context).colorScheme.onSurface,
 | 
											
												
													
														|  | 
 |  | +        ),
 | 
											
												
													
														|  | 
 |  | +      ),
 | 
											
												
													
														|  |      );
 |  |      );
 | 
											
												
													
														|  |    }
 |  |    }
 | 
											
												
													
														|  |  }
 |  |  }
 | 
											
										
											
												
													
														|  | @@ -131,9 +168,9 @@ class _ExpandedListState extends State<_ExpandedList> {
 | 
											
												
													
														|  |      final apps = state.views;
 |  |      final apps = state.views;
 | 
											
												
													
														|  |      final expanded = state.expanded;
 |  |      final expanded = state.expanded;
 | 
											
												
													
														|  |      final selectedItems = state.selectedItems;
 |  |      final selectedItems = state.selectedItems;
 | 
											
												
													
														|  | -    final isExpaned = expanded[index] == true;
 |  | 
 | 
											
												
													
														|  | 
 |  | +    final isExpanded = expanded[index] == true;
 | 
											
												
													
														|  |      List<Widget> expandedChildren = [];
 |  |      List<Widget> expandedChildren = [];
 | 
											
												
													
														|  | -    if (isExpaned) {
 |  | 
 | 
											
												
													
														|  | 
 |  | +    if (isExpanded) {
 | 
											
												
													
														|  |        for (var i = 0; i < selectedItems[index].length; i++) {
 |  |        for (var i = 0; i < selectedItems[index].length; i++) {
 | 
											
												
													
														|  |          final name = apps[index].childViews[i].name;
 |  |          final name = apps[index].childViews[i].name;
 | 
											
												
													
														|  |          final checkbox = CheckboxListTile(
 |  |          final checkbox = CheckboxListTile(
 | 
											
										
											
												
													
														|  | @@ -160,7 +197,7 @@ class _ExpandedListState extends State<_ExpandedList> {
 | 
											
												
													
														|  |            child: ListTile(
 |  |            child: ListTile(
 | 
											
												
													
														|  |              title: FlowyText.medium(apps[index].name),
 |  |              title: FlowyText.medium(apps[index].name),
 | 
											
												
													
														|  |              trailing: Icon(
 |  |              trailing: Icon(
 | 
											
												
													
														|  | -              isExpaned
 |  | 
 | 
											
												
													
														|  | 
 |  | +              isExpanded
 | 
											
												
													
														|  |                    ? Icons.arrow_drop_down_rounded
 |  |                    ? Icons.arrow_drop_down_rounded
 | 
											
												
													
														|  |                    : Icons.arrow_drop_up_rounded,
 |  |                    : Icons.arrow_drop_up_rounded,
 | 
											
												
													
														|  |              ),
 |  |              ),
 | 
											
										
											
												
													
														|  | @@ -182,3 +219,45 @@ extension AppFlowy on dartz.Either {
 | 
											
												
													
														|  |      return null;
 |  |      return null;
 | 
											
												
													
														|  |    }
 |  |    }
 | 
											
												
													
														|  |  }
 |  |  }
 | 
											
												
													
														|  | 
 |  | +
 | 
											
												
													
														|  | 
 |  | +class _AppFlowyFileExporter {
 | 
											
												
													
														|  | 
 |  | +  static Future<(bool result, List<String> failedNames)> exportToPath(
 | 
											
												
													
														|  | 
 |  | +    String path,
 | 
											
												
													
														|  | 
 |  | +    List<ViewPB> views,
 | 
											
												
													
														|  | 
 |  | +  ) async {
 | 
											
												
													
														|  | 
 |  | +    final failedFileNames = <String>[];
 | 
											
												
													
														|  | 
 |  | +    final Map<String, int> names = {};
 | 
											
												
													
														|  | 
 |  | +    final documentService = DocumentService();
 | 
											
												
													
														|  | 
 |  | +    for (final view in views) {
 | 
											
												
													
														|  | 
 |  | +      String? content;
 | 
											
												
													
														|  | 
 |  | +      String? fileExtension;
 | 
											
												
													
														|  | 
 |  | +      switch (view.layout) {
 | 
											
												
													
														|  | 
 |  | +        case ViewLayoutPB.Document:
 | 
											
												
													
														|  | 
 |  | +          final document = await documentService.openDocument(view: view);
 | 
											
												
													
														|  | 
 |  | +          document.fold((l) => Log.error(l), (r) {
 | 
											
												
													
														|  | 
 |  | +            final json = r.toDocument()?.toJson();
 | 
											
												
													
														|  | 
 |  | +            if (json != null) {
 | 
											
												
													
														|  | 
 |  | +              content = jsonEncode(json);
 | 
											
												
													
														|  | 
 |  | +            }
 | 
											
												
													
														|  | 
 |  | +          });
 | 
											
												
													
														|  | 
 |  | +          fileExtension = 'afdocument';
 | 
											
												
													
														|  | 
 |  | +          break;
 | 
											
												
													
														|  | 
 |  | +        default:
 | 
											
												
													
														|  | 
 |  | +          // TODO(nathan): export the new databse data to json
 | 
											
												
													
														|  | 
 |  | +          content = null;
 | 
											
												
													
														|  | 
 |  | +          break;
 | 
											
												
													
														|  | 
 |  | +      }
 | 
											
												
													
														|  | 
 |  | +      if (content != null) {
 | 
											
												
													
														|  | 
 |  | +        final count = names.putIfAbsent(view.name, () => 0);
 | 
											
												
													
														|  | 
 |  | +        final name = count == 0 ? view.name : '${view.name}($count)';
 | 
											
												
													
														|  | 
 |  | +        final file = File(p.join(path, '$name.$fileExtension'));
 | 
											
												
													
														|  | 
 |  | +        await file.writeAsString(content!);
 | 
											
												
													
														|  | 
 |  | +        names[view.name] = count + 1;
 | 
											
												
													
														|  | 
 |  | +      } else {
 | 
											
												
													
														|  | 
 |  | +        failedFileNames.add(view.name);
 | 
											
												
													
														|  | 
 |  | +      }
 | 
											
												
													
														|  | 
 |  | +    }
 | 
											
												
													
														|  | 
 |  | +
 | 
											
												
													
														|  | 
 |  | +    return (failedFileNames.isEmpty, failedFileNames);
 | 
											
												
													
														|  | 
 |  | +  }
 | 
											
												
													
														|  | 
 |  | +}
 |