diff --git a/.github/workflows/deploy_test_web.yaml b/.github/workflows/deploy_test_web.yaml deleted file mode 100644 index f7d3339d77..0000000000 --- a/.github/workflows/deploy_test_web.yaml +++ /dev/null @@ -1,72 +0,0 @@ -name: Deploy Web (Test) - -on: - push: - branches: - - build/test -env: - NODE_VERSION: "18.16.0" - PNPM_VERSION: "8.5.0" - -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true - -jobs: - deploy: - runs-on: ubuntu-latest - env: - SSH_PRIVATE_KEY: ${{ secrets.WEB_TEST_SSH_PRIVATE_KEY }} - REMOTE_HOST: ${{ secrets.WEB_TEST_REMOTE_HOST }} - REMOTE_USER: ${{ secrets.WEB_TEST_REMOTE_USER }} - SSL_CERTIFICATE: ${{ secrets.WEB_TEST_SSL_CERTIFICATE }} - SSL_CERTIFICATE_KEY: ${{ secrets.WEB_TEST_SSL_CERTIFICATE_KEY }} - ENV_FILE: test.env - steps: - - uses: actions/checkout@v4 - - name: setup node - uses: actions/setup-node@v3 - with: - node-version: ${{ env.NODE_VERSION }} - - name: setup pnpm - uses: pnpm/action-setup@v2 - with: - version: ${{ env.PNPM_VERSION }} - - name: Node_modules cache - uses: actions/cache@v2 - with: - path: frontend/appflowy_web_app/node_modules - key: node-modules-${{ runner.os }} - - name: install frontend dependencies - working-directory: frontend/appflowy_web_app - run: | - pnpm install - - name: copy env file - working-directory: frontend/appflowy_web_app - run: | - cp ${{ env.ENV_FILE }} .env - - name: test and lint - working-directory: frontend/appflowy_web_app - run: | - pnpm run lint - - name: build - working-directory: frontend/appflowy_web_app - run: | - pnpm run build - - name: generate SSL certificate - run: | - echo "${{ env.SSL_CERTIFICATE }}" > nginx-signed.crt - echo "${{ env.SSL_CERTIFICATE_KEY }}" > nginx-signed.key - - name: Deploy to EC2 - uses: easingthemes/ssh-deploy@main - with: - SSH_PRIVATE_KEY: ${{ env.SSH_PRIVATE_KEY }} - ARGS: "-rlgoDzvc -i" - SOURCE: "frontend/appflowy_web_app/dist frontend/appflowy_web_app/server.cjs frontend/appflowy_web_app/start.sh frontend/appflowy_web_app/Dockerfile frontend/appflowy_web_app/nginx.conf frontend/appflowy_web_app/.env nginx-signed.crt nginx-signed.key" - REMOTE_HOST: ${{ env.REMOTE_HOST }} - REMOTE_USER: ${{ env.REMOTE_USER }} - EXCLUDE: "frontend/appflowy_web_app/dist/, frontend/appflowy_web_app/node_modules/" - SCRIPT_AFTER: | - docker build -t appflowy-web-app . - docker rm -f appflowy-web-app || true - docker run -d -p 80:80 -p 443:443 --name appflowy-web-app appflowy-web-app diff --git a/frontend/appflowy_flutter/integration_test/desktop/uncategorized/empty_test.dart b/frontend/appflowy_flutter/integration_test/desktop/uncategorized/empty_test.dart index 8ff3cf3c72..8b1756cde1 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/uncategorized/empty_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/uncategorized/empty_test.dart @@ -8,7 +8,7 @@ void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); group('Empty', () { - testWidgets('toggle theme mode', (tester) async { + testWidgets('empty test', (tester) async { await tester.initializeAppFlowy(); await tester.tapAnonymousSignInButton(); await tester.wait(1000); diff --git a/frontend/appflowy_flutter/integration_test/desktop/uncategorized/share_markdown_test.dart b/frontend/appflowy_flutter/integration_test/desktop/uncategorized/share_markdown_test.dart index f73e61ea82..26788beb9c 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/uncategorized/share_markdown_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/uncategorized/share_markdown_test.dart @@ -51,13 +51,14 @@ void main() { }, ); - final shareButton = find.byType(ShareActionList); + final shareButton = find.byType(DocumentShareButton); final shareButtonState = - tester.state(shareButton) as ShareActionListState; + tester.widget(shareButton) as DocumentShareButton; + final path = await mockSaveFilePath( p.join( context.applicationDataDirectory, - '${shareButtonState.name}.md', + '${shareButtonState.view.name}.md', ), ); diff --git a/frontend/appflowy_flutter/lib/plugins/document/application/document_share_bloc.dart b/frontend/appflowy_flutter/lib/plugins/document/application/document_share_bloc.dart index af0d5081f1..fbf0a88a0c 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/application/document_share_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/application/document_share_bloc.dart @@ -1,58 +1,190 @@ import 'dart:io'; +import 'package:appflowy/user/application/user_service.dart'; import 'package:appflowy/workspace/application/export/document_exporter.dart'; +import 'package:appflowy/workspace/application/view/view_listener.dart'; +import 'package:appflowy/workspace/application/view/view_service.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-document/entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; import 'package:appflowy_result/appflowy_result.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; part 'document_share_bloc.freezed.dart'; +const _url = 'https://appflowy.com'; + class DocumentShareBloc extends Bloc { DocumentShareBloc({ required this.view, - }) : super(const DocumentShareState.initial()) { + }) : super(DocumentShareState.initial()) { on((event, emit) async { await event.when( + initial: () async { + viewListener = ViewListener(viewId: view.id) + ..start( + onViewUpdated: (value) { + add(DocumentShareEvent.updateViewName(value.name)); + }, + onViewMoveToTrash: (p0) { + add(const DocumentShareEvent.setPublishStatus(false)); + }, + ); + + add(const DocumentShareEvent.updatePublishStatus()); + }, share: (type, path) async { if (DocumentShareType.unimplemented.contains(type)) { Log.error('DocumentShareType $type is not implemented'); return; } - emit(const DocumentShareState.loading()); + emit(state.copyWith(isLoading: true)); - final exporter = DocumentExporter(view); - final FlowyResult result = - await exporter.export(type.exportType).then((value) { - return value.fold( - (s) { - if (path != null) { - switch (type) { - case DocumentShareType.markdown: - return FlowyResult.success(_saveMarkdownToPath(s, path)); - case DocumentShareType.html: - return FlowyResult.success(_saveHTMLToPath(s, path)); - default: - break; - } - } - return FlowyResult.failure(FlowyError()); - }, - (f) => FlowyResult.failure(f), + final result = await _export(type, path); + + emit( + state.copyWith( + isLoading: false, + exportResult: result, + ), + ); + }, + publish: (nameSpace, publishName) async { + // set space name + try { + final result = + await ViewBackendService.getPublishNameSpace().getOrThrow(); + + await ViewBackendService.publish( + view, + name: publishName, + ).getOrThrow(); + + emit( + state.copyWith( + isPublished: true, + publishResult: FlowySuccess(null), + unpublishResult: null, + url: '$_url/${result.namespace}/$publishName', + ), ); + } catch (e) { + Log.error('publish error: $e'); + + emit( + state.copyWith( + isPublished: false, + publishResult: FlowyResult.failure( + FlowyError(msg: 'publish error: $e'), + ), + unpublishResult: null, + url: '', + ), + ); + } + }, + unPublish: () async { + emit( + state.copyWith( + publishResult: null, + unpublishResult: null, + ), + ); + + final result = await ViewBackendService.unpublish(view); + final isPublished = !result.isSuccess; + result.onFailure((f) { + Log.error('unpublish error: $f'); }); - emit(DocumentShareState.finish(result)); + emit( + state.copyWith( + isPublished: isPublished, + publishResult: null, + unpublishResult: result, + url: result.fold((_) => '', (_) => state.url), + ), + ); + }, + updateViewName: (viewName) async { + emit(state.copyWith(viewName: viewName)); + }, + setPublishStatus: (isPublished) { + emit( + state.copyWith( + isPublished: isPublished, + url: isPublished ? state.url : '', + ), + ); + }, + updatePublishStatus: () async { + final publishInfo = await ViewBackendService.getPublishInfo(view); + final enablePublish = + await UserBackendService.getCurrentUserProfile().fold( + (v) => v.authenticator == AuthenticatorPB.AppFlowyCloud, + (p) => false, + ); + publishInfo.fold((s) { + emit( + state.copyWith( + isPublished: true, + url: '$_url/${s.namespace}/${s.publishName}', + viewName: view.name, + enablePublish: enablePublish, + ), + ); + }, (f) { + emit( + state.copyWith( + isPublished: false, + url: '', + viewName: view.name, + enablePublish: enablePublish, + ), + ); + }); }, ); }); } final ViewPB view; + late final ViewListener viewListener; + + late final exporter = DocumentExporter(view); + + @override + Future close() async { + await viewListener.stop(); + return super.close(); + } + + Future> _export( + DocumentShareType type, + String? path, + ) async { + final result = await exporter.export(type.exportType); + return result.fold( + (s) { + if (path != null) { + switch (type) { + case DocumentShareType.markdown: + return FlowySuccess(_saveMarkdownToPath(s, path)); + case DocumentShareType.html: + return FlowySuccess(_saveHTMLToPath(s, path)); + default: + break; + } + } + return FlowyResult.failure(FlowyError()); + }, + (f) => FlowyResult.failure(f), + ); + } ExportDataPB _saveMarkdownToPath(String markdown, String path) { File(path).writeAsStringSync(markdown); @@ -93,15 +225,41 @@ enum DocumentShareType { @freezed class DocumentShareEvent with _$DocumentShareEvent { - const factory DocumentShareEvent.share(DocumentShareType type, String? path) = - Share; + const factory DocumentShareEvent.initial() = _Initial; + const factory DocumentShareEvent.share( + DocumentShareType type, + String? path, + ) = _Share; + const factory DocumentShareEvent.publish( + String nameSpace, + String pageId, + ) = _Publish; + const factory DocumentShareEvent.unPublish() = _UnPublish; + const factory DocumentShareEvent.updateViewName(String name) = + _UpdateViewName; + const factory DocumentShareEvent.updatePublishStatus() = _UpdatePublishStatus; + const factory DocumentShareEvent.setPublishStatus(bool isPublished) = + _SetPublishStatus; } @freezed class DocumentShareState with _$DocumentShareState { - const factory DocumentShareState.initial() = _Initial; - const factory DocumentShareState.loading() = _Loading; - const factory DocumentShareState.finish( - FlowyResult successOrFail, - ) = _Finish; + const factory DocumentShareState({ + required bool isPublished, + required bool isLoading, + required String url, + required String viewName, + required bool enablePublish, + FlowyResult? exportResult, + FlowyResult? publishResult, + FlowyResult? unpublishResult, + }) = _DocumentShareState; + + factory DocumentShareState.initial() => const DocumentShareState( + isLoading: false, + isPublished: false, + enablePublish: true, + url: '', + viewName: '', + ); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/share/export_tab.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/share/export_tab.dart new file mode 100644 index 0000000000..c5bbecbe0b --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/share/export_tab.dart @@ -0,0 +1,130 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/document/application/document_share_bloc.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart'; +import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/util/string_extension.dart'; +import 'package:appflowy/util/theme_extension.dart'; +import 'package:appflowy/workspace/application/export/document_exporter.dart'; +import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/file_picker/file_picker_service.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class ExportTab extends StatelessWidget { + const ExportTab({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + const VSpace(10), + _ExportButton( + title: LocaleKeys.shareAction_html.tr(), + svg: FlowySvgs.export_html_s, + onTap: () => _exportHTML(context), + ), + const VSpace(10), + _ExportButton( + title: LocaleKeys.shareAction_markdown.tr(), + svg: FlowySvgs.export_markdown_s, + onTap: () => _exportMarkdown(context), + ), + const VSpace(10), + _ExportButton( + title: LocaleKeys.shareAction_clipboard.tr(), + svg: FlowySvgs.duplicate_s, + onTap: () => _exportToClipboard(context), + ), + ], + ); + } + + Future _exportHTML(BuildContext context) async { + final viewName = context.read().state.viewName; + final exportPath = await getIt().saveFile( + dialogTitle: '', + fileName: '${viewName.toFileName()}.html', + ); + if (context.mounted && exportPath != null) { + context.read().add( + DocumentShareEvent.share( + DocumentShareType.html, + exportPath, + ), + ); + } + } + + Future _exportMarkdown(BuildContext context) async { + final viewName = context.read().state.viewName; + final exportPath = await getIt().saveFile( + dialogTitle: '', + fileName: '${viewName.toFileName()}.md', + ); + if (context.mounted && exportPath != null) { + context.read().add( + DocumentShareEvent.share( + DocumentShareType.markdown, + exportPath, + ), + ); + } + } + + Future _exportToClipboard(BuildContext context) async { + final documentExporter = + DocumentExporter(context.read().view); + final result = await documentExporter.export(DocumentExportType.markdown); + result.fold( + (markdown) { + getIt().setData( + ClipboardServiceData(plainText: markdown), + ); + showToastNotification( + context, + message: LocaleKeys.grid_url_copiedNotification.tr(), + ); + }, + (error) => showToastNotification(context, message: error.msg), + ); + } +} + +class _ExportButton extends StatelessWidget { + const _ExportButton({ + required this.title, + required this.svg, + required this.onTap, + }); + + final String title; + final FlowySvgData svg; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + final color = Theme.of(context).isLightMode + ? const Color(0x1E14171B) + : Colors.white.withOpacity(0.1); + final radius = BorderRadius.circular(10.0); + return FlowyButton( + margin: const EdgeInsets.symmetric(vertical: 10, horizontal: 14), + iconPadding: 12, + decoration: BoxDecoration( + border: Border.all( + color: color, + ), + borderRadius: radius, + ), + radius: radius, + text: FlowyText(title), + leftIcon: FlowySvg(svg), + onTap: onTap, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/share/pubish_color_extension.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/share/pubish_color_extension.dart new file mode 100644 index 0000000000..960f59b07d --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/share/pubish_color_extension.dart @@ -0,0 +1,11 @@ +import 'package:appflowy/util/theme_extension.dart'; +import 'package:flutter/material.dart'; + +class ShareMenuColors { + static Color borderColor(BuildContext context) { + final borderColor = Theme.of(context).isLightMode + ? const Color(0x1E14171B) + : Colors.white.withOpacity(0.1); + return borderColor; + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/share/publish_name_generator.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/share/publish_name_generator.dart new file mode 100644 index 0000000000..eae1d56a18 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/share/publish_name_generator.dart @@ -0,0 +1,19 @@ +String replaceInvalidChars(String input) { + final RegExp invalidCharsRegex = RegExp('[^a-zA-Z0-9-]'); + return input.replaceAll(invalidCharsRegex, '-'); +} + +Future generateNameSpace() async { + return ''; +} + +// The backend limits the publish name to a maximum of 120 characters. +// If the combined length of the ID and the name exceeds 120 characters, +// we will truncate the name to ensure the final result is within the limit. +// The name should only contain alphanumeric characters and hyphens. +Future generatePublishName(String id, String name) async { + if (name.length >= 120 - id.length) { + name = name.substring(0, 120 - id.length); + } + return replaceInvalidChars('$name-$id'); +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/share/publish_tab.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/share/publish_tab.dart new file mode 100644 index 0000000000..f5e52b8958 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/share/publish_tab.dart @@ -0,0 +1,284 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/document/application/document_share_bloc.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart'; +import 'package:appflowy/plugins/document/presentation/share/pubish_color_extension.dart'; +import 'package:appflowy/plugins/document/presentation/share/publish_name_generator.dart'; +import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flowy_infra_ui/style_widget/hover.dart'; +import 'package:flowy_infra_ui/widget/rounded_button.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class PublishTab extends StatelessWidget { + const PublishTab({super.key}); + + @override + Widget build(BuildContext context) { + return BlocConsumer( + listener: (context, state) { + _showToast(context, state); + }, + builder: (context, state) { + return state.isPublished + ? _PublishedWidget( + url: state.url, + onVisitSite: () {}, + onUnPublish: () { + context + .read() + .add(const DocumentShareEvent.unPublish()); + }, + ) + : _UnPublishWidget( + onPublish: () async { + final id = context.read().view.id; + final publishName = await generatePublishName( + id, + state.viewName, + ); + if (context.mounted) { + context.read().add( + DocumentShareEvent.publish('', publishName), + ); + } + }, + ); + }, + ); + } + + void _showToast(BuildContext context, DocumentShareState state) { + if (state.publishResult != null) { + state.publishResult!.fold( + (value) => showToastNotification( + context, + message: LocaleKeys.publish_publishSuccessfully.tr(), + ), + (error) => showToastNotification( + context, + message: '${LocaleKeys.publish_publishFailed.tr()}: ${error.code}', + ), + ); + } else if (state.unpublishResult != null) { + state.unpublishResult!.fold( + (value) => showToastNotification( + context, + message: LocaleKeys.publish_unpublishSuccessfully.tr(), + ), + (error) => showToastNotification( + context, + message: LocaleKeys.publish_unpublishFailed.tr(), + description: error.msg, + ), + ); + } + } +} + +class _PublishedWidget extends StatefulWidget { + const _PublishedWidget({ + required this.url, + required this.onVisitSite, + required this.onUnPublish, + }); + + final String url; + final VoidCallback onVisitSite; + final VoidCallback onUnPublish; + + @override + State<_PublishedWidget> createState() => _PublishedWidgetState(); +} + +class _PublishedWidgetState extends State<_PublishedWidget> { + final controller = TextEditingController(); + + @override + void initState() { + super.initState(); + controller.text = widget.url; + } + + @override + void dispose() { + controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const VSpace(16), + const _PublishTabHeader(), + const VSpace(16), + _PublishUrl( + controller: controller, + onCopy: (url) { + getIt().setData( + ClipboardServiceData(plainText: url), + ); + + showToastNotification( + context, + message: LocaleKeys.grid_url_copy.tr(), + ); + }, + onSubmitted: (url) {}, + ), + const VSpace(16), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + _buildUnpublishButton(), + const Spacer(), + _buildVisitSiteButton(), + ], + ), + ], + ); + } + + Widget _buildUnpublishButton() { + return SizedBox( + height: 36, + width: 184, + child: FlowyButton( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + border: Border.all(color: ShareMenuColors.borderColor(context)), + ), + radius: BorderRadius.circular(10), + text: FlowyText.regular( + LocaleKeys.shareAction_unPublish.tr(), + textAlign: TextAlign.center, + ), + onTap: widget.onUnPublish, + ), + ); + } + + Widget _buildVisitSiteButton() { + return RoundedTextButton( + onPressed: () { + safeLaunchUrl(controller.text); + }, + title: LocaleKeys.shareAction_visitSite.tr(), + width: 184, + height: 36, + borderRadius: const BorderRadius.all(Radius.circular(10)), + fillColor: Theme.of(context).colorScheme.primary, + textColor: Theme.of(context).colorScheme.onPrimary, + ); + } +} + +class _UnPublishWidget extends StatelessWidget { + const _UnPublishWidget({ + required this.onPublish, + }); + + final VoidCallback onPublish; + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const VSpace(16), + const _PublishTabHeader(), + const VSpace(16), + RoundedTextButton( + height: 36, + title: LocaleKeys.shareAction_publish.tr(), + padding: const EdgeInsets.symmetric(vertical: 9.0), + fontSize: 14.0, + textColor: Theme.of(context).colorScheme.onPrimary, + onPressed: onPublish, + ), + ], + ); + } +} + +class _PublishTabHeader extends StatelessWidget { + const _PublishTabHeader(); + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + const FlowySvg(FlowySvgs.share_publish_s), + const HSpace(6), + FlowyText(LocaleKeys.shareAction_publishToTheWeb.tr()), + ], + ), + const VSpace(4), + FlowyText.regular( + LocaleKeys.shareAction_publishToTheWebHint.tr(), + fontSize: 12, + maxLines: 3, + color: Theme.of(context).hintColor, + ), + ], + ); + } +} + +class _PublishUrl extends StatelessWidget { + const _PublishUrl({ + required this.controller, + required this.onCopy, + required this.onSubmitted, + }); + + final TextEditingController controller; + final void Function(String url) onCopy; + final void Function(String url) onSubmitted; + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 36, + child: FlowyTextField( + readOnly: true, + autoFocus: false, + controller: controller, + enableBorderColor: ShareMenuColors.borderColor(context), + suffixIcon: _buildCopyLinkIcon(context), + ), + ); + } + + Widget _buildCopyLinkIcon(BuildContext context) { + return FlowyHover( + child: GestureDetector( + onTap: () => onCopy(controller.text), + child: Container( + width: 36, + height: 36, + alignment: Alignment.center, + padding: const EdgeInsets.all(10), + decoration: const BoxDecoration( + border: Border(left: BorderSide(color: Color(0x141F2329))), + ), + child: const FlowySvg( + FlowySvgs.m_toolbar_link_m, + ), + ), + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/share/share_button.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/share/share_button.dart index eb82f3d1fa..0770cc98fc 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/share/share_button.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/share/share_button.dart @@ -1,6 +1,7 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/application/document_share_bloc.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart'; +import 'package:appflowy/plugins/document/presentation/share/share_menu.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/util/string_extension.dart'; import 'package:appflowy/workspace/application/export/document_exporter.dart'; @@ -13,6 +14,7 @@ import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/file_picker/file_picker_service.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/widget/rounded_button.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -28,23 +30,44 @@ class DocumentShareButton extends StatelessWidget { @override Widget build(BuildContext context) { return BlocProvider( - create: (context) => getIt(param1: view), + create: (context) => getIt(param1: view) + ..add(const DocumentShareEvent.initial()), child: BlocListener( listener: (context, state) { - state.mapOrNull( - finish: (state) { - state.successOrFail.fold( - (data) => _handleExportData(context, data), - _handleExportError, - ); - }, - ); + if (state.isLoading == false && state.exportResult != null) { + state.exportResult!.fold( + (data) => _handleExportData(context, data), + _handleExportError, + ); + } }, child: BlocBuilder( - builder: (context, state) => SizedBox( - height: 32.0, - child: IntrinsicWidth(child: ShareActionList(view: view)), - ), + builder: (context, state) { + final tabs = [ + if (state.enablePublish) ShareMenuTab.publish, + ShareMenuTab.exportAs, + ]; + final shareBloc = context.read(); + return SizedBox( + height: 32.0, + child: IntrinsicWidth( + child: AppFlowyPopover( + direction: PopoverDirection.bottomWithRightAligned, + constraints: const BoxConstraints( + maxWidth: 422, + ), + offset: const Offset(0, 8), + popupBuilder: (context) => BlocProvider.value( + value: shareBloc, + child: ShareMenu( + tabs: tabs, + ), + ), + child: const InnerDocumentShareButton(), + ), + ), + ); + }, ), ), ); @@ -75,6 +98,24 @@ class DocumentShareButton extends StatelessWidget { } } +class InnerDocumentShareButton extends StatelessWidget { + const InnerDocumentShareButton({super.key}); + + @override + Widget build(BuildContext context) { + return RoundedTextButton( + title: LocaleKeys.shareAction_buttonText.tr(), + padding: const EdgeInsets.symmetric(horizontal: 14.0), + fontSize: 14.0, + fontWeight: FontWeight.w500, + borderRadius: const BorderRadius.all( + Radius.circular(10.0), + ), + textColor: Theme.of(context).colorScheme.onPrimary, + ); + } +} + class ShareActionList extends StatefulWidget { const ShareActionList({ super.key, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/share/share_menu.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/share/share_menu.dart new file mode 100644 index 0000000000..2c27f4135d --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/share/share_menu.dart @@ -0,0 +1,186 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/mobile/presentation/home/tab/_round_underline_tab_indicator.dart'; +import 'package:appflowy/plugins/document/application/document_share_bloc.dart'; +import 'package:appflowy/plugins/document/presentation/share/export_tab.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'publish_tab.dart'; + +enum ShareMenuTab { + share, + publish, + exportAs; + + String get i18n { + switch (this) { + case ShareMenuTab.share: + return LocaleKeys.shareAction_shareTab.tr(); + case ShareMenuTab.publish: + return LocaleKeys.shareAction_publishTab.tr(); + case ShareMenuTab.exportAs: + return LocaleKeys.shareAction_exportAsTab.tr(); + } + } +} + +class ShareMenu extends StatefulWidget { + const ShareMenu({ + super.key, + required this.tabs, + }); + + final List tabs; + + @override + State createState() => _ShareMenuState(); +} + +class _ShareMenuState extends State + with SingleTickerProviderStateMixin { + late ShareMenuTab selectedTab = widget.tabs.first; + late final tabController = TabController( + length: widget.tabs.length, + vsync: this, + initialIndex: widget.tabs.indexOf(selectedTab), + ); + + @override + Widget build(BuildContext context) { + if (widget.tabs.isEmpty) { + return const SizedBox.shrink(); + } + + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + const VSpace(10), + Container( + alignment: Alignment.centerLeft, + height: 30, + child: _buildTabBar(context), + ), + Divider( + color: Theme.of(context).dividerColor, + height: 1, + thickness: 1, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 14.0), + child: _buildTab(context), + ), + const VSpace(20), + ], + ); + } + + @override + void dispose() { + tabController.dispose(); + super.dispose(); + } + + Widget _buildTabBar(BuildContext context) { + final children = [ + for (final tab in widget.tabs) + Padding( + padding: const EdgeInsets.only(bottom: 10), + child: _Segment( + tab: tab, + isSelected: selectedTab == tab, + ), + ), + ]; + return TabBar( + indicatorSize: TabBarIndicatorSize.label, + indicator: RoundUnderlineTabIndicator( + width: 68.0, + borderSide: BorderSide( + color: Theme.of(context).colorScheme.primary, + width: 3, + ), + insets: const EdgeInsets.only(bottom: -2), + ), + isScrollable: true, + controller: tabController, + tabs: children, + onTap: (index) { + setState(() { + selectedTab = widget.tabs[index]; + }); + }, + ); + } + + Widget _buildTab(BuildContext context) { + switch (selectedTab) { + case ShareMenuTab.publish: + return const PublishTab(); + case ShareMenuTab.exportAs: + return const ExportTab(); + default: + return const Center( + child: FlowyText('🏡 under construction'), + ); + } + } +} + +class _Segment extends StatefulWidget { + const _Segment({ + required this.tab, + required this.isSelected, + }); + + final bool isSelected; + final ShareMenuTab tab; + + @override + State<_Segment> createState() => _SegmentState(); +} + +class _SegmentState extends State<_Segment> { + bool isHovered = false; + + @override + Widget build(BuildContext context) { + Color? textColor = Theme.of(context).hintColor; + if (isHovered) { + textColor = const Color(0xFF00BCF0); + } else if (widget.isSelected) { + textColor = null; + } + + Widget child = MouseRegion( + onEnter: (_) => setState(() => isHovered = true), + onExit: (_) => setState(() => isHovered = false), + child: FlowyText( + widget.tab.i18n, + textAlign: TextAlign.center, + color: textColor, + ), + ); + + if (widget.tab == ShareMenuTab.publish) { + final isPublished = context.watch().state.isPublished; + // show checkmark icon if published + if (isPublished) { + child = Row( + children: [ + const FlowySvg( + FlowySvgs.published_checkmark_s, + blendMode: null, + ), + const HSpace(6), + child, + ], + ); + } + } + + return child; + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/application/view/view_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/view/view_bloc.dart index ff486fbace..44300468a1 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/view/view_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/view/view_bloc.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:convert'; import 'package:appflowy/core/config/kv.dart'; @@ -7,6 +8,7 @@ import 'package:appflowy/workspace/application/favorite/favorite_listener.dart'; import 'package:appflowy/workspace/application/recent/cached_recent_service.dart'; import 'package:appflowy/workspace/application/view/view_listener.dart'; import 'package:appflowy/workspace/application/view/view_service.dart'; +import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; @@ -138,11 +140,18 @@ class ViewBloc extends Bloc { ); }, delete: (e) async { + // unpublish the page and all its child pages if they are published + await _unpublishPage(view); + final result = await ViewBackendService.delete(viewId: view.id); + emit( result.fold( - (l) => - state.copyWith(successOrFailure: FlowyResult.success(null)), + (l) { + return state.copyWith( + successOrFailure: FlowyResult.success(null), + ); + }, (error) => state.copyWith( successOrFailure: FlowyResult.failure(error), ), @@ -180,8 +189,11 @@ class ViewBloc extends Bloc { ); emit( result.fold( - (l) => - state.copyWith(successOrFailure: FlowyResult.success(null)), + (l) { + return state.copyWith( + successOrFailure: FlowyResult.success(null), + ); + }, (error) => state.copyWith( successOrFailure: FlowyResult.failure(error), ), @@ -236,6 +248,13 @@ class ViewBloc extends Bloc { } add(const ViewEvent.setIsExpanded(false)); }, + unpublish: (value) async { + if (value.sync) { + await _unpublishPage(view); + } else { + unawaited(_unpublishPage(view)); + } + }, ); }, ); @@ -383,6 +402,20 @@ class ViewBloc extends Bloc { return null; } + // unpublish the page and all its child pages + Future _unpublishPage(ViewPB views) async { + final (_, publishedPages) = await ViewBackendService.containPublishedPage( + view, + ); + + await Future.wait( + publishedPages.map((view) async { + Log.info('unpublishing page: ${view.id}, ${view.name}'); + await ViewBackendService.unpublish(view); + }), + ); + } + bool _isSameViewIgnoreChildren(ViewPB from, ViewPB to) { return _hash(from) == _hash(to); } @@ -430,6 +463,8 @@ class ViewEvent with _$ViewEvent { ) = UpdateViewVisibility; const factory ViewEvent.updateIcon(String? icon) = UpdateIcon; const factory ViewEvent.collapseAllPages() = CollapseAllPages; + // this event will unpublish the page and all its child pages if they are published + const factory ViewEvent.unpublish({required bool sync}) = Unpublish; } @freezed diff --git a/frontend/appflowy_flutter/lib/workspace/application/view/view_service.dart b/frontend/appflowy_flutter/lib/workspace/application/view/view_service.dart index 4d59958333..f07b90ef4f 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/view/view_service.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/view/view_service.dart @@ -291,4 +291,78 @@ class ViewBackendService { ); return FolderEventUpdateViewVisibilityStatus(payload).send(); } + + static Future> getPublishInfo( + ViewPB view, + ) async { + final payload = ViewIdPB()..value = view.id; + return FolderEventGetPublishInfo(payload).send(); + } + + static Future> publish( + ViewPB view, { + String? name, + }) async { + final payload = PublishViewParamsPB()..viewId = view.id; + + if (name != null) { + payload.publishName = name; + } + return FolderEventPublishView(payload).send(); + } + + static Future> unpublish( + ViewPB view, + ) async { + final payload = UnpublishViewsPayloadPB(viewIds: [view.id]); + return FolderEventUnpublishViews(payload).send(); + } + + static Future> setPublishNameSpace( + String name, + ) async { + final payload = SetPublishNamespacePayloadPB()..newNamespace = name; + return FolderEventSetPublishNamespace(payload).send(); + } + + static Future> + getPublishNameSpace() async { + return FolderEventGetPublishNamespace().send(); + } + + static Future> getAllChildViews(ViewPB view) async { + final views = []; + + final childViews = + await ViewBackendService.getChildViews(viewId: view.id).fold( + (s) => s, + (f) => [], + ); + + for (final child in childViews) { + // filter the view itself + if (child.id == view.id) { + continue; + } + views.add(child); + views.addAll(await getAllChildViews(child)); + } + + return views; + } + + static Future<(bool, List)> containPublishedPage(ViewPB view) async { + final childViews = await ViewBackendService.getAllChildViews(view); + final views = [view, ...childViews]; + final List publishedPages = []; + + for (final view in views) { + final publishInfo = await ViewBackendService.getPublishInfo(view); + if (publishInfo.isSuccess) { + publishedPages.add(view); + } + } + + return (publishedPages.isNotEmpty, publishedPages); + } } diff --git a/frontend/appflowy_flutter/lib/workspace/application/view_title/view_title_bar_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/view_title/view_title_bar_bloc.dart index 491ff36786..35f13d3de2 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/view_title/view_title_bar_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/view_title/view_title_bar_bloc.dart @@ -15,6 +15,14 @@ class ViewTitleBarBloc extends Bloc { await event.when( initial: () async { add(const ViewTitleBarEvent.reload()); + + viewListener = ViewListener( + viewId: view.id, + )..start( + onViewUpdated: (p0) { + add(const ViewTitleBarEvent.reload()); + }, + ); }, reload: () async { final List ancestors = @@ -30,6 +38,13 @@ class ViewTitleBarBloc extends Bloc { } final ViewPB view; + late final ViewListener viewListener; + + @override + Future close() { + viewListener.stop(); + return super.close(); + } } @freezed diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/move_to/move_page_menu.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/move_to/move_page_menu.dart index 6146311648..7fb3c6902f 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/move_to/move_page_menu.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/move_to/move_page_menu.dart @@ -11,6 +11,8 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +typedef MovePageMenuOnSelected = void Function(ViewPB space, ViewPB view); + class MovePageMenu extends StatefulWidget { const MovePageMenu({ super.key, @@ -23,7 +25,7 @@ class MovePageMenu extends StatefulWidget { final ViewPB sourceView; final UserProfilePB userProfile; final String workspaceId; - final void Function(ViewPB view) onSelected; + final MovePageMenuOnSelected onSelected; @override State createState() => _MovePageMenuState(); @@ -88,7 +90,7 @@ class _MovePageMenuState extends State { ); } return Expanded( - child: _buildGroupedViews(state.queryResults!), + child: _buildGroupedViews(space, state.queryResults!), ); }, ), @@ -99,7 +101,7 @@ class _MovePageMenuState extends State { ); } - Widget _buildGroupedViews(List views) { + Widget _buildGroupedViews(ViewPB space, List views) { final groupedViews = views .where( (view) => @@ -108,7 +110,7 @@ class _MovePageMenuState extends State { .toList(); return _MovePageGroupedViews( views: groupedViews, - onSelected: widget.onSelected, + onSelected: (view) => widget.onSelected(space, view), ); } @@ -124,7 +126,7 @@ class _MovePageMenuState extends State { child: CurrentSpace( onTapBlankArea: () { // move the page to current space - widget.onSelected(space); + widget.onSelected(space, space); }, space: space, ), @@ -145,7 +147,7 @@ class _MovePageMenuState extends State { disableSelectedStatus: true, // hide the ... and + buttons rightIconsBuilder: (context, view) => [], - onSelected: (_, view) => widget.onSelected(view), + onSelected: (_, view) => widget.onSelected(space, view), ), ), ), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/shared_widget.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/shared_widget.dart index edc15dea5e..d82e81f48a 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/shared_widget.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/shared_widget.dart @@ -19,6 +19,7 @@ import 'package:flowy_infra_ui/style_widget/hover.dart'; import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class SpacePermissionSwitch extends StatefulWidget { @@ -222,55 +223,82 @@ class SpaceCancelOrConfirmButton extends StatelessWidget { } } -class DeleteSpacePopup extends StatelessWidget { - const DeleteSpacePopup({super.key}); +class ConfirmDeletionPopup extends StatefulWidget { + const ConfirmDeletionPopup({ + super.key, + required this.title, + required this.description, + required this.onConfirm, + }); + + final String title; + final String description; + final VoidCallback onConfirm; + + @override + State createState() => _ConfirmDeletionPopupState(); +} + +class _ConfirmDeletionPopupState extends State { + final focusNode = FocusNode(); @override Widget build(BuildContext context) { - final space = context.read().state.currentSpace; - final name = space != null ? space.name : ''; - return Padding( - padding: const EdgeInsets.symmetric( - vertical: 20.0, - horizontal: 20.0, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - FlowyText( - LocaleKeys.space_deleteConfirmation.tr() + name, - fontSize: 14.0, - ), - const Spacer(), - FlowyButton( - useIntrinsicWidth: true, - text: const FlowySvg(FlowySvgs.upgrade_close_s), - onTap: () => Navigator.of(context).pop(), - ), - ], - ), - const VSpace(8.0), - FlowyText.regular( - LocaleKeys.space_deleteConfirmationDescription.tr(), - fontSize: 12.0, - color: Theme.of(context).hintColor, - maxLines: 3, - lineHeight: 1.4, - ), - const VSpace(20.0), - SpaceCancelOrConfirmButton( - onCancel: () => Navigator.of(context).pop(), - onConfirm: () { - context.read().add(const SpaceEvent.delete(null)); - Navigator.of(context).pop(); - }, - confirmButtonName: LocaleKeys.space_delete.tr(), - confirmButtonColor: Theme.of(context).colorScheme.error, - ), - ], + return KeyboardListener( + focusNode: focusNode, + autofocus: true, + onKeyEvent: (event) { + if (event is KeyDownEvent && + event.logicalKey == LogicalKeyboardKey.escape) { + Navigator.of(context).pop(); + } + }, + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 20.0, + horizontal: 20.0, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Flexible( + child: FlowyText( + widget.title, + fontSize: 14.0, + overflow: TextOverflow.ellipsis, + ), + ), + const HSpace(6.0), + FlowyButton( + useIntrinsicWidth: true, + text: const FlowySvg(FlowySvgs.upgrade_close_s), + onTap: () => Navigator.of(context).pop(), + ), + ], + ), + const VSpace(8.0), + FlowyText.regular( + widget.description, + fontSize: 12.0, + color: Theme.of(context).hintColor, + maxLines: 3, + lineHeight: 1.4, + ), + const VSpace(20.0), + SpaceCancelOrConfirmButton( + onCancel: () => Navigator.of(context).pop(), + onConfirm: () { + widget.onConfirm(); + Navigator.of(context).pop(); + }, + confirmButtonName: LocaleKeys.space_delete.tr(), + confirmButtonColor: Theme.of(context).colorScheme.error, + ), + ], + ), ), ); } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/sidebar_space_header.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/sidebar_space_header.dart index 9adcdb6822..2eaafe90f9 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/sidebar_space_header.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/sidebar_space_header.dart @@ -227,18 +227,14 @@ class _SidebarSpaceHeaderState extends State { void _showDeleteSpaceDialog(BuildContext context) { final spaceBloc = context.read(); - showDialog( + final space = spaceBloc.state.currentSpace; + final name = space != null ? space.name : ''; + showConfirmDeletionDialog( context: context, - builder: (_) { - return Dialog( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12.0), - ), - child: BlocProvider.value( - value: spaceBloc, - child: const SizedBox(width: 440, child: DeleteSpacePopup()), - ), - ); + name: name, + description: LocaleKeys.space_deleteConfirmationDescription.tr(), + onConfirm: () { + context.read().add(const SpaceEvent.delete(null)); }, ); } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_actions.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_actions.dart index 53baa2599e..1df1b0fe2a 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_actions.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_actions.dart @@ -105,14 +105,16 @@ class _WorkspaceMoreActionWrapper extends CustomActionCell { case WorkspaceMoreAction.divider: break; case WorkspaceMoreAction.delete: - await NavigatorAlertDialog( - title: LocaleKeys.workspace_deleteWorkspaceHintText.tr(), - confirm: () { + await showConfirmDeletionDialog( + context: context, + name: workspace.name, + description: LocaleKeys.workspace_deleteWorkspaceHintText.tr(), + onConfirm: () { workspaceBloc.add( UserWorkspaceEvent.deleteWorkspace(workspace.workspaceId), ); }, - ).show(context); + ); case WorkspaceMoreAction.rename: await NavigatorTextFieldDialog( title: LocaleKeys.workspace_renameWorkspace.tr(), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_item.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_item.dart index 9145c9ac78..5ad37d0e49 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_item.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_item.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/base/icon/icon_picker.dart'; @@ -5,6 +7,7 @@ import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart'; import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart'; import 'package:appflowy/workspace/application/sidebar/rename_view/rename_view_bloc.dart'; +import 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart'; import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart'; import 'package:appflowy/workspace/application/view/prelude.dart'; import 'package:appflowy/workspace/application/view/view_ext.dart'; @@ -17,9 +20,10 @@ import 'package:appflowy/workspace/presentation/home/menu/view/view_add_button.d import 'package:appflowy/workspace/presentation/home/menu/view/view_more_action_button.dart'; import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy/workspace/presentation/widgets/rename_view_popover.dart'; +import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_editor/appflowy_editor.dart' hide Log; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; @@ -334,6 +338,7 @@ class _InnerViewItemState extends State { onMove: widget.isPlaceholder ? (from, to) => _moveViewCrossSection( context, + null, widget.view, widget.parentView, widget.spaceType, @@ -690,7 +695,7 @@ class _SingleInnerViewItemState extends State { spaceType: widget.spaceType, onEditing: (value) => context.read().add(ViewEvent.setIsEditing(value)), - onAction: (action, data) { + onAction: (action, data) async { switch (action) { case ViewMoreActionType.favorite: case ViewMoreActionType.unFavorite: @@ -699,18 +704,36 @@ class _SingleInnerViewItemState extends State { .add(FavoriteEvent.toggle(widget.view)); break; case ViewMoreActionType.rename: - NavigatorTextFieldDialog( - title: LocaleKeys.disclosureAction_rename.tr(), - autoSelectAllText: true, - value: widget.view.name, - maxLength: 256, - onConfirm: (newValue, _) { - context.read().add(ViewEvent.rename(newValue)); - }, - ).show(context); + unawaited( + NavigatorTextFieldDialog( + title: LocaleKeys.disclosureAction_rename.tr(), + autoSelectAllText: true, + value: widget.view.name, + maxLength: 256, + onConfirm: (newValue, _) { + context.read().add(ViewEvent.rename(newValue)); + }, + ).show(context), + ); break; case ViewMoreActionType.delete: - context.read().add(const ViewEvent.delete()); + // get if current page contains published child views + final (containPublishedPage, _) = + await ViewBackendService.containPublishedPage( + widget.view, + ); + if (containPublishedPage && context.mounted) { + await showConfirmDeletionDialog( + context: context, + name: widget.view.name, + description: LocaleKeys.publish_containsPublishedPage.tr(), + onConfirm: () { + context.read().add(const ViewEvent.delete()); + }, + ); + } else if (context.mounted) { + context.read().add(const ViewEvent.delete()); + } break; case ViewMoreActionType.duplicate: context.read().add(const ViewEvent.duplicate()); @@ -726,22 +749,22 @@ class _SingleInnerViewItemState extends State { return; } final result = data; - ViewBackendService.updateViewIcon( + await ViewBackendService.updateViewIcon( viewId: widget.view.id, viewIcon: result.emoji, iconType: result.type.toProto(), ); break; case ViewMoreActionType.moveTo: - final target = data; - if (target is! ViewPB) { + final value = data; + if (value is! (ViewPB, ViewPB)) { return; } - debugPrint( - 'Move view ${widget.view.id}, ${widget.view.name} to ${target.id}, ${target.name}', - ); + final space = value.$1; + final target = value.$2; _moveViewCrossSection( context, + space, widget.view, widget.parentView, widget.spaceType, @@ -802,6 +825,7 @@ bool isReferencedDatabaseView(ViewPB view, ViewPB? parentView) { void _moveViewCrossSection( BuildContext context, + ViewPB? toSpace, ViewPB view, ViewPB? parentView, FolderSpaceType spaceType, @@ -822,6 +846,17 @@ void _moveViewCrossSection( final toSection = spaceType == FolderSpaceType.public ? ViewSectionPB.Public : ViewSectionPB.Private; + + final currentSpace = context.read().state.currentSpace; + if (currentSpace != null && + toSpace != null && + currentSpace.id != toSpace.id) { + Log.info( + 'Move view(${from.name}) to another space(${toSpace.name}), unpublish the view', + ); + context.read().add(const ViewEvent.unpublish(sync: false)); + } + context.read().add( ViewEvent.move( from, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_more_action_button.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_more_action_button.dart index 0cecd4849c..9b472fc57d 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_more_action_button.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_more_action_button.dart @@ -192,8 +192,8 @@ class ViewMoreActionTypeWrapper extends CustomActionCell { sourceView: sourceView, userProfile: userProfile, workspaceId: workspaceId, - onSelected: (view) { - onTap(controller, view); + onSelected: (space, view) { + onTap(controller, (space, view)); }, ); }, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart index 7576530c17..8d1196b503 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart @@ -1,7 +1,6 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/startup/tasks/app_widget.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/size.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; @@ -10,6 +9,8 @@ import 'package:flowy_infra_ui/widget/buttons/primary_button.dart'; import 'package:flowy_infra_ui/widget/buttons/secondary_button.dart'; import 'package:flowy_infra_ui/widget/dialog/styled_dialogs.dart'; import 'package:flowy_infra_ui/widget/spacing.dart'; +import 'package:flutter/material.dart'; +import 'package:toastification/toastification.dart'; export 'package:flowy_infra_ui/widget/dialog/styled_dialogs.dart'; @@ -283,3 +284,58 @@ class OkCancelButton extends StatelessWidget { ); } } + +void showToastNotification( + BuildContext context, { + required String message, + String? description, +}) { + toastification.show( + context: context, + type: ToastificationType.success, + style: ToastificationStyle.flat, + title: FlowyText(message), + description: description != null + ? FlowyText.regular( + description, + fontSize: 12, + lineHeight: 1.2, + maxLines: 3, + ) + : null, + alignment: Alignment.bottomCenter, + autoCloseDuration: const Duration(milliseconds: 3000), + showProgressBar: false, + backgroundColor: Theme.of(context).colorScheme.surface, + borderSide: BorderSide( + color: Colors.grey.withOpacity(0.4), + ), + ); +} + +Future showConfirmDeletionDialog({ + required BuildContext context, + required String name, + required String description, + required VoidCallback onConfirm, +}) { + return showDialog( + context: context, + builder: (_) { + final title = LocaleKeys.space_deleteConfirmation.tr() + name; + return Dialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12.0), + ), + child: SizedBox( + width: 440, + child: ConfirmDeletionPopup( + title: title, + description: description, + onConfirm: onConfirm, + ), + ), + ); + }, + ); +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/widgets/common_view_action.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/widgets/common_view_action.dart index c54f6169a2..a2fb503553 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/widgets/common_view_action.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/widgets/common_view_action.dart @@ -1,14 +1,15 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/workspace/application/view/view_bloc.dart'; +import 'package:appflowy/workspace/application/view/view_service.dart'; +import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/style_widget/button.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; enum ViewActionType { @@ -46,8 +47,8 @@ class ViewAction extends StatelessWidget { @override Widget build(BuildContext context) { return FlowyButton( - onTap: () { - context.read().add(type.actionEvent); + onTap: () async { + await _onAction(context); mutex?.close(); }, text: FlowyText.regular( @@ -63,4 +64,25 @@ class ViewAction extends StatelessWidget { hoverColor: AFThemeExtension.of(context).lightGreyHover, ); } + + Future _onAction(BuildContext context) async { + if (type == ViewActionType.delete) { + final (containPublishedPage, _) = + await ViewBackendService.containPublishedPage(view); + if (containPublishedPage && context.mounted) { + await showConfirmDeletionDialog( + context: context, + name: view.name, + description: LocaleKeys.publish_containsPublishedPage.tr(), + onConfirm: () { + context.read().add(const ViewEvent.delete()); + }, + ); + } else if (context.mounted) { + context.read().add(const ViewEvent.delete()); + } + } else { + context.read().add(type.actionEvent); + } + } } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/view_title_bar.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/view_title_bar.dart index ea453e4666..7f06849908 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/view_title_bar.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/view_title_bar.dart @@ -37,7 +37,9 @@ class ViewTitleBar extends StatelessWidget { scrollDirection: Axis.horizontal, child: SizedBox( height: 24, - child: Row(children: _buildViewTitles(context, ancestors)), + child: Row( + children: _buildViewTitles(context, ancestors), + ), ), ); }, @@ -76,6 +78,7 @@ class ViewTitleBar extends StatelessWidget { } final child = FlowyTooltip( + key: ValueKey(view.id), message: view.name, child: _ViewTitle( view: view, diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text_field.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text_field.dart index 1b46682628..32cc2fa1fc 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text_field.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text_field.dart @@ -1,10 +1,9 @@ import 'dart:async'; +import 'package:flowy_infra/size.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:flowy_infra/size.dart'; - class FlowyTextField extends StatefulWidget { final String? hintText; final String? text; @@ -37,6 +36,8 @@ class FlowyTextField extends StatefulWidget { final List? inputFormatters; final bool obscureText; final bool isDense; + final bool readOnly; + final Color? enableBorderColor; const FlowyTextField({ super.key, @@ -71,6 +72,8 @@ class FlowyTextField extends StatefulWidget { this.inputFormatters, this.obscureText = false, this.isDense = true, + this.readOnly = false, + this.enableBorderColor, }); @override @@ -144,6 +147,7 @@ class FlowyTextFieldState extends State { @override Widget build(BuildContext context) { return TextField( + readOnly: widget.readOnly, controller: controller, focusNode: focusNode, onChanged: (text) { @@ -178,7 +182,8 @@ class FlowyTextFieldState extends State { enabledBorder: OutlineInputBorder( borderRadius: Corners.s8Border, borderSide: BorderSide( - color: Theme.of(context).colorScheme.outline, + color: widget.enableBorderColor ?? + Theme.of(context).colorScheme.outline, ), ), isDense: false, @@ -199,7 +204,10 @@ class FlowyTextFieldState extends State { focusedBorder: OutlineInputBorder( borderRadius: Corners.s8Border, borderSide: BorderSide( - color: Theme.of(context).colorScheme.primary, + color: widget.readOnly + ? widget.enableBorderColor ?? + Theme.of(context).colorScheme.outline + : Theme.of(context).colorScheme.primary, ), ), errorBorder: OutlineInputBorder( diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/rounded_button.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/rounded_button.dart index a73d96f454..0fa181a1dd 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/rounded_button.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/rounded_button.dart @@ -13,6 +13,7 @@ class RoundedTextButton extends StatelessWidget { final Color? hoverColor; final Color? textColor; final double? fontSize; + final FontWeight? fontWeight; final EdgeInsets padding; const RoundedTextButton({ @@ -27,6 +28,7 @@ class RoundedTextButton extends StatelessWidget { this.hoverColor, this.textColor, this.fontSize, + this.fontWeight, this.padding = const EdgeInsets.symmetric(horizontal: 8, vertical: 6), }); @@ -42,6 +44,7 @@ class RoundedTextButton extends StatelessWidget { child: SizedBox.expand( child: FlowyTextButton( title ?? '', + fontWeight: fontWeight, onPressed: onPressed, fontSize: fontSize, mainAxisAlignment: MainAxisAlignment.center, diff --git a/frontend/appflowy_flutter/pubspec.lock b/frontend/appflowy_flutter/pubspec.lock index defd58dadf..d223227470 100644 --- a/frontend/appflowy_flutter/pubspec.lock +++ b/frontend/appflowy_flutter/pubspec.lock @@ -394,6 +394,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + custom_sliding_segmented_control: + dependency: "direct main" + description: + name: custom_sliding_segmented_control + sha256: "53c3e931c3ae1f696085d1ec70ac8e934da836595a9b7d9b88fdd0fcbf2a5574" + url: "https://pub.dev" + source: hosted + version: "1.8.3" dart_style: dependency: transitive description: @@ -949,6 +957,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.2" + iconsax_flutter: + dependency: transitive + description: + name: iconsax_flutter + sha256: "95b65699da8ea98f87c5d232f06b0debaaf1ec1332b697e4d90969ec9a93037d" + url: "https://pub.dev" + source: hosted + version: "1.0.0" image_gallery_saver: dependency: "direct main" description: @@ -1410,6 +1426,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.1" + pausable_timer: + dependency: transitive + description: + name: pausable_timer + sha256: "6ef1a95441ec3439de6fb63f39a011b67e693198e7dae14e20675c3c00e86074" + url: "https://pub.dev" + source: hosted + version: "3.1.0+3" percent_indicator: dependency: "direct main" description: @@ -2049,6 +2073,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.1" + toastification: + dependency: "direct main" + description: + name: toastification + sha256: "5e751acc2fb5b8d008138dac255d62290fde4e5a24824f29809ac098c3dfe395" + url: "https://pub.dev" + source: hosted + version: "2.0.0" tuple: dependency: transitive description: diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index 51ec3d483f..368a07d99f 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -149,6 +149,8 @@ dependencies: bitsdojo_window: ^0.1.6 flutter_highlight: ^0.7.0 + custom_sliding_segmented_control: ^1.8.3 + toastification: ^2.0.0 dev_dependencies: flutter_lints: ^3.0.1 diff --git a/frontend/appflowy_web_app/Dockerfile b/frontend/appflowy_web_app/Dockerfile deleted file mode 100644 index 97e3a85559..0000000000 --- a/frontend/appflowy_web_app/Dockerfile +++ /dev/null @@ -1,34 +0,0 @@ -FROM oven/bun:latest - -WORKDIR /app - -RUN apt-get update && \ - apt-get install -y nginx - -RUN bun install cheerio pino axios pino-pretty - -COPY . . - -RUN addgroup --system nginx && \ - adduser --system --no-create-home --disabled-login --ingroup nginx nginx - -RUN apt-get clean && rm -rf /var/lib/apt/lists/* - - -COPY dist/ /usr/share/nginx/html/ - -COPY nginx.conf /etc/nginx/nginx.conf - -COPY nginx-signed.crt /etc/ssl/certs/nginx-signed.crt -COPY nginx-signed.key /etc/ssl/private/nginx-signed.key - -RUN chown -R nginx:nginx /etc/ssl/certs/nginx-signed.crt /etc/ssl/private/nginx-signed.key - -COPY start.sh /app/start.sh - -RUN chmod +x /app/start.sh - - -EXPOSE 80 443 - -CMD ["/app/start.sh"] diff --git a/frontend/appflowy_web_app/README.md b/frontend/appflowy_web_app/README.md index c5c8ebf51f..30777f7abb 100644 --- a/frontend/appflowy_web_app/README.md +++ b/frontend/appflowy_web_app/README.md @@ -1,284 +1,163 @@
- -

AppFlowy Web Project

- -
Welcome to the AppFlowy Web Project, a robust and versatile platform designed to bring the innovative features of -AppFlowy to the web. This project uniquely supports running as a desktop application via Tauri, and offers web -interfaces powered by WebAssembly (WASM). Dive into an exceptional development experience with high performance and -extensive capabilities.
- +
+

AppFlowy Web

+
+ + + + +
-## 🐑 Features +## 🌟 Introduction -- **Cross-Platform Compatibility**: Seamlessly run on desktop environments with Tauri, and on any web browser through - WASM. -- **High Performance**: Leverage the speed and efficiency of WebAssembly for your web interfaces. -- **Tauri Integration**: Build lightweight, secure, and efficient desktop applications. -- **Flexible Development**: Utilize a wide range of AppFlowy's functionalities in your web or desktop projects. +Welcome to the AppFlowy Web project! This project aims to bring the powerful features of AppFlowy to the web. Whether +you're a developer looking to contribute or a user eager to try out the latest features, this guide will help you get +started. -## 🚀 Getting Started +AppFlowy Web is built with the following technologies: -### 🛠️ Prerequisites +- **React**: A JavaScript library for building user interfaces. +- **TypeScript**: A typed superset of JavaScript that compiles to plain JavaScript. +- **Bun**: A fast all-in-one JavaScript runtime. +- **Nginx**: A high-performance web server. +- **Docker**: A platform to develop, ship, and run applications in containers. -Before you begin, ensure you have the following installed: +### Resource Sharing -- Node.js (v14 or later) -- Rust (latest stable version) -- Tauri prerequisites for your operating system -- PNPM (8.5.0) +To maintain consistency across different platforms, the Web project shares i18n translation files and Icons with the +Flutter project. This ensures a unified user experience and reduces duplication of effort in maintaining these +resources. -### 🏗️ Installation +- **i18n Translation Files**: The translation files are shared to provide a consistent localization experience across + both Web and Flutter applications. The path to the translation files is `frontend/resources/translations/`. -#### Clone the Repository + > The translation files are stored in JSON format and contain translations for different languages. The files are + named according to the language code (e.g., `en.json` for English, `es.json` for Spanish, etc.). - ```bash - git clone https://github.com/AppFlowy-IO/AppFlowy - ``` +- **Icons**: The icon set used in the Web project is the same as the one used in the Flutter project, ensuring visual + consistency. The icons are stored in the `frontend/resources/flowy_icons/` directory. -#### 🌐 Install the frontend dependencies: +Let's dive in and get the project up and running! 🚀 - ```bash - cd frontend/appflowy_web_app - pnpm install - ``` +## 🛠 Getting Started -#### 🖥️ Desktop Application (Tauri) (Optional) +### Prerequisites -> **Note**: if you want to run the web app in the browser, skip this step +Before you begin, make sure you have the following installed on your system: -- Follow the instructions [here](https://tauri.app/v1/guides/getting-started/prerequisites/) to install Tauri +- [Node.js](https://nodejs.org/) (v18.6.0) 🌳 +- [pnpm](https://pnpm.io/) (package manager) 📦 +- [Jest](https://jestjs.io/) (testing framework) 🃏 +- [Cypress](https://www.cypress.io/) (end-to-end testing) 🧪 -##### Windows and Linux Prerequisites +### Clone the Repository -###### Windows only +First, clone the repository to your local machine: -- Install the Duckscript CLI and vcpkg +```bash +git clone https://github.com/AppFlowy-IO/AppFlowy.git +cd frontend/appflowy_web_app +``` - ```bash - cargo install --force duckscript_cli - vcpkg integrate install - ``` +### Install Dependencies -###### Linux only +Install the required dependencies using pnpm: -- Install the required dependencies +```bash +## ensure you have pnpm installed, if not run the following command +# npm install -g pnpm@8.5.0 - ```bash - sudo apt-get update - sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf - ``` +pnpm install +``` -- **Get error**: failed to run custom build command for librocksdb-sys v6.11.4 +### Configure Environment Variables - ```bash - sudo apt install clang - ``` +Create a `.env` file in the root of the project and add the following environment variables: -##### Install Tauri Dependencies +```bash +AF_BASE_URL=http://localhost:8080 +AF_GOTRUE_URL=http://localhost:9999 +AF_WS_URL=ws://localhost:8080/ws/v1 +``` -- Install cargo-make +### Start the Development Server - ```bash - cargo install --force cargo-make - ``` +To start the development server, run the following command: +```bash +pnpm run dev +``` -- Install AppFlowy dev tools +### 🚀 Building for Production(Optional) - ```bash - # install development tools - # make sure you are in the root directory of the project - cd frontend - cargo make appflowy-tauri-deps-tools - ``` +if you want to run the production build, use the following commands -- Build the service/dependency +```bash +pnpm run build +pnpm run start +``` - ```bash - # make sure you are in the root directory of the project - cd frontend/appflowy_web_app - mkdir dist - cd src-tauri - cargo build - ``` +This will start the application in development mode. Open http://localhost:3000 to view it in the browser. -### 🚀 Running the Application +## 🧪 Running Tests -#### 🌐 Web Application +### Unit Tests -- Run the web application +We use **Jest** for running unit tests. To run the tests, use the following command: - ```bash - pnpm run dev - ``` -- Open your browser and navigate to `http://localhost:3000`, You can now interact with the AppFlowy web application +```bash +pnpm run test:unit +``` -#### 🖥️ Desktop Application (Tauri) +This will execute all the unit tests in the project and provide a summary of the results. ✅ -**Ensure close web application before running the desktop application** +### Components Tests -- Run the desktop application +We use **Cypress** for end-to-end testing. To run the Cypress tests, use the following command: - ```bash - pnpm run tauri:dev - ``` -- The AppFlowy desktop application will open, and you can interact with it +```bash +pnpm run cypress:open +``` -### 🛠️ Development +This will open the Cypress Test Runner where you can run your end-to-end tests. 🧪 -#### How to add or modify i18n keys - -- Modify the i18n files in `frontend/resources/translations/en.json` to add or modify i18n keys -- Run the following command to update the i18n keys in the application - - ```bash - pnpm run sync:i18n - ``` - -#### How to modify the theme - -Don't modify the theme file in `frontend/appflowy_web_app/src/styles/variables` directly) - -- Modify the theme file in `frontend/appflowy_web_app/style-dictionary/tokens/base.json( or dark.json or light.json)` to - add or modify theme keys -- Run the following command to update the theme in the application - - ```bash - pnpm run css:variables - ``` - -#### How to add or modify the environment variables - -- Modify the environment file in `frontend/appflowy_web_app/.env` to add or modify environment variables - -#### How to create symlink for the @appflowyinc/client-api-wasm in local development - -- Run the following command to create a symlink for the @appflowyinc/client-api-wasm - - ```bash - # ensure you are in the frontend/appflowy_web_app directory - - pnpm run link:client-api $source_path $target_path - - # Example - # pnpm run link:client-api ../../../AppFlowy-Cloud/libs/client-api-wasm/pkg ./node_modules/@appflowyinc/client-api-wasm - ``` - -### 📝 About the Project - -#### 📁 Directory Structure - -- `frontend/appflowy_web_app`: Contains the web application source code -- `frontend/appflowy_web_app/src`: Contains the app entry point and the source code -- `frontend/appflowy_web_app/src/components`: Contains the react components -- `frontend/appflowy_web_app/src/styles`: Contains the styles for the application -- `frontend/appflowy_web_app/src/utils`: Contains the utility functions -- `frontend/appflowy_web_app/src/i18n`: Contains the i18n files -- `frontend/appflowy_web_app/src/assets`: Contains the assets for the application -- `frontend/appflowy_web_app/src/store`: Contains the redux store -- `frontend/appflowy_web_app/src/@types`: Contains the typescript types -- `frontend/appflowy_web_app/src/applications/services`: Contains the services for the application. In vite.config.ts, - we have defined the alias for the services directory for different environments(Tauri/Web) - ```typescript - resolve: { - alias: [ - // ... - { - find: '$client-services', - replacement: !!process.env.TAURI_PLATFORM - ? `${__dirname}/src/application/services/tauri-services` - : `${__dirname}/src/application/services/js-services`, - }, - ] - } - ``` - -### 📦 Deployment - -Use the AppFlowy CI/CD pipeline to deploy the application to the test and production environments. - -- Push the changes to the main branch -- Deploy Test Environment - - Automatically, the test environment will be deployed if merged to the main branch or build/test branch -- Deploy Production Environment - - Navigate to the Actions tab - - Click on the workflow and select the Run workflow - - Enter the options - - Click on the Run workflow button - -#### 📦 Deployment (Self-Hosted EC2) - -##### Pre-requisites - -Please ensure you have learned about: - -- [Deploy Web application on AWS Cloud using EC2 Instance](https://www.youtube.com/watch?v=gWVIIU1ev0Y) -- [How to Install and Use Rsync Command](https://operavps.com/docs/install-rsync-command-in-linux/) -- [How to Use ssh-keygen to Generate a New SSH Key?](https://www.ssh.com/academy/ssh/keygen) -- [Linux post-installation steps for Docker Engine](https://docs.docker.com/engine/install/linux-postinstall/) -- [Configuring HTTPS servers](https://nginx.org/en/docs/http/configuring_https_servers.html) - -And then follow the steps below: - -1. Ensure you have the following installed on your server: - - Docker: [Install Docker](https://docs.docker.com/engine/install/) - - Rsync: [Install Rsync](https://operavps.com/docs/install-rsync-command-in-linux/) - -2. Create a new user for deploy, and generate an SSH key for the user - - ```bash - sudo adduser appflowy(or any name) - sudo su - appflowy - mkdir ~/.ssh - chmod 700 ~/.ssh - ssh-keygen -t rsa - chmod 600 ~/.ssh/authorized_keys - # add the user to the docker group, to run docker commands without sudo - sudo usermod -aG docker ${USER} - ``` - - visit the `~/.ssh/id_rsa` and `~/.ssh/id_rsa.pub` to get the private and public key respectively - - add the public key to the `~/.ssh/authorized_keys` file - - ensure the private key is kept safe - - exit and login back to the server with the new - user: `ssh -i your-existing-key.pem ec2-user@your-instance-public-dns` - -3. Clone the AppFlowy repository - -4. Set the following secrets in your - repository, have to - know [Using secrets in GitHub Actions](https://docs.github.com/en/actions/security-guides/using-secrets-in-github-actions) - -> Note: Test Environment: prefix the secret with `WEB_TEST_` and Production Environment: prefix the secret with `WEB_` - -> for example, `WEB_TEST_SSH_PRIVATE_KEY` and `WEB_SSH_PRIVATE_KEY` - -- `SSH_PRIVATE_KEY`: The private key generated in step 2: cat ~/.ssh/id_rsa -- `REMOTE_HOST`: The host of the server: `your-instance-public-dns` or `your-instance-ip` -- `REMOTE_USER`: The user created in step 2: `appflowy` -- `SSL_CERTIFICATE`: The SSL certificate for the - server - [Configuring HTTPS servers](https://nginx.org/en/docs/http/configuring_https_servers.html) -- `SSL_CERTIFICATE_KEY`: The SSL certificate key for the - server - [Configuring HTTPS servers](https://nginx.org/en/docs/http/configuring_https_servers.html) - -5. Run the deployment workflow to deploy the application(production or test environment) - -> Note: the test server will **automatically** deploy if merged to the main branch or build/test branch - -### 🧪 Testing - -> We use Cypress for end-to-end testing and component testing - [Cypress](https://www.cypress.io/) - -#### 🧪 End-to-End Testing - -> to be continued - -#### 🧪 Component Testing - -Run the following command to run the component tests +Alternatively, to run Cypress tests in the headless mode, use: ```bash pnpm run test:components ``` +Both commands will provide detailed test results and generate a code coverage report. + +## 🔄 Development Workflow + +### Linting + +To maintain code quality, we use **ESLint**. To run the linter and fix any linting errors, use the following command: + +```bash +pnpm run lint +``` + +## 🚀 Production Deployment + +Our production deployment process is automated using GitHub Actions. The process involves: + +1. **Setting up an AWS EC2 instance**: We use an EC2 instance to host the application. +2. **Installing Docker and Docker Compose**: Docker is installed on the AWS instance. +3. **Configuring SSH Access**: SSH access is set up with a user and password. +4. **Preparing Project Configuration**: We configure `Dockerfile`, `nginx.conf`, and `server.cjs` in the web project. +5. **Using GitHub Actions**: We use the easingthemes/ssh-deploy@main action to deploy the project to the remote server. + +The deployment steps include building the Docker image and running the Docker container with the necessary port +mappings: + +```bash +docker build -t appflowy-web-app . +docker rm -f appflowy-web-app || true +docker run -d -p 80:80 -p 443:443 --name appflowy-web-app appflowy-web-app +``` + +The Web server runs on Bun. For more details about Bun, please refer to the [Bun documentation](https://bun.sh/). diff --git a/frontend/appflowy_web_app/beta.env b/frontend/appflowy_web_app/beta.env deleted file mode 100644 index ab31b57db7..0000000000 --- a/frontend/appflowy_web_app/beta.env +++ /dev/null @@ -1,3 +0,0 @@ -AF_WS_URL=wss://beta.appflowy.cloud/ws/v1 -AF_BASE_URL=https://beta.appflowy.cloud -AF_GOTRUE_URL=https://beta.appflowy.cloud/gotrue \ No newline at end of file diff --git a/frontend/appflowy_web_app/cypress/fixtures/current_workspace.json b/frontend/appflowy_web_app/cypress/fixtures/current_workspace.json deleted file mode 100644 index cef4bad369..0000000000 --- a/frontend/appflowy_web_app/cypress/fixtures/current_workspace.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "id": "9eebea03-3ed5-4298-86b2-a7f77856d48b", - "name": "workspace", - "icon": "", - "owner": { - "id": 0, - "name": "system" - }, - "type": 0, - "workspaceDatabaseId": "375874be-7a4f-4b7c-8b89-1dc9a39838f4" -} \ No newline at end of file diff --git a/frontend/appflowy_web_app/cypress/fixtures/database/databases.json b/frontend/appflowy_web_app/cypress/fixtures/database/databases.json deleted file mode 100644 index 675bb0b42a..0000000000 --- a/frontend/appflowy_web_app/cypress/fixtures/database/databases.json +++ /dev/null @@ -1,80 +0,0 @@ -[ - { - "database_id": "037a985f-f369-4c4a-8011-620012850a68", - "created_at": "1713429700", - "views": [ - "48c52cf7-bf98-43fa-96ad-b31aade9b071" - ] - }, - { - "database_id": "daea6aee-9365-4703-a8e2-a2fa6a07b214", - "created_at": "1714449533", - "views": [ - "b6347acb-3174-4f0e-98e9-dcce07e5dbf7" - ] - }, - { - "database_id": "4c658817-20db-4f56-b7f9-0637a22dfeb6", - "created_at": "0", - "views": [ - "7d2148fc-cace-4452-9c5c-96e52e6bf8b5", - "e410747b-5f2f-45a0-b2f7-890ad3001355", - "2143e95d-5dcb-4e0f-bb2c-50944e6e019f", - "a5566e49-f156-4168-9b2d-17926c5da329", - "135615fa-66f7-4451-9b54-d7e99445fca4", - "b4e77203-5c8b-48df-bbc5-2e1143eb0e61", - "a6af311f-cbc8-42c2-b801-7115619c3776" - ] - }, - { - "database_id": "4c658817-20db-4f56-b7f9-0637a22dfeb6", - "created_at": "0", - "views": [ - "7d2148fc-cace-4452-9c5c-96e52e6bf8b5", - "e97877f5-c365-4025-9e6a-e590c4b19dbb", - "f0c59921-04ee-4971-995c-79b7fd8c00e2", - "7eb697cd-6a55-40bb-96ac-0d4a3bc924b2" - ] - }, - { - "database_id": "ee63da2b-aa2a-4d0b-aab0-59008635363a", - "created_at": "0", - "views": [ - "2c1ee95a-1b09-4a1f-8d5e-501bc4861a9d", - "91ea7c08-f6b3-4b81-aa1e-d3664686186f" - ] - }, - { - "database_id": "e788f014-d0d3-4dfe-81ef-aa1ebb4d6366", - "created_at": "0", - "views": [ - "1b0e322d-4909-4c63-914a-d034fc363097", - "350f425b-b671-4e2d-8182-5998a6e62924" - ] - }, - { - "database_id": "ad7dc45b-44b5-498f-bfa2-0f43bf05cc0d", - "created_at": "0", - "views": [ - "0ce13415-6cce-4497-94c6-475ad96c249e", - "e4c89421-12b2-4d02-863d-20949eec9271" - ] - }, - { - "database_id": "ce267d12-3b61-4ebb-bb03-d65272f5f817", - "created_at": "0", - "views": [ - "ee3ae8ce-959a-4df3-8734-40b535ff88e3", - "66a6f3bc-c78f-4f74-a09e-08d4717bf1fd", - "2bf50c03-f41f-4363-b5b1-101216a6c5cc" - ] - }, - { - "database_id": "87bc006e-c1eb-47fd-9ac6-e39b17956369", - "created_at": "0", - "views": [ - "7f233be4-1b4d-46b2-bcfc-f341b8d75267", - "a734a068-e73d-4b4b-853c-4daffea389c0" - ] - } -] \ No newline at end of file diff --git a/frontend/appflowy_web_app/cypress/fixtures/document/f56bdf0f-90c8-53fb-97d9-ad5860d2b7a0.json b/frontend/appflowy_web_app/cypress/fixtures/document/f56bdf0f-90c8-53fb-97d9-ad5860d2b7a0.json deleted file mode 100644 index 53c4b0f6df..0000000000 --- a/frontend/appflowy_web_app/cypress/fixtures/document/f56bdf0f-90c8-53fb-97d9-ad5860d2b7a0.json +++ /dev/null @@ -1 +0,0 @@ -{"data":{"state_vector":[6,248,208,217,159,7,16,226,250,246,177,3,17,211,142,141,147,5,8,196,148,203,38,26,230,150,209,139,14,193,6,167,238,246,72,86],"doc_state":[6,177,5,230,150,209,139,14,0,0,2,0,1,0,5,39,0,196,148,203,38,4,6,122,45,72,76,78,68,2,4,0,230,150,209,139,14,8,4,49,49,49,49,39,0,196,148,203,38,1,6,54,70,57,105,75,107,1,40,0,230,150,209,139,14,13,2,105,100,1,119,6,54,70,57,105,75,107,40,0,230,150,209,139,14,13,2,116,121,1,119,7,104,101,97,100,105,110,103,40,0,230,150,209,139,14,13,6,112,97,114,101,110,116,1,119,10,74,118,87,74,108,105,53,79,117,84,40,0,230,150,209,139,14,13,8,99,104,105,108,100,114,101,110,1,119,6,97,118,105,69,115,45,33,0,230,150,209,139,14,13,4,100,97,116,97,1,40,0,230,150,209,139,14,13,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,230,150,209,139,14,13,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,196,148,203,38,3,6,97,118,105,69,115,45,0,72,196,148,203,38,24,1,119,6,54,70,57,105,75,107,39,0,196,148,203,38,4,6,69,115,57,50,84,74,2,33,0,196,148,203,38,1,6,98,71,84,69,74,116,1,0,7,33,0,196,148,203,38,3,6,87,56,66,102,66,110,1,129,196,148,203,38,24,1,168,230,150,209,139,14,18,1,119,39,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,49,49,49,49,34,125,93,44,34,108,101,118,101,108,34,58,50,125,4,0,230,150,209,139,14,23,1,35,0,1,132,230,150,209,139,14,35,1,35,0,1,132,230,150,209,139,14,37,1,35,0,1,132,230,150,209,139,14,39,1,35,0,1,39,0,196,148,203,38,4,6,56,76,76,113,48,56,2,39,0,196,148,203,38,1,6,56,70,105,117,112,98,1,40,0,230,150,209,139,14,44,2,105,100,1,119,6,56,70,105,117,112,98,40,0,230,150,209,139,14,44,2,116,121,1,119,7,104,101,97,100,105,110,103,40,0,230,150,209,139,14,44,6,112,97,114,101,110,116,1,119,10,74,118,87,74,108,105,53,79,117,84,40,0,230,150,209,139,14,44,8,99,104,105,108,100,114,101,110,1,119,6,49,66,67,68,107,53,33,0,230,150,209,139,14,44,4,100,97,116,97,1,40,0,230,150,209,139,14,44,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,230,150,209,139,14,44,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,196,148,203,38,3,6,49,66,67,68,107,53,0,200,196,148,203,38,24,230,150,209,139,14,33,1,119,6,56,70,105,117,112,98,4,0,230,150,209,139,14,43,1,50,161,230,150,209,139,14,49,1,132,230,150,209,139,14,54,1,50,161,230,150,209,139,14,55,1,132,230,150,209,139,14,56,1,50,161,230,150,209,139,14,57,1,132,230,150,209,139,14,58,1,50,161,230,150,209,139,14,59,1,39,0,196,148,203,38,4,6,103,76,57,104,70,45,2,39,0,196,148,203,38,1,6,72,99,71,82,109,56,1,40,0,230,150,209,139,14,63,2,105,100,1,119,6,72,99,71,82,109,56,40,0,230,150,209,139,14,63,2,116,121,1,119,9,112,97,114,97,103,114,97,112,104,40,0,230,150,209,139,14,63,6,112,97,114,101,110,116,1,119,10,74,118,87,74,108,105,53,79,117,84,40,0,230,150,209,139,14,63,8,99,104,105,108,100,114,101,110,1,119,6,50,114,117,49,72,109,33,0,230,150,209,139,14,63,4,100,97,116,97,1,40,0,230,150,209,139,14,63,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,230,150,209,139,14,63,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,196,148,203,38,3,6,50,114,117,49,72,109,0,136,230,150,209,139,14,33,1,119,6,72,99,71,82,109,56,168,230,150,209,139,14,61,1,119,39,123,34,108,101,118,101,108,34,58,52,44,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,50,50,50,50,34,125,93,125,1,0,230,150,209,139,14,62,1,161,230,150,209,139,14,68,1,129,230,150,209,139,14,74,1,161,230,150,209,139,14,75,1,129,230,150,209,139,14,76,2,161,230,150,209,139,14,77,1,129,230,150,209,139,14,79,1,161,230,150,209,139,14,80,1,129,230,150,209,139,14,81,1,161,230,150,209,139,14,82,1,68,230,150,209,139,14,74,6,228,189,160,229,165,189,168,230,150,209,139,14,84,1,119,31,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,228,189,160,229,165,189,34,125,93,125,39,0,196,148,203,38,4,6,113,111,68,56,109,111,2,33,0,196,148,203,38,1,6,113,118,66,98,82,83,1,0,7,33,0,196,148,203,38,3,6,118,76,97,51,57,90,1,129,230,150,209,139,14,72,1,1,0,230,150,209,139,14,88,1,0,1,129,230,150,209,139,14,99,1,0,1,129,230,150,209,139,14,101,1,0,4,132,230,150,209,139,14,103,1,91,0,1,132,230,150,209,139,14,108,1,93,0,1,39,0,196,148,203,38,4,6,69,114,99,104,55,72,2,39,0,196,148,203,38,1,6,51,111,95,70,117,67,1,40,0,230,150,209,139,14,113,2,105,100,1,119,6,51,111,95,70,117,67,40,0,230,150,209,139,14,113,2,116,121,1,119,9,116,111,100,111,95,108,105,115,116,40,0,230,150,209,139,14,113,6,112,97,114,101,110,116,1,119,10,74,118,87,74,108,105,53,79,117,84,40,0,230,150,209,139,14,113,8,99,104,105,108,100,114,101,110,1,119,6,68,56,109,75,57,97,33,0,230,150,209,139,14,113,4,100,97,116,97,1,40,0,230,150,209,139,14,113,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,230,150,209,139,14,113,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,196,148,203,38,3,6,68,56,109,75,57,97,0,200,230,150,209,139,14,72,230,150,209,139,14,98,1,119,6,51,111,95,70,117,67,4,0,230,150,209,139,14,112,1,49,161,230,150,209,139,14,118,1,132,230,150,209,139,14,123,1,50,161,230,150,209,139,14,124,1,132,230,150,209,139,14,125,1,51,161,230,150,209,139,14,126,1,39,0,196,148,203,38,4,6,122,55,116,111,73,55,2,33,0,196,148,203,38,1,6,120,66,121,81,55,113,1,0,7,33,0,196,148,203,38,3,6,81,98,51,101,57,117,1,129,230,150,209,139,14,98,1,168,230,150,209,139,14,128,1,1,119,44,123,34,99,104,101,99,107,101,100,34,58,102,97,108,115,101,44,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,49,50,51,34,125,93,125,1,0,230,150,209,139,14,129,1,1,0,1,129,230,150,209,139,14,141,1,1,0,1,129,230,150,209,139,14,143,1,1,0,4,132,230,150,209,139,14,145,1,1,45,0,1,39,0,196,148,203,38,4,6,84,119,106,104,109,54,2,39,0,196,148,203,38,1,6,69,76,108,103,56,112,1,40,0,230,150,209,139,14,153,1,2,105,100,1,119,6,69,76,108,103,56,112,40,0,230,150,209,139,14,153,1,2,116,121,1,119,13,98,117,108,108,101,116,101,100,95,108,105,115,116,40,0,230,150,209,139,14,153,1,6,112,97,114,101,110,116,1,119,10,74,118,87,74,108,105,53,79,117,84,40,0,230,150,209,139,14,153,1,8,99,104,105,108,100,114,101,110,1,119,6,80,56,121,104,83,86,33,0,230,150,209,139,14,153,1,4,100,97,116,97,1,40,0,230,150,209,139,14,153,1,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,230,150,209,139,14,153,1,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,196,148,203,38,3,6,80,56,121,104,83,86,0,200,230,150,209,139,14,98,230,150,209,139,14,139,1,1,119,6,69,76,108,103,56,112,4,0,230,150,209,139,14,152,1,1,49,161,230,150,209,139,14,158,1,1,132,230,150,209,139,14,163,1,1,50,161,230,150,209,139,14,164,1,1,132,230,150,209,139,14,165,1,1,51,168,230,150,209,139,14,166,1,1,119,28,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,49,50,51,34,125,93,125,39,0,196,148,203,38,4,6,100,53,73,101,79,51,2,33,0,196,148,203,38,1,6,79,100,120,77,110,80,1,0,7,33,0,196,148,203,38,3,6,120,89,113,72,122,98,1,129,230,150,209,139,14,139,1,1,4,0,230,150,209,139,14,169,1,1,49,0,1,132,230,150,209,139,14,180,1,1,46,0,1,39,0,196,148,203,38,4,6,50,78,117,49,104,78,2,39,0,196,148,203,38,1,6,88,110,106,101,115,65,1,40,0,230,150,209,139,14,185,1,2,105,100,1,119,6,88,110,106,101,115,65,40,0,230,150,209,139,14,185,1,2,116,121,1,119,13,110,117,109,98,101,114,101,100,95,108,105,115,116,40,0,230,150,209,139,14,185,1,6,112,97,114,101,110,116,1,119,10,74,118,87,74,108,105,53,79,117,84,40,0,230,150,209,139,14,185,1,8,99,104,105,108,100,114,101,110,1,119,6,102,119,53,69,102,68,33,0,230,150,209,139,14,185,1,4,100,97,116,97,1,40,0,230,150,209,139,14,185,1,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,230,150,209,139,14,185,1,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,196,148,203,38,3,6,102,119,53,69,102,68,0,200,230,150,209,139,14,139,1,230,150,209,139,14,179,1,1,119,6,88,110,106,101,115,65,4,0,230,150,209,139,14,184,1,1,49,161,230,150,209,139,14,190,1,1,132,230,150,209,139,14,195,1,1,50,161,230,150,209,139,14,196,1,1,132,230,150,209,139,14,197,1,1,51,161,230,150,209,139,14,198,1,1,39,0,196,148,203,38,4,6,107,79,86,116,87,117,2,39,0,196,148,203,38,1,6,107,106,98,87,90,118,1,40,0,230,150,209,139,14,202,1,2,105,100,1,119,6,107,106,98,87,90,118,40,0,230,150,209,139,14,202,1,2,116,121,1,119,13,110,117,109,98,101,114,101,100,95,108,105,115,116,40,0,230,150,209,139,14,202,1,6,112,97,114,101,110,116,1,119,10,74,118,87,74,108,105,53,79,117,84,40,0,230,150,209,139,14,202,1,8,99,104,105,108,100,114,101,110,1,119,6,56,115,49,108,106,52,33,0,230,150,209,139,14,202,1,4,100,97,116,97,1,40,0,230,150,209,139,14,202,1,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,230,150,209,139,14,202,1,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,196,148,203,38,3,6,56,115,49,108,106,52,0,136,230,150,209,139,14,179,1,1,119,6,107,106,98,87,90,118,168,230,150,209,139,14,200,1,1,119,39,123,34,110,117,109,98,101,114,34,58,49,44,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,49,50,51,34,125,93,125,4,0,230,150,209,139,14,201,1,1,51,161,230,150,209,139,14,207,1,1,132,230,150,209,139,14,213,1,1,50,161,230,150,209,139,14,214,1,1,132,230,150,209,139,14,215,1,1,49,168,230,150,209,139,14,216,1,1,119,28,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,51,50,49,34,125,93,125,39,0,196,148,203,38,4,6,101,56,113,73,98,66,2,33,0,196,148,203,38,1,6,120,83,99,121,67,100,1,0,7,33,0,196,148,203,38,3,6,102,88,53,51,101,80,1,129,230,150,209,139,14,211,1,1,39,0,196,148,203,38,4,6,57,87,73,111,101,83,2,39,0,196,148,203,38,1,6,48,77,97,84,52,54,1,40,0,230,150,209,139,14,231,1,2,105,100,1,119,6,48,77,97,84,52,54,40,0,230,150,209,139,14,231,1,2,116,121,1,119,13,110,117,109,98,101,114,101,100,95,108,105,115,116,40,0,230,150,209,139,14,231,1,6,112,97,114,101,110,116,1,119,6,107,106,98,87,90,118,40,0,230,150,209,139,14,231,1,8,99,104,105,108,100,114,101,110,1,119,6,68,70,72,68,101,106,33,0,230,150,209,139,14,231,1,4,100,97,116,97,1,40,0,230,150,209,139,14,231,1,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,230,150,209,139,14,231,1,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,196,148,203,38,3,6,68,70,72,68,101,106,0,8,0,230,150,209,139,14,210,1,1,119,6,48,77,97,84,52,54,4,0,230,150,209,139,14,230,1,1,51,161,230,150,209,139,14,236,1,1,132,230,150,209,139,14,241,1,1,50,161,230,150,209,139,14,242,1,1,132,230,150,209,139,14,243,1,1,49,168,230,150,209,139,14,244,1,1,119,28,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,51,50,49,34,125,93,125,39,0,196,148,203,38,4,6,114,104,72,106,57,65,2,33,0,196,148,203,38,1,6,84,45,109,108,72,57,1,0,7,33,0,196,148,203,38,3,6,118,110,77,57,77,84,1,129,230,150,209,139,14,240,1,1,39,0,196,148,203,38,4,6,49,90,55,100,72,105,2,39,0,196,148,203,38,1,6,72,102,88,86,103,83,1,40,0,230,150,209,139,14,131,2,2,105,100,1,119,6,72,102,88,86,103,83,40,0,230,150,209,139,14,131,2,2,116,121,1,119,13,110,117,109,98,101,114,101,100,95,108,105,115,116,40,0,230,150,209,139,14,131,2,6,112,97,114,101,110,116,1,119,6,48,77,97,84,52,54,40,0,230,150,209,139,14,131,2,8,99,104,105,108,100,114,101,110,1,119,6,57,122,75,84,78,57,33,0,230,150,209,139,14,131,2,4,100,97,116,97,1,40,0,230,150,209,139,14,131,2,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,230,150,209,139,14,131,2,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,196,148,203,38,3,6,57,122,75,84,78,57,0,8,0,230,150,209,139,14,239,1,1,119,6,72,102,88,86,103,83,4,0,230,150,209,139,14,130,2,1,51,161,230,150,209,139,14,136,2,1,132,230,150,209,139,14,141,2,1,50,161,230,150,209,139,14,142,2,1,132,230,150,209,139,14,143,2,1,49,168,230,150,209,139,14,144,2,1,119,28,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,51,50,49,34,125,93,125,39,0,196,148,203,38,4,6,65,98,83,76,67,56,2,33,0,196,148,203,38,1,6,97,86,52,120,55,81,1,0,7,33,0,196,148,203,38,3,6,69,87,113,113,77,67,1,129,230,150,209,139,14,140,2,1,39,0,196,148,203,38,4,6,80,82,49,65,99,76,2,33,0,196,148,203,38,1,6,115,66,104,71,113,108,1,0,7,33,0,196,148,203,38,3,6,114,86,95,54,88,121,1,129,230,150,209,139,14,129,2,1,1,0,230,150,209,139,14,158,2,1,0,2,39,0,196,148,203,38,4,6,51,54,82,71,101,79,2,33,0,196,148,203,38,1,6,81,99,88,112,117,56,1,0,7,33,0,196,148,203,38,3,6,106,53,72,95,112,48,1,193,230,150,209,139,14,129,2,230,150,209,139,14,168,2,1,39,0,196,148,203,38,4,6,101,120,109,55,104,73,2,33,0,196,148,203,38,1,6,52,70,81,69,87,98,1,0,7,33,0,196,148,203,38,3,6,79,82,107,84,68,115,1,129,230,150,209,139,14,229,1,1,4,0,230,150,209,139,14,183,2,1,34,0,1,39,0,196,148,203,38,4,6,52,48,110,100,113,55,2,39,0,196,148,203,38,1,6,100,53,57,122,110,74,1,40,0,230,150,209,139,14,197,2,2,105,100,1,119,6,100,53,57,122,110,74,40,0,230,150,209,139,14,197,2,2,116,121,1,119,5,113,117,111,116,101,40,0,230,150,209,139,14,197,2,6,112,97,114,101,110,116,1,119,10,74,118,87,74,108,105,53,79,117,84,40,0,230,150,209,139,14,197,2,8,99,104,105,108,100,114,101,110,1,119,6,81,83,101,49,78,65,33,0,230,150,209,139,14,197,2,4,100,97,116,97,1,40,0,230,150,209,139,14,197,2,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,230,150,209,139,14,197,2,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,196,148,203,38,3,6,81,83,101,49,78,65,0,200,230,150,209,139,14,229,1,230,150,209,139,14,193,2,1,119,6,100,53,57,122,110,74,4,0,230,150,209,139,14,196,2,1,49,161,230,150,209,139,14,202,2,1,132,230,150,209,139,14,207,2,1,50,161,230,150,209,139,14,208,2,1,132,230,150,209,139,14,209,2,1,51,168,230,150,209,139,14,210,2,1,119,28,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,49,50,51,34,125,93,125,39,0,196,148,203,38,4,6,98,84,120,48,103,106,2,33,0,196,148,203,38,1,6,99,72,87,116,90,66,1,0,7,33,0,196,148,203,38,3,6,54,67,53,67,68,81,1,129,230,150,209,139,14,193,2,1,4,0,230,150,209,139,14,213,2,1,62,0,1,39,0,196,148,203,38,4,6,48,95,102,53,81,87,2,39,0,196,148,203,38,1,6,115,106,51,120,82,67,1,40,0,230,150,209,139,14,227,2,2,105,100,1,119,6,115,106,51,120,82,67,40,0,230,150,209,139,14,227,2,2,116,121,1,119,11,116,111,103,103,108,101,95,108,105,115,116,40,0,230,150,209,139,14,227,2,6,112,97,114,101,110,116,1,119,10,74,118,87,74,108,105,53,79,117,84,40,0,230,150,209,139,14,227,2,8,99,104,105,108,100,114,101,110,1,119,6,106,77,71,51,69,107,33,0,230,150,209,139,14,227,2,4,100,97,116,97,1,40,0,230,150,209,139,14,227,2,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,230,150,209,139,14,227,2,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,196,148,203,38,3,6,106,77,71,51,69,107,0,200,230,150,209,139,14,193,2,230,150,209,139,14,223,2,1,119,6,115,106,51,120,82,67,4,0,230,150,209,139,14,226,2,1,49,161,230,150,209,139,14,232,2,1,132,230,150,209,139,14,237,2,1,50,161,230,150,209,139,14,238,2,1,132,230,150,209,139,14,239,2,1,51,161,230,150,209,139,14,240,2,1,132,230,150,209,139,14,241,2,1,51,161,230,150,209,139,14,242,2,1,39,0,196,148,203,38,4,6,75,108,79,113,102,103,2,39,0,196,148,203,38,1,6,103,102,97,107,74,56,1,40,0,230,150,209,139,14,246,2,2,105,100,1,119,6,103,102,97,107,74,56,40,0,230,150,209,139,14,246,2,2,116,121,1,119,9,112,97,114,97,103,114,97,112,104,40,0,230,150,209,139,14,246,2,6,112,97,114,101,110,116,1,119,6,115,106,51,120,82,67,40,0,230,150,209,139,14,246,2,8,99,104,105,108,100,114,101,110,1,119,6,104,112,106,70,101,78,33,0,230,150,209,139,14,246,2,4,100,97,116,97,1,40,0,230,150,209,139,14,246,2,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,230,150,209,139,14,246,2,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,196,148,203,38,3,6,104,112,106,70,101,78,0,8,0,230,150,209,139,14,235,2,1,119,6,103,102,97,107,74,56,168,230,150,209,139,14,244,2,1,119,47,123,34,99,111,108,108,97,112,115,101,100,34,58,102,97,108,115,101,44,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,49,50,51,51,34,125,93,125,4,0,230,150,209,139,14,245,2,1,49,161,230,150,209,139,14,251,2,1,132,230,150,209,139,14,129,3,1,50,161,230,150,209,139,14,130,3,1,132,230,150,209,139,14,131,3,1,51,161,230,150,209,139,14,132,3,1,132,230,150,209,139,14,133,3,1,51,168,230,150,209,139,14,134,3,1,119,29,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,49,50,51,51,34,125,93,125,39,0,196,148,203,38,4,6,112,112,49,122,122,76,2,33,0,196,148,203,38,1,6,49,112,69,74,83,110,1,0,7,33,0,196,148,203,38,3,6,55,53,120,75,111,69,1,129,230,150,209,139,14,255,2,1,39,0,196,148,203,38,4,6,69,89,89,101,73,97,2,39,0,196,148,203,38,1,6,53,73,97,85,112,119,1,40,0,230,150,209,139,14,149,3,2,105,100,1,119,6,53,73,97,85,112,119,40,0,230,150,209,139,14,149,3,2,116,121,1,119,9,112,97,114,97,103,114,97,112,104,40,0,230,150,209,139,14,149,3,6,112,97,114,101,110,116,1,119,6,103,102,97,107,74,56,40,0,230,150,209,139,14,149,3,8,99,104,105,108,100,114,101,110,1,119,6,97,118,100,122,99,120,33,0,230,150,209,139,14,149,3,4,100,97,116,97,1,40,0,230,150,209,139,14,149,3,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,230,150,209,139,14,149,3,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,196,148,203,38,3,6,97,118,100,122,99,120,0,8,0,230,150,209,139,14,254,2,1,119,6,53,73,97,85,112,119,4,0,230,150,209,139,14,148,3,1,51,161,230,150,209,139,14,154,3,1,132,230,150,209,139,14,159,3,1,50,161,230,150,209,139,14,160,3,1,132,230,150,209,139,14,161,3,1,49,168,230,150,209,139,14,162,3,1,119,28,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,51,50,49,34,125,93,125,39,0,196,148,203,38,4,6,45,71,114,111,55,97,2,33,0,196,148,203,38,1,6,86,52,66,50,115,90,1,0,7,33,0,196,148,203,38,3,6,77,54,103,66,71,77,1,129,230,150,209,139,14,158,3,1,39,0,196,148,203,38,4,6,65,71,102,55,65,110,2,33,0,196,148,203,38,1,6,74,87,105,89,112,50,1,0,7,33,0,196,148,203,38,3,6,49,86,56,87,104,113,1,129,230,150,209,139,14,175,3,1,39,0,196,148,203,38,4,6,88,116,53,71,97,50,2,33,0,196,148,203,38,1,6,115,76,114,115,90,118,1,0,7,33,0,196,148,203,38,3,6,97,113,55,117,111,77,1,129,230,150,209,139,14,147,3,1,39,0,196,148,203,38,4,6,87,109,86,51,97,97,2,33,0,196,148,203,38,1,6,48,85,79,89,119,97,1,0,7,33,0,196,148,203,38,3,6,52,119,71,102,45,122,1,129,230,150,209,139,14,223,2,1,39,0,196,148,203,38,4,6,78,116,76,65,67,112,2,33,0,196,148,203,38,1,6,68,65,117,54,49,114,1,0,7,33,0,196,148,203,38,3,6,81,112,115,98,120,66,1,129,230,150,209,139,14,197,3,1,39,0,196,148,203,38,4,6,85,75,78,70,53,53,2,33,0,196,148,203,38,1,6,80,111,100,107,78,98,1,0,7,33,0,196,148,203,38,3,6,113,66,65,113,81,87,1,129,230,150,209,139,14,208,3,1,39,0,196,148,203,38,1,6,70,45,78,106,49,118,1,40,0,230,150,209,139,14,231,3,2,105,100,1,119,6,70,45,78,106,49,118,40,0,230,150,209,139,14,231,3,2,116,121,1,119,5,105,109,97,103,101,40,0,230,150,209,139,14,231,3,6,112,97,114,101,110,116,1,119,10,74,118,87,74,108,105,53,79,117,84,40,0,230,150,209,139,14,231,3,8,99,104,105,108,100,114,101,110,1,119,6,65,121,108,69,98,88,33,0,230,150,209,139,14,231,3,4,100,97,116,97,1,40,0,230,150,209,139,14,231,3,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,230,150,209,139,14,231,3,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,196,148,203,38,3,6,65,121,108,69,98,88,0,200,230,150,209,139,14,208,3,230,150,209,139,14,230,3,1,119,6,70,45,78,106,49,118,39,0,196,148,203,38,4,6,54,113,56,86,101,76,2,33,0,196,148,203,38,1,6,82,100,68,97,117,119,1,0,7,33,0,196,148,203,38,3,6,111,117,122,65,90,71,1,129,230,150,209,139,14,230,3,1,168,230,150,209,139,14,236,3,1,119,212,1,123,34,97,108,105,103,110,34,58,34,99,101,110,116,101,114,34,44,34,117,114,108,34,58,34,104,116,116,112,115,58,47,47,105,109,97,103,101,115,46,117,110,115,112,108,97,115,104,46,99,111,109,47,112,104,111,116,111,45,49,55,49,53,51,57,48,51,50,49,50,49,51,45,99,56,100,56,56,98,51,101,48,50,52,97,63,99,114,111,112,61,101,110,116,114,111,112,121,38,99,115,61,116,105,110,121,115,114,103,98,38,102,105,116,61,109,97,120,38,102,109,61,106,112,103,38,105,120,105,100,61,77,51,119,49,77,84,69,49,77,122,100,56,77,72,119,120,102,72,74,104,98,109,82,118,98,88,120,56,102,72,120,56,102,72,120,56,102,68,69,51,77,84,89,48,78,68,99,49,78,122,100,56,38,105,120,108,105,98,61,114,98,45,52,46,48,46,51,38,113,61,56,48,38,119,61,49,48,56,48,34,125,39,0,196,148,203,38,4,6,85,57,76,71,106,107,2,39,0,196,148,203,38,4,6,114,87,117,84,54,65,2,39,0,196,148,203,38,4,6,66,106,53,104,119,48,2,39,0,196,148,203,38,4,6,111,88,109,81,73,90,2,33,0,196,148,203,38,1,6,116,56,115,72,75,74,1,0,7,33,0,196,148,203,38,3,6,52,113,115,55,118,84,1,65,230,150,209,139,14,22,1,39,0,196,148,203,38,4,6,52,66,101,119,114,104,2,39,0,196,148,203,38,1,6,45,45,112,51,53,121,1,40,0,230,150,209,139,14,140,4,2,105,100,1,119,6,45,45,112,51,53,121,40,0,230,150,209,139,14,140,4,2,116,121,1,119,5,116,97,98,108,101,40,0,230,150,209,139,14,140,4,6,112,97,114,101,110,116,1,119,10,74,118,87,74,108,105,53,79,117,84,40,0,230,150,209,139,14,140,4,8,99,104,105,108,100,114,101,110,1,119,6,82,73,74,99,48,98,33,0,230,150,209,139,14,140,4,4,100,97,116,97,1,40,0,230,150,209,139,14,140,4,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,230,150,209,139,14,140,4,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,196,148,203,38,3,6,82,73,74,99,48,98,0,200,230,150,209,139,14,230,3,230,150,209,139,14,251,3,1,119,6,45,45,112,51,53,121,39,0,196,148,203,38,1,6,78,95,121,106,84,95,1,40,0,230,150,209,139,14,150,4,2,105,100,1,119,6,78,95,121,106,84,95,40,0,230,150,209,139,14,150,4,2,116,121,1,119,10,116,97,98,108,101,47,99,101,108,108,40,0,230,150,209,139,14,150,4,6,112,97,114,101,110,116,1,119,6,45,45,112,51,53,121,40,0,230,150,209,139,14,150,4,8,99,104,105,108,100,114,101,110,1,119,6,73,76,68,100,48,55,33,0,230,150,209,139,14,150,4,4,100,97,116,97,1,40,0,230,150,209,139,14,150,4,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,230,150,209,139,14,150,4,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,196,148,203,38,3,6,73,76,68,100,48,55,0,8,0,230,150,209,139,14,148,4,1,119,6,78,95,121,106,84,95,39,0,196,148,203,38,1,6,80,80,120,80,86,55,1,40,0,230,150,209,139,14,160,4,2,105,100,1,119,6,80,80,120,80,86,55,40,0,230,150,209,139,14,160,4,2,116,121,1,119,9,112,97,114,97,103,114,97,112,104,40,0,230,150,209,139,14,160,4,6,112,97,114,101,110,116,1,119,6,78,95,121,106,84,95,40,0,230,150,209,139,14,160,4,8,99,104,105,108,100,114,101,110,1,119,6,67,82,84,77,83,108,33,0,230,150,209,139,14,160,4,4,100,97,116,97,1,40,0,230,150,209,139,14,160,4,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,230,150,209,139,14,160,4,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,196,148,203,38,3,6,67,82,84,77,83,108,0,8,0,230,150,209,139,14,158,4,1,119,6,80,80,120,80,86,55,39,0,196,148,203,38,1,6,111,57,69,90,109,75,1,40,0,230,150,209,139,14,170,4,2,105,100,1,119,6,111,57,69,90,109,75,40,0,230,150,209,139,14,170,4,2,116,121,1,119,10,116,97,98,108,101,47,99,101,108,108,40,0,230,150,209,139,14,170,4,6,112,97,114,101,110,116,1,119,6,45,45,112,51,53,121,40,0,230,150,209,139,14,170,4,8,99,104,105,108,100,114,101,110,1,119,6,118,97,67,119,70,45,33,0,230,150,209,139,14,170,4,4,100,97,116,97,1,40,0,230,150,209,139,14,170,4,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,230,150,209,139,14,170,4,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,196,148,203,38,3,6,118,97,67,119,70,45,0,136,230,150,209,139,14,159,4,1,119,6,111,57,69,90,109,75,39,0,196,148,203,38,1,6,95,114,49,111,86,55,1,40,0,230,150,209,139,14,180,4,2,105,100,1,119,6,95,114,49,111,86,55,40,0,230,150,209,139,14,180,4,2,116,121,1,119,9,112,97,114,97,103,114,97,112,104,40,0,230,150,209,139,14,180,4,6,112,97,114,101,110,116,1,119,6,111,57,69,90,109,75,40,0,230,150,209,139,14,180,4,8,99,104,105,108,100,114,101,110,1,119,6,97,80,74,89,49,98,33,0,230,150,209,139,14,180,4,4,100,97,116,97,1,40,0,230,150,209,139,14,180,4,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,230,150,209,139,14,180,4,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,196,148,203,38,3,6,97,80,74,89,49,98,0,8,0,230,150,209,139,14,178,4,1,119,6,95,114,49,111,86,55,39,0,196,148,203,38,1,6,120,118,83,84,69,100,1,40,0,230,150,209,139,14,190,4,2,105,100,1,119,6,120,118,83,84,69,100,40,0,230,150,209,139,14,190,4,2,116,121,1,119,10,116,97,98,108,101,47,99,101,108,108,40,0,230,150,209,139,14,190,4,6,112,97,114,101,110,116,1,119,6,45,45,112,51,53,121,40,0,230,150,209,139,14,190,4,8,99,104,105,108,100,114,101,110,1,119,6,106,68,100,121,83,101,33,0,230,150,209,139,14,190,4,4,100,97,116,97,1,40,0,230,150,209,139,14,190,4,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,230,150,209,139,14,190,4,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,196,148,203,38,3,6,106,68,100,121,83,101,0,136,230,150,209,139,14,179,4,1,119,6,120,118,83,84,69,100,39,0,196,148,203,38,1,6,116,69,111,81,110,97,1,40,0,230,150,209,139,14,200,4,2,105,100,1,119,6,116,69,111,81,110,97,40,0,230,150,209,139,14,200,4,2,116,121,1,119,9,112,97,114,97,103,114,97,112,104,40,0,230,150,209,139,14,200,4,6,112,97,114,101,110,116,1,119,6,120,118,83,84,69,100,40,0,230,150,209,139,14,200,4,8,99,104,105,108,100,114,101,110,1,119,6,103,106,77,67,117,55,33,0,230,150,209,139,14,200,4,4,100,97,116,97,1,40,0,230,150,209,139,14,200,4,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,230,150,209,139,14,200,4,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,196,148,203,38,3,6,103,106,77,67,117,55,0,8,0,230,150,209,139,14,198,4,1,119,6,116,69,111,81,110,97,39,0,196,148,203,38,1,6,76,112,106,89,74,119,1,40,0,230,150,209,139,14,210,4,2,105,100,1,119,6,76,112,106,89,74,119,40,0,230,150,209,139,14,210,4,2,116,121,1,119,10,116,97,98,108,101,47,99,101,108,108,40,0,230,150,209,139,14,210,4,6,112,97,114,101,110,116,1,119,6,45,45,112,51,53,121,40,0,230,150,209,139,14,210,4,8,99,104,105,108,100,114,101,110,1,119,6,113,68,56,99,76,101,33,0,230,150,209,139,14,210,4,4,100,97,116,97,1,40,0,230,150,209,139,14,210,4,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,230,150,209,139,14,210,4,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,196,148,203,38,3,6,113,68,56,99,76,101,0,136,230,150,209,139,14,199,4,1,119,6,76,112,106,89,74,119,39,0,196,148,203,38,1,6,83,50,85,85,71,53,1,40,0,230,150,209,139,14,220,4,2,105,100,1,119,6,83,50,85,85,71,53,40,0,230,150,209,139,14,220,4,2,116,121,1,119,9,112,97,114,97,103,114,97,112,104,40,0,230,150,209,139,14,220,4,6,112,97,114,101,110,116,1,119,6,76,112,106,89,74,119,40,0,230,150,209,139,14,220,4,8,99,104,105,108,100,114,101,110,1,119,6,57,51,116,87,52,111,33,0,230,150,209,139,14,220,4,4,100,97,116,97,1,40,0,230,150,209,139,14,220,4,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,230,150,209,139,14,220,4,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,196,148,203,38,3,6,57,51,116,87,52,111,0,8,0,230,150,209,139,14,218,4,1,119,6,83,50,85,85,71,53,4,0,230,150,209,139,14,253,3,1,53,161,230,150,209,139,14,165,4,1,132,230,150,209,139,14,230,4,1,53,161,230,150,209,139,14,231,4,1,132,230,150,209,139,14,232,4,1,53,168,230,150,209,139,14,233,4,1,119,28,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,53,53,53,34,125,93,125,39,0,196,148,203,38,4,6,67,99,84,107,50,83,2,39,0,196,148,203,38,4,6,55,75,51,73,100,102,2,161,230,150,209,139,14,195,4,1,161,230,150,209,139,14,215,4,1,39,0,196,148,203,38,1,6,84,82,74,113,85,51,1,40,0,230,150,209,139,14,240,4,2,105,100,1,119,6,84,82,74,113,85,51,40,0,230,150,209,139,14,240,4,2,116,121,1,119,10,116,97,98,108,101,47,99,101,108,108,40,0,230,150,209,139,14,240,4,6,112,97,114,101,110,116,1,119,6,45,45,112,51,53,121,40,0,230,150,209,139,14,240,4,8,99,104,105,108,100,114,101,110,1,119,6,112,97,86,50,73,50,33,0,230,150,209,139,14,240,4,4,100,97,116,97,1,40,0,230,150,209,139,14,240,4,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,230,150,209,139,14,240,4,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,196,148,203,38,3,6,112,97,86,50,73,50,0,200,230,150,209,139,14,179,4,230,150,209,139,14,199,4,1,119,6,84,82,74,113,85,51,39,0,196,148,203,38,1,6,69,52,67,51,90,84,1,40,0,230,150,209,139,14,250,4,2,105,100,1,119,6,69,52,67,51,90,84,40,0,230,150,209,139,14,250,4,2,116,121,1,119,9,112,97,114,97,103,114,97,112,104,40,0,230,150,209,139,14,250,4,6,112,97,114,101,110,116,1,119,6,84,82,74,113,85,51,40,0,230,150,209,139,14,250,4,8,99,104,105,108,100,114,101,110,1,119,6,110,54,120,99,83,88,33,0,230,150,209,139,14,250,4,4,100,97,116,97,1,40,0,230,150,209,139,14,250,4,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,230,150,209,139,14,250,4,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,196,148,203,38,3,6,110,54,120,99,83,88,0,8,0,230,150,209,139,14,248,4,1,119,6,69,52,67,51,90,84,39,0,196,148,203,38,1,6,100,100,76,78,115,71,1,40,0,230,150,209,139,14,132,5,2,105,100,1,119,6,100,100,76,78,115,71,40,0,230,150,209,139,14,132,5,2,116,121,1,119,10,116,97,98,108,101,47,99,101,108,108,40,0,230,150,209,139,14,132,5,6,112,97,114,101,110,116,1,119,6,45,45,112,51,53,121,40,0,230,150,209,139,14,132,5,8,99,104,105,108,100,114,101,110,1,119,6,77,72,104,98,74,67,33,0,230,150,209,139,14,132,5,4,100,97,116,97,1,40,0,230,150,209,139,14,132,5,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,230,150,209,139,14,132,5,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,196,148,203,38,3,6,77,72,104,98,74,67,0,200,230,150,209,139,14,249,4,230,150,209,139,14,199,4,1,119,6,100,100,76,78,115,71,39,0,196,148,203,38,1,6,84,100,49,45,100,117,1,40,0,230,150,209,139,14,142,5,2,105,100,1,119,6,84,100,49,45,100,117,40,0,230,150,209,139,14,142,5,2,116,121,1,119,9,112,97,114,97,103,114,97,112,104,40,0,230,150,209,139,14,142,5,6,112,97,114,101,110,116,1,119,6,100,100,76,78,115,71,40,0,230,150,209,139,14,142,5,8,99,104,105,108,100,114,101,110,1,119,6,98,90,70,75,49,119,33,0,230,150,209,139,14,142,5,4,100,97,116,97,1,40,0,230,150,209,139,14,142,5,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,230,150,209,139,14,142,5,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,196,148,203,38,3,6,98,90,70,75,49,119,0,8,0,230,150,209,139,14,140,5,1,119,6,84,100,49,45,100,117,161,230,150,209,139,14,145,4,1,4,0,230,150,209,139,14,236,4,1,57,161,230,150,209,139,14,255,4,1,132,230,150,209,139,14,153,5,1,57,161,230,150,209,139,14,154,5,1,132,230,150,209,139,14,155,5,1,57,168,230,150,209,139,14,156,5,1,119,28,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,57,57,57,34,125,93,125,4,0,230,150,209,139,14,128,4,1,51,161,230,150,209,139,14,205,4,1,132,230,150,209,139,14,159,5,1,51,161,230,150,209,139,14,160,5,1,132,230,150,209,139,14,161,5,1,51,168,230,150,209,139,14,162,5,1,119,28,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,51,51,51,34,125,93,125,4,0,230,150,209,139,14,237,4,1,56,161,230,150,209,139,14,147,5,1,132,230,150,209,139,14,165,5,1,53,168,230,150,209,139,14,166,5,1,119,27,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,56,53,34,125,93,125,4,0,230,150,209,139,14,254,3,1,49,161,230,150,209,139,14,185,4,1,132,230,150,209,139,14,169,5,1,50,168,230,150,209,139,14,170,5,1,119,27,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,49,50,34,125,93,125,4,0,230,150,209,139,14,139,4,1,52,161,230,150,209,139,14,225,4,1,132,230,150,209,139,14,173,5,1,54,168,230,150,209,139,14,174,5,1,119,27,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,52,54,34,125,93,125,39,0,196,148,203,38,4,6,81,110,48,86,56,77,2,39,0,196,148,203,38,4,6,95,88,68,53,76,66,2,39,0,196,148,203,38,4,6,55,111,66,57,53,51,2,161,230,150,209,139,14,152,5,3,39,0,196,148,203,38,1,6,69,87,51,110,90,107,1,40,0,230,150,209,139,14,183,5,2,105,100,1,119,6,69,87,51,110,90,107,40,0,230,150,209,139,14,183,5,2,116,121,1,119,10,116,97,98,108,101,47,99,101,108,108,40,0,230,150,209,139,14,183,5,6,112,97,114,101,110,116,1,119,6,45,45,112,51,53,121,40,0,230,150,209,139,14,183,5,8,99,104,105,108,100,114,101,110,1,119,6,99,119,105,70,65,119,33,0,230,150,209,139,14,183,5,4,100,97,116,97,1,40,0,230,150,209,139,14,183,5,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,230,150,209,139,14,183,5,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,196,148,203,38,3,6,99,119,105,70,65,119,0,200,230,150,209,139,14,179,4,230,150,209,139,14,249,4,1,119,6,69,87,51,110,90,107,39,0,196,148,203,38,1,6,77,109,56,115,79,50,1,40,0,230,150,209,139,14,193,5,2,105,100,1,119,6,77,109,56,115,79,50,40,0,230,150,209,139,14,193,5,2,116,121,1,119,9,112,97,114,97,103,114,97,112,104,40,0,230,150,209,139,14,193,5,6,112,97,114,101,110,116,1,119,6,69,87,51,110,90,107,40,0,230,150,209,139,14,193,5,8,99,104,105,108,100,114,101,110,1,119,6,101,70,65,80,56,102,33,0,230,150,209,139,14,193,5,4,100,97,116,97,1,40,0,230,150,209,139,14,193,5,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,230,150,209,139,14,193,5,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,196,148,203,38,3,6,101,70,65,80,56,102,0,8,0,230,150,209,139,14,191,5,1,119,6,77,109,56,115,79,50,39,0,196,148,203,38,1,6,87,78,105,110,55,95,1,40,0,230,150,209,139,14,203,5,2,105,100,1,119,6,87,78,105,110,55,95,40,0,230,150,209,139,14,203,5,2,116,121,1,119,10,116,97,98,108,101,47,99,101,108,108,40,0,230,150,209,139,14,203,5,6,112,97,114,101,110,116,1,119,6,45,45,112,51,53,121,40,0,230,150,209,139,14,203,5,8,99,104,105,108,100,114,101,110,1,119,6,102,104,97,84,95,74,33,0,230,150,209,139,14,203,5,4,100,97,116,97,1,40,0,230,150,209,139,14,203,5,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,230,150,209,139,14,203,5,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,196,148,203,38,3,6,102,104,97,84,95,74,0,200,230,150,209,139,14,141,5,230,150,209,139,14,199,4,1,119,6,87,78,105,110,55,95,39,0,196,148,203,38,1,6,86,69,105,86,107,114,1,40,0,230,150,209,139,14,213,5,2,105,100,1,119,6,86,69,105,86,107,114,40,0,230,150,209,139,14,213,5,2,116,121,1,119,9,112,97,114,97,103,114,97,112,104,40,0,230,150,209,139,14,213,5,6,112,97,114,101,110,116,1,119,6,87,78,105,110,55,95,40,0,230,150,209,139,14,213,5,8,99,104,105,108,100,114,101,110,1,119,6,101,119,80,54,101,102,33,0,230,150,209,139,14,213,5,4,100,97,116,97,1,40,0,230,150,209,139,14,213,5,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,230,150,209,139,14,213,5,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,196,148,203,38,3,6,101,119,80,54,101,102,0,8,0,230,150,209,139,14,211,5,1,119,6,86,69,105,86,107,114,39,0,196,148,203,38,1,6,75,81,106,70,66,89,1,40,0,230,150,209,139,14,223,5,2,105,100,1,119,6,75,81,106,70,66,89,40,0,230,150,209,139,14,223,5,2,116,121,1,119,10,116,97,98,108,101,47,99,101,108,108,40,0,230,150,209,139,14,223,5,6,112,97,114,101,110,116,1,119,6,45,45,112,51,53,121,40,0,230,150,209,139,14,223,5,8,99,104,105,108,100,114,101,110,1,119,6,55,71,56,98,84,78,33,0,230,150,209,139,14,223,5,4,100,97,116,97,1,40,0,230,150,209,139,14,223,5,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,230,150,209,139,14,223,5,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,196,148,203,38,3,6,55,71,56,98,84,78,0,136,230,150,209,139,14,219,4,1,119,6,75,81,106,70,66,89,39,0,196,148,203,38,1,6,103,115,109,113,111,48,1,40,0,230,150,209,139,14,233,5,2,105,100,1,119,6,103,115,109,113,111,48,40,0,230,150,209,139,14,233,5,2,116,121,1,119,9,112,97,114,97,103,114,97,112,104,40,0,230,150,209,139,14,233,5,6,112,97,114,101,110,116,1,119,6,75,81,106,70,66,89,40,0,230,150,209,139,14,233,5,8,99,104,105,108,100,114,101,110,1,119,6,77,54,56,53,90,83,33,0,230,150,209,139,14,233,5,4,100,97,116,97,1,40,0,230,150,209,139,14,233,5,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,230,150,209,139,14,233,5,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,196,148,203,38,3,6,77,54,56,53,90,83,0,8,0,230,150,209,139,14,231,5,1,119,6,103,115,109,113,111,48,4,0,230,150,209,139,14,178,5,1,57,161,230,150,209,139,14,218,5,1,132,230,150,209,139,14,243,5,1,57,168,230,150,209,139,14,244,5,1,119,27,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,57,57,34,125,93,125,4,0,230,150,209,139,14,177,5,1,50,161,230,150,209,139,14,198,5,1,132,230,150,209,139,14,247,5,1,50,168,230,150,209,139,14,248,5,1,119,27,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,50,50,34,125,93,125,4,0,230,150,209,139,14,179,5,1,52,161,230,150,209,139,14,238,5,1,132,230,150,209,139,14,251,5,1,52,168,230,150,209,139,14,252,5,1,119,27,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,52,52,34,125,93,125,39,0,196,148,203,38,4,6,84,81,118,97,82,53,2,39,0,196,148,203,38,1,6,53,90,85,81,98,56,1,40,0,230,150,209,139,14,128,6,2,105,100,1,119,6,53,90,85,81,98,56,40,0,230,150,209,139,14,128,6,2,116,121,1,119,7,99,97,108,108,111,117,116,40,0,230,150,209,139,14,128,6,6,112,97,114,101,110,116,1,119,10,74,118,87,74,108,105,53,79,117,84,40,0,230,150,209,139,14,128,6,8,99,104,105,108,100,114,101,110,1,119,6,105,84,113,54,95,110,33,0,230,150,209,139,14,128,6,4,100,97,116,97,1,40,0,230,150,209,139,14,128,6,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,230,150,209,139,14,128,6,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,196,148,203,38,3,6,105,84,113,54,95,110,0,136,230,150,209,139,14,251,3,1,119,6,53,90,85,81,98,56,4,0,230,150,209,139,14,255,5,1,100,161,230,150,209,139,14,133,6,1,132,230,150,209,139,14,138,6,1,100,161,230,150,209,139,14,139,6,1,132,230,150,209,139,14,140,6,1,100,161,230,150,209,139,14,141,6,1,132,230,150,209,139,14,142,6,1,115,161,230,150,209,139,14,143,6,1,132,230,150,209,139,14,144,6,1,97,161,230,150,209,139,14,145,6,1,168,230,150,209,139,14,155,4,1,119,60,123,34,119,105,100,116,104,34,58,56,48,46,48,44,34,114,111,119,80,111,115,105,116,105,111,110,34,58,48,44,34,104,101,105,103,104,116,34,58,51,55,46,48,44,34,99,111,108,80,111,115,105,116,105,111,110,34,58,48,125,168,230,150,209,139,14,245,4,1,119,60,123,34,114,111,119,80,111,115,105,116,105,111,110,34,58,48,44,34,119,105,100,116,104,34,58,56,48,46,48,44,34,99,111,108,80,111,115,105,116,105,111,110,34,58,49,44,34,104,101,105,103,104,116,34,58,51,55,46,48,125,168,230,150,209,139,14,238,4,1,119,60,123,34,119,105,100,116,104,34,58,56,48,46,48,44,34,99,111,108,80,111,115,105,116,105,111,110,34,58,50,44,34,104,101,105,103,104,116,34,58,51,55,46,48,44,34,114,111,119,80,111,115,105,116,105,111,110,34,58,48,125,161,230,150,209,139,14,182,5,1,168,230,150,209,139,14,175,4,1,119,60,123,34,119,105,100,116,104,34,58,56,48,46,48,44,34,99,111,108,80,111,115,105,116,105,111,110,34,58,48,44,34,104,101,105,103,104,116,34,58,51,55,46,48,44,34,114,111,119,80,111,115,105,116,105,111,110,34,58,49,125,168,230,150,209,139,14,137,5,1,119,60,123,34,119,105,100,116,104,34,58,56,48,46,48,44,34,104,101,105,103,104,116,34,58,51,55,46,48,44,34,99,111,108,80,111,115,105,116,105,111,110,34,58,49,44,34,114,111,119,80,111,115,105,116,105,111,110,34,58,49,125,168,230,150,209,139,14,239,4,1,119,60,123,34,104,101,105,103,104,116,34,58,51,55,46,48,44,34,99,111,108,80,111,115,105,116,105,111,110,34,58,50,44,34,119,105,100,116,104,34,58,56,48,46,48,44,34,114,111,119,80,111,115,105,116,105,111,110,34,58,49,125,161,230,150,209,139,14,151,6,1,168,230,150,209,139,14,188,5,1,119,60,123,34,114,111,119,80,111,115,105,116,105,111,110,34,58,50,44,34,99,111,108,80,111,115,105,116,105,111,110,34,58,48,44,34,119,105,100,116,104,34,58,56,48,46,48,44,34,104,101,105,103,104,116,34,58,51,55,46,48,125,168,230,150,209,139,14,208,5,1,119,60,123,34,119,105,100,116,104,34,58,56,48,46,48,44,34,104,101,105,103,104,116,34,58,51,55,46,48,44,34,99,111,108,80,111,115,105,116,105,111,110,34,58,49,44,34,114,111,119,80,111,115,105,116,105,111,110,34,58,50,125,168,230,150,209,139,14,228,5,1,119,60,123,34,104,101,105,103,104,116,34,58,51,55,46,48,44,34,114,111,119,80,111,115,105,116,105,111,110,34,58,50,44,34,99,111,108,80,111,115,105,116,105,111,110,34,58,50,44,34,119,105,100,116,104,34,58,56,48,46,48,125,161,230,150,209,139,14,155,6,1,168,230,150,209,139,14,159,6,1,119,114,123,34,99,111,108,77,105,110,105,109,117,109,87,105,100,116,104,34,58,52,48,46,48,44,34,99,111,108,115,72,101,105,103,104,116,34,58,49,49,57,46,48,44,34,99,111,108,115,76,101,110,34,58,51,44,34,114,111,119,115,76,101,110,34,58,51,44,34,114,111,119,68,101,102,97,117,108,116,72,101,105,103,104,116,34,58,52,48,46,48,44,34,99,111,108,68,101,102,97,117,108,116,87,105,100,116,104,34,58,56,48,46,48,125,39,0,196,148,203,38,4,6,68,98,116,65,114,106,2,4,0,230,150,209,139,14,161,6,6,100,100,100,115,97,50,161,230,150,209,139,14,147,6,1,132,230,150,209,139,14,167,6,1,50,168,230,150,209,139,14,168,6,1,119,69,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,100,100,100,115,97,50,50,34,125,93,44,34,105,99,111,110,34,58,34,240,159,147,140,34,44,34,98,103,67,111,108,111,114,34,58,34,48,120,102,102,102,50,102,50,102,50,34,125,39,0,196,148,203,38,4,6,66,116,102,84,86,95,2,39,0,196,148,203,38,1,6,103,73,113,80,112,117,1,40,0,230,150,209,139,14,172,6,2,105,100,1,119,6,103,73,113,80,112,117,40,0,230,150,209,139,14,172,6,2,116,121,1,119,9,112,97,114,97,103,114,97,112,104,40,0,230,150,209,139,14,172,6,6,112,97,114,101,110,116,1,119,10,74,118,87,74,108,105,53,79,117,84,40,0,230,150,209,139,14,172,6,8,99,104,105,108,100,114,101,110,1,119,6,108,52,49,118,71,70,40,0,230,150,209,139,14,172,6,4,100,97,116,97,1,119,12,123,34,100,101,108,116,97,34,58,91,93,125,40,0,230,150,209,139,14,172,6,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,230,150,209,139,14,172,6,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,196,148,203,38,3,6,108,52,49,118,71,70,0,200,230,150,209,139,14,251,3,230,150,209,139,14,137,6,1,119,6,103,73,113,80,112,117,39,0,196,148,203,38,4,6,86,52,45,51,122,45,2,39,0,196,148,203,38,1,6,119,122,102,78,72,69,1,40,0,230,150,209,139,14,183,6,2,105,100,1,119,6,119,122,102,78,72,69,40,0,230,150,209,139,14,183,6,2,116,121,1,119,9,112,97,114,97,103,114,97,112,104,40,0,230,150,209,139,14,183,6,6,112,97,114,101,110,116,1,119,10,74,118,87,74,108,105,53,79,117,84,40,0,230,150,209,139,14,183,6,8,99,104,105,108,100,114,101,110,1,119,6,100,110,56,106,114,112,40,0,230,150,209,139,14,183,6,4,100,97,116,97,1,119,12,123,34,100,101,108,116,97,34,58,91,93,125,40,0,230,150,209,139,14,183,6,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,230,150,209,139,14,183,6,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,196,148,203,38,3,6,100,110,56,106,114,112,0,200,230,150,209,139,14,230,3,230,150,209,139,14,149,4,1,119,6,119,122,102,78,72,69,9,248,208,217,159,7,0,0,2,0,1,0,3,0,1,0,3,0,1,0,3,0,1,0,1,1,211,142,141,147,5,0,161,226,250,246,177,3,16,8,1,226,250,246,177,3,0,33,1,4,109,101,116,97,12,108,97,115,116,95,115,121,110,99,95,97,116,17,2,167,238,246,72,0,161,211,142,141,147,5,7,85,168,167,238,246,72,84,1,122,0,0,0,0,102,78,252,249,22,196,148,203,38,0,39,1,4,100,97,116,97,8,100,111,99,117,109,101,110,116,1,39,0,196,148,203,38,0,6,98,108,111,99,107,115,1,39,0,196,148,203,38,0,4,109,101,116,97,1,39,0,196,148,203,38,2,12,99,104,105,108,100,114,101,110,95,109,97,112,1,39,0,196,148,203,38,2,8,116,101,120,116,95,109,97,112,1,40,0,196,148,203,38,0,7,112,97,103,101,95,105,100,1,119,10,74,118,87,74,108,105,53,79,117,84,39,0,196,148,203,38,1,10,74,118,87,74,108,105,53,79,117,84,1,40,0,196,148,203,38,6,2,105,100,1,119,10,74,118,87,74,108,105,53,79,117,84,40,0,196,148,203,38,6,2,116,121,1,119,4,112,97,103,101,40,0,196,148,203,38,6,6,112,97,114,101,110,116,1,119,0,40,0,196,148,203,38,6,8,99,104,105,108,100,114,101,110,1,119,10,50,121,100,104,90,109,56,81,67,117,40,0,196,148,203,38,6,4,100,97,116,97,1,119,2,123,125,40,0,196,148,203,38,6,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,196,148,203,38,6,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,196,148,203,38,3,10,50,121,100,104,90,109,56,81,67,117,0,33,0,196,148,203,38,1,10,69,115,119,105,88,82,80,121,106,72,1,0,5,0,1,0,1,33,0,196,148,203,38,3,10,76,119,103,107,118,65,79,53,66,103,1,1,0,196,148,203,38,14,1,33,0,196,148,203,38,4,10,104,115,108,107,104,97,102,49,51,111,1,6,248,208,217,159,7,1,0,16,226,250,246,177,3,1,0,17,211,142,141,147,5,1,0,8,196,148,203,38,1,15,11,230,150,209,139,14,121,0,8,18,1,24,10,36,1,38,1,40,1,42,1,49,1,55,1,57,1,59,1,61,1,68,1,74,11,89,19,109,1,111,1,118,1,124,1,126,1,128,1,1,130,1,10,141,1,9,151,1,1,158,1,1,164,1,1,166,1,1,170,1,10,181,1,1,183,1,1,190,1,1,196,1,1,198,1,1,200,1,1,207,1,1,214,1,1,216,1,1,220,1,10,236,1,1,242,1,1,244,1,1,248,1,10,136,2,1,142,2,1,144,2,1,148,2,10,159,2,13,173,2,10,184,2,10,195,2,1,202,2,1,208,2,1,210,2,1,214,2,10,225,2,1,232,2,1,238,2,1,240,2,1,242,2,1,244,2,1,251,2,1,130,3,1,132,3,1,134,3,1,138,3,10,154,3,1,160,3,1,162,3,1,166,3,10,177,3,10,188,3,10,199,3,10,210,3,10,221,3,10,236,3,1,242,3,10,129,4,10,145,4,1,155,4,1,165,4,1,175,4,1,185,4,1,195,4,1,205,4,1,215,4,1,225,4,1,231,4,1,233,4,1,238,4,2,245,4,1,255,4,1,137,5,1,147,5,1,152,5,1,154,5,1,156,5,1,160,5,1,162,5,1,166,5,1,170,5,1,174,5,1,180,5,3,188,5,1,198,5,1,208,5,1,218,5,1,228,5,1,238,5,1,244,5,1,248,5,1,252,5,1,133,6,1,139,6,1,141,6,1,143,6,1,145,6,1,147,6,1,151,6,1,155,6,1,159,6,1,168,6,1,167,238,246,72,1,0,85],"version":0,"object_id":"f56bdf0f-90c8-53fb-97d9-ad5860d2b7a0"},"code":0,"message":"Operation completed successfully."} \ No newline at end of file diff --git a/frontend/appflowy_web_app/cypress/fixtures/folder.json b/frontend/appflowy_web_app/cypress/fixtures/folder.json deleted file mode 100644 index 2b1a53a989..0000000000 --- a/frontend/appflowy_web_app/cypress/fixtures/folder.json +++ /dev/null @@ -1 +0,0 @@ -{"data":{"state_vector":[160,1,128,252,161,128,4,33,130,180,254,251,6,18,135,232,133,203,9,15,135,240,136,178,10,45,137,226,192,199,6,20,138,202,240,189,2,134,1,139,240,196,145,14,138,2,139,152,215,249,10,34,141,216,158,150,1,3,142,130,192,134,11,3,143,184,153,180,6,6,140,228,230,243,1,2,145,190,137,224,10,2,145,144,146,185,5,12,140,152,206,145,6,165,1,135,166,246,235,6,3,147,206,229,235,1,3,137,164,190,210,1,39,152,158,185,230,10,7,153,130,203,161,6,97,152,252,186,192,1,2,154,244,246,165,8,198,1,156,148,170,169,10,16,157,240,144,231,2,3,158,156,181,152,10,49,158,182,250,251,9,15,160,192,253,131,5,3,161,178,132,150,11,101,159,156,204,250,6,27,163,236,177,169,4,12,164,188,201,172,1,3,158,184,218,165,3,38,162,238,198,212,7,3,170,140,240,234,9,31,171,204,155,217,8,3,171,142,166,254,1,6,173,252,148,184,13,64,176,154,159,227,1,20,178,162,190,217,10,3,179,252,154,44,3,180,230,210,212,13,7,182,172,247,194,5,3,183,226,184,158,8,8,187,220,199,239,8,70,195,242,227,194,8,3,196,154,250,183,6,62,197,254,154,201,10,3,200,142,208,241,4,157,1,202,160,246,212,1,6,203,184,221,173,11,37,206,220,129,131,4,21,207,228,238,162,8,50,209,250,203,254,15,3,210,228,153,221,12,3,211,202,217,232,12,7,211,166,203,229,4,195,1,214,168,149,214,3,15,219,220,239,171,8,10,225,248,138,176,2,49,226,212,179,248,2,15,229,154,128,197,12,24,234,182,182,157,9,8,235,178,165,206,5,72,234,156,130,211,12,12,241,130,161,205,7,12,244,226,228,149,2,30,245,220,194,52,39,247,200,243,247,14,100,248,136,168,181,1,3,248,210,237,129,13,2,250,198,166,187,7,2,248,196,187,185,10,31,252,218,241,167,14,3,255,140,248,220,6,3,128,211,179,216,2,10,133,159,138,205,12,131,1,135,167,156,250,14,16,135,193,208,135,7,16,141,245,194,142,11,7,141,205,220,149,4,3,143,131,148,152,6,15,141,171,170,217,4,2,145,159,164,217,14,3,149,129,169,191,12,16,149,249,242,175,4,31,149,189,189,215,8,3,149,161,132,184,14,221,2,154,243,157,196,14,12,154,193,208,134,10,9,155,165,205,152,11,3,154,235,215,240,4,13,155,159,180,195,15,6,157,207,243,216,6,2,161,239,241,154,13,106,162,159,252,196,11,3,164,155,139,169,7,35,165,139,157,171,15,103,164,203,250,235,13,7,167,131,133,162,9,11,166,201,221,141,13,72,169,197,188,221,3,3,170,255,211,105,38,166,203,155,46,6,173,187,245,170,14,46,174,151,139,93,179,2,175,225,172,150,8,11,175,147,217,214,1,33,177,161,136,243,11,10,178,203,205,182,4,1,175,205,156,228,6,10,180,205,189,133,13,20,181,175,219,209,12,10,182,143,233,195,4,111,177,219,160,167,7,4,184,201,188,172,10,3,184,231,170,67,6,186,197,166,179,15,7,187,173,214,176,15,3,188,171,136,250,8,21,188,237,223,145,6,26,190,139,191,155,1,2,191,157,147,233,9,32,193,249,142,142,4,17,198,189,216,175,6,23,200,205,214,172,10,30,201,129,238,197,4,84,201,191,159,147,14,17,200,203,236,184,2,35,200,159,185,206,9,8,205,149,231,236,11,68,213,161,242,209,13,35,214,139,213,136,8,66,213,255,156,145,1,2,219,227,140,137,6,34,221,147,167,147,15,177,2,222,205,223,235,7,31,223,209,193,147,11,81,227,209,197,253,2,14,229,153,197,202,7,241,6,231,139,244,188,8,13,232,207,157,148,2,2,233,165,139,246,14,43,233,247,183,159,1,4,235,225,184,133,10,3,234,153,236,158,4,73,234,187,164,181,1,24,231,189,134,196,8,77,239,199,189,146,3,24,240,149,229,225,6,15,241,155,213,233,1,52,240,253,240,229,1,79,243,239,182,181,13,30,236,229,225,232,8,116,246,185,174,192,6,90,248,153,216,10,3,251,189,220,155,14,3,252,163,130,200,6,30,253,205,145,137,11,10,252,171,209,175,15,4,255,255,147,249,10,3],"doc_state":[160,1,3,209,250,203,254,15,0,161,226,212,179,248,2,9,1,161,226,212,179,248,2,10,1,136,174,151,139,93,249,1,1,118,2,2,105,100,119,36,50,54,100,53,99,56,99,49,45,49,99,54,54,45,52,53,57,99,45,98,99,54,99,45,102,52,100,97,49,97,54,54,51,51,52,56,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,83,246,78,2,155,159,180,195,15,0,161,198,189,216,175,6,22,1,161,253,205,145,137,11,9,5,7,186,197,166,179,15,0,161,161,178,132,150,11,98,1,161,161,178,132,150,11,99,1,129,161,178,132,150,11,100,1,161,161,178,132,150,11,97,1,161,161,178,132,150,11,94,1,161,161,178,132,150,11,95,1,129,186,197,166,179,15,2,1,3,187,173,214,176,15,0,168,244,226,228,149,2,27,1,122,4,56,115,160,190,64,16,0,168,244,226,228,149,2,28,1,122,0,0,0,0,102,32,153,171,136,248,153,216,10,2,1,118,2,2,105,100,119,36,48,53,51,51,50,98,97,52,45,97,54,57,48,45,52,50,57,51,45,57,56,54,54,45,56,52,100,97,99,55,102,101,50,97,101,97,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,32,153,171,1,252,171,209,175,15,0,161,128,211,179,216,2,9,4,103,165,139,157,171,15,0,8,0,201,129,238,197,4,52,1,118,1,2,105,100,119,36,97,53,53,54,54,101,52,57,45,102,49,53,54,45,52,49,54,56,45,57,98,50,100,45,49,55,57,50,54,99,53,100,97,51,50,57,168,201,129,238,197,4,51,1,122,0,0,0,0,102,74,244,14,161,170,140,240,234,9,22,1,39,0,203,184,221,173,11,1,36,97,53,53,54,54,101,52,57,45,102,49,53,54,45,52,49,54,56,45,57,98,50,100,45,49,55,57,50,54,99,53,100,97,51,50,57,1,40,0,165,139,157,171,15,3,2,105,100,1,119,36,97,53,53,54,54,101,52,57,45,102,49,53,54,45,52,49,54,56,45,57,98,50,100,45,49,55,57,50,54,99,53,100,97,51,50,57,40,0,165,139,157,171,15,3,4,110,97,109,101,1,119,14,66,111,97,114,100,32,99,104,101,99,107,98,111,120,40,0,165,139,157,171,15,3,3,98,105,100,1,119,36,101,52,49,48,55,52,55,98,45,53,102,50,102,45,52,53,97,48,45,98,50,102,55,45,56,57,48,97,100,51,48,48,49,51,53,53,40,0,165,139,157,171,15,3,4,100,101,115,99,1,119,0,40,0,165,139,157,171,15,3,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,2,33,0,165,139,157,171,15,3,10,99,114,101,97,116,101,100,95,97,116,1,39,0,203,184,221,173,11,4,36,97,53,53,54,54,101,52,57,45,102,49,53,54,45,52,49,54,56,45,57,98,50,100,45,49,55,57,50,54,99,53,100,97,51,50,57,0,40,0,165,139,157,171,15,3,4,105,99,111,110,1,119,0,40,0,165,139,157,171,15,3,10,99,114,101,97,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,33,0,165,139,157,171,15,3,16,108,97,115,116,95,101,100,105,116,101,100,95,116,105,109,101,1,33,0,165,139,157,171,15,3,14,108,97,115,116,95,101,100,105,116,101,100,95,98,121,1,161,170,140,240,234,9,21,1,161,165,139,157,171,15,2,1,129,159,156,204,250,6,26,1,136,225,248,138,176,2,17,1,118,1,2,105,100,119,36,49,98,48,101,51,50,50,100,45,52,57,48,57,45,52,99,54,51,45,57,49,52,97,45,100,48,51,52,102,99,51,54,51,48,57,55,161,225,248,138,176,2,18,1,161,193,249,142,142,4,8,1,39,0,203,184,221,173,11,1,36,49,98,48,101,51,50,50,100,45,52,57,48,57,45,52,99,54,51,45,57,49,52,97,45,100,48,51,52,102,99,51,54,51,48,57,55,1,40,0,165,139,157,171,15,21,2,105,100,1,119,36,49,98,48,101,51,50,50,100,45,52,57,48,57,45,52,99,54,51,45,57,49,52,97,45,100,48,51,52,102,99,51,54,51,48,57,55,40,0,165,139,157,171,15,21,4,110,97,109,101,1,119,8,85,110,116,105,116,108,101,100,40,0,165,139,157,171,15,21,3,98,105,100,1,119,36,50,54,100,53,99,56,99,49,45,49,99,54,54,45,52,53,57,99,45,98,99,54,99,45,102,52,100,97,49,97,54,54,51,51,52,56,40,0,165,139,157,171,15,21,4,100,101,115,99,1,119,0,40,0,165,139,157,171,15,21,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,2,33,0,165,139,157,171,15,21,10,99,114,101,97,116,101,100,95,97,116,1,39,0,203,184,221,173,11,4,36,49,98,48,101,51,50,50,100,45,52,57,48,57,45,52,99,54,51,45,57,49,52,97,45,100,48,51,52,102,99,51,54,51,48,57,55,0,40,0,165,139,157,171,15,21,4,105,99,111,110,1,119,0,40,0,165,139,157,171,15,21,10,99,114,101,97,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,33,0,165,139,157,171,15,21,16,108,97,115,116,95,101,100,105,116,101,100,95,116,105,109,101,1,33,0,165,139,157,171,15,21,14,108,97,115,116,95,101,100,105,116,101,100,95,98,121,1,161,193,249,142,142,4,13,1,161,165,139,157,171,15,32,1,161,165,139,157,171,15,31,1,129,165,139,157,171,15,17,1,8,0,165,139,157,171,15,28,1,118,1,2,105,100,119,36,51,53,48,102,52,50,53,98,45,98,54,55,49,45,52,101,50,100,45,56,49,56,50,45,53,57,57,56,97,54,101,54,50,57,50,52,168,165,139,157,171,15,27,1,122,0,0,0,0,102,74,246,67,161,165,139,157,171,15,35,1,39,0,203,184,221,173,11,1,36,51,53,48,102,52,50,53,98,45,98,54,55,49,45,52,101,50,100,45,56,49,56,50,45,53,57,57,56,97,54,101,54,50,57,50,52,1,40,0,165,139,157,171,15,40,2,105,100,1,119,36,51,53,48,102,52,50,53,98,45,98,54,55,49,45,52,101,50,100,45,56,49,56,50,45,53,57,57,56,97,54,101,54,50,57,50,52,40,0,165,139,157,171,15,40,4,110,97,109,101,1,119,8,85,110,116,105,116,108,101,100,40,0,165,139,157,171,15,40,3,98,105,100,1,119,36,49,98,48,101,51,50,50,100,45,52,57,48,57,45,52,99,54,51,45,57,49,52,97,45,100,48,51,52,102,99,51,54,51,48,57,55,40,0,165,139,157,171,15,40,4,100,101,115,99,1,119,0,40,0,165,139,157,171,15,40,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,1,40,0,165,139,157,171,15,40,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,74,246,67,39,0,203,184,221,173,11,4,36,51,53,48,102,52,50,53,98,45,98,54,55,49,45,52,101,50,100,45,56,49,56,50,45,53,57,57,56,97,54,101,54,50,57,50,52,0,40,0,165,139,157,171,15,40,4,105,99,111,110,1,119,0,40,0,165,139,157,171,15,40,10,99,114,101,97,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,40,0,165,139,157,171,15,40,16,108,97,115,116,95,101,100,105,116,101,100,95,116,105,109,101,1,122,0,0,0,0,102,74,246,67,40,0,165,139,157,171,15,40,14,108,97,115,116,95,101,100,105,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,161,165,139,157,171,15,34,1,161,165,139,157,171,15,39,1,129,165,139,157,171,15,36,1,161,176,154,159,227,1,17,1,161,176,154,159,227,1,18,1,40,0,176,154,159,227,1,4,5,101,120,116,114,97,1,119,36,123,34,99,111,118,101,114,34,58,123,34,116,121,112,101,34,58,34,110,111,110,101,34,44,34,118,97,108,117,101,34,58,34,34,125,125,161,165,139,157,171,15,55,1,161,165,139,157,171,15,56,1,129,165,139,157,171,15,54,1,161,165,139,157,171,15,52,1,161,165,139,157,171,15,53,1,129,165,139,157,171,15,60,1,161,165,139,157,171,15,61,1,161,165,139,157,171,15,62,1,129,165,139,157,171,15,63,1,161,165,139,157,171,15,64,1,161,165,139,157,171,15,65,1,129,165,139,157,171,15,66,1,8,0,225,248,138,176,2,27,1,118,1,2,105,100,119,36,57,49,101,97,55,99,48,56,45,102,54,98,51,45,52,98,56,49,45,97,97,49,101,45,100,51,54,54,52,54,56,54,49,56,54,102,168,225,248,138,176,2,26,1,122,0,0,0,0,102,74,248,249,161,225,248,138,176,2,40,1,39,0,203,184,221,173,11,1,36,57,49,101,97,55,99,48,56,45,102,54,98,51,45,52,98,56,49,45,97,97,49,101,45,100,51,54,54,52,54,56,54,49,56,54,102,1,40,0,165,139,157,171,15,73,2,105,100,1,119,36,57,49,101,97,55,99,48,56,45,102,54,98,51,45,52,98,56,49,45,97,97,49,101,45,100,51,54,54,52,54,56,54,49,56,54,102,40,0,165,139,157,171,15,73,4,110,97,109,101,1,119,8,85,110,116,105,116,108,101,100,40,0,165,139,157,171,15,73,3,98,105,100,1,119,36,50,99,49,101,101,57,53,97,45,49,98,48,57,45,52,97,49,102,45,56,100,53,101,45,53,48,49,98,99,52,56,54,49,97,57,100,40,0,165,139,157,171,15,73,4,100,101,115,99,1,119,0,40,0,165,139,157,171,15,73,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,2,40,0,165,139,157,171,15,73,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,74,248,249,39,0,203,184,221,173,11,4,36,57,49,101,97,55,99,48,56,45,102,54,98,51,45,52,98,56,49,45,97,97,49,101,45,100,51,54,54,52,54,56,54,49,56,54,102,0,40,0,165,139,157,171,15,73,4,105,99,111,110,1,119,0,40,0,165,139,157,171,15,73,10,99,114,101,97,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,40,0,165,139,157,171,15,73,16,108,97,115,116,95,101,100,105,116,101,100,95,116,105,109,101,1,122,0,0,0,0,102,74,248,249,40,0,165,139,157,171,15,73,14,108,97,115,116,95,101,100,105,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,161,225,248,138,176,2,39,1,161,165,139,157,171,15,72,1,129,165,139,157,171,15,69,1,161,165,139,157,171,15,67,1,161,165,139,157,171,15,68,1,129,165,139,157,171,15,87,1,161,165,139,157,171,15,85,1,161,165,139,157,171,15,86,1,129,165,139,157,171,15,90,1,161,159,156,204,250,6,24,1,161,159,156,204,250,6,25,1,129,165,139,157,171,15,93,1,161,165,139,157,171,15,15,1,161,165,139,157,171,15,16,1,129,165,139,157,171,15,96,1,161,165,139,157,171,15,97,1,161,165,139,157,171,15,98,1,129,165,139,157,171,15,99,1,1,221,147,167,147,15,0,161,139,240,196,145,14,137,2,177,2,1,135,167,156,250,14,0,161,188,237,223,145,6,21,16,100,247,200,243,247,14,0,136,140,152,206,145,6,100,1,118,1,2,105,100,119,36,55,57,100,48,54,51,50,100,45,97,53,97,56,45,52,53,52,48,45,97,100,99,100,45,54,101,101,57,50,50,100,56,54,55,101,100,161,140,152,206,145,6,101,1,161,245,220,194,52,7,1,39,0,203,184,221,173,11,1,36,55,57,100,48,54,51,50,100,45,97,53,97,56,45,52,53,52,48,45,97,100,99,100,45,54,101,101,57,50,50,100,56,54,55,101,100,1,40,0,247,200,243,247,14,3,2,105,100,1,119,36,55,57,100,48,54,51,50,100,45,97,53,97,56,45,52,53,52,48,45,97,100,99,100,45,54,101,101,57,50,50,100,56,54,55,101,100,33,0,247,200,243,247,14,3,4,110,97,109,101,1,40,0,247,200,243,247,14,3,3,98,105,100,1,119,36,50,54,100,53,99,56,99,49,45,49,99,54,54,45,52,53,57,99,45,98,99,54,99,45,102,52,100,97,49,97,54,54,51,51,52,56,40,0,247,200,243,247,14,3,4,100,101,115,99,1,119,0,40,0,247,200,243,247,14,3,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,0,40,0,247,200,243,247,14,3,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,86,193,79,39,0,203,184,221,173,11,4,36,55,57,100,48,54,51,50,100,45,97,53,97,56,45,52,53,52,48,45,97,100,99,100,45,54,101,101,57,50,50,100,56,54,55,101,100,0,40,0,247,200,243,247,14,3,4,105,99,111,110,1,119,0,40,0,247,200,243,247,14,3,10,99,114,101,97,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,33,0,247,200,243,247,14,3,16,108,97,115,116,95,101,100,105,116,101,100,95,116,105,109,101,1,33,0,247,200,243,247,14,3,14,108,97,115,116,95,101,100,105,116,101,100,95,98,121,1,161,170,255,211,105,34,1,161,247,200,243,247,14,14,1,161,247,200,243,247,14,13,1,129,135,166,246,235,6,2,1,161,247,200,243,247,14,16,1,161,247,200,243,247,14,17,1,40,0,247,200,243,247,14,3,5,101,120,116,114,97,1,119,36,123,34,99,111,118,101,114,34,58,123,34,116,121,112,101,34,58,34,110,111,110,101,34,44,34,118,97,108,117,101,34,58,34,34,125,125,161,247,200,243,247,14,19,1,161,247,200,243,247,14,20,1,161,247,200,243,247,14,5,1,161,247,200,243,247,14,22,1,161,247,200,243,247,14,23,1,161,247,200,243,247,14,24,1,161,247,200,243,247,14,25,1,161,247,200,243,247,14,26,1,161,247,200,243,247,14,27,1,161,247,200,243,247,14,28,1,161,247,200,243,247,14,29,1,161,247,200,243,247,14,30,1,161,247,200,243,247,14,31,1,161,247,200,243,247,14,32,1,161,247,200,243,247,14,33,1,161,247,200,243,247,14,34,1,161,247,200,243,247,14,35,1,161,247,200,243,247,14,36,1,161,247,200,243,247,14,37,1,161,247,200,243,247,14,38,1,161,247,200,243,247,14,39,1,161,247,200,243,247,14,40,1,161,247,200,243,247,14,41,1,161,247,200,243,247,14,42,1,161,247,200,243,247,14,43,1,161,247,200,243,247,14,44,1,161,247,200,243,247,14,45,1,161,247,200,243,247,14,46,1,161,247,200,243,247,14,47,1,161,247,200,243,247,14,48,1,161,247,200,243,247,14,49,1,161,247,200,243,247,14,50,1,168,247,200,243,247,14,51,1,119,10,116,101,115,116,32,101,118,101,110,116,161,247,200,243,247,14,52,1,161,247,200,243,247,14,53,1,129,247,200,243,247,14,18,1,161,166,201,221,141,13,41,1,161,166,201,221,141,13,42,1,161,166,201,221,141,13,43,1,161,247,200,243,247,14,58,1,161,247,200,243,247,14,59,1,161,247,200,243,247,14,60,1,161,247,200,243,247,14,61,1,161,247,200,243,247,14,62,1,161,247,200,243,247,14,63,1,161,247,200,243,247,14,64,1,161,247,200,243,247,14,65,1,161,247,200,243,247,14,66,1,161,247,200,243,247,14,67,1,161,247,200,243,247,14,68,1,161,247,200,243,247,14,69,1,161,247,200,243,247,14,70,1,161,247,200,243,247,14,71,1,161,247,200,243,247,14,72,1,161,247,200,243,247,14,73,1,161,247,200,243,247,14,74,1,161,247,200,243,247,14,75,1,161,247,200,243,247,14,76,1,161,247,200,243,247,14,77,1,161,247,200,243,247,14,78,1,161,247,200,243,247,14,79,1,161,247,200,243,247,14,80,1,161,247,200,243,247,14,81,1,161,247,200,243,247,14,82,1,161,247,200,243,247,14,83,1,161,247,200,243,247,14,84,1,161,247,200,243,247,14,85,1,161,247,200,243,247,14,86,1,161,247,200,243,247,14,87,1,161,247,200,243,247,14,88,1,161,247,200,243,247,14,89,1,161,247,200,243,247,14,90,1,161,247,200,243,247,14,91,1,161,247,200,243,247,14,92,1,161,247,200,243,247,14,93,1,168,247,200,243,247,14,94,1,122,4,56,115,160,190,64,16,0,168,247,200,243,247,14,95,1,122,0,0,0,0,102,87,4,247,168,247,200,243,247,14,96,1,119,145,1,123,34,99,111,118,101,114,34,58,123,34,116,121,112,101,34,58,34,103,114,97,100,105,101,110,116,34,44,34,118,97,108,117,101,34,58,34,97,112,112,102,108,111,119,121,95,116,104,101,109,95,99,111,108,111,114,95,103,114,97,100,105,101,110,116,55,34,125,44,34,102,111,110,116,95,108,97,121,111,117,116,34,58,34,110,111,114,109,97,108,34,44,34,108,105,110,101,95,104,101,105,103,104,116,95,108,97,121,111,117,116,34,58,34,110,111,114,109,97,108,34,44,34,102,111,110,116,34,58,34,65,68,76,97,77,32,68,105,115,112,108,97,121,34,125,42,233,165,139,246,14,0,161,203,184,221,173,11,34,1,161,203,184,221,173,11,33,1,39,0,203,184,221,173,11,6,18,51,48,52,49,50,48,49,48,57,48,55,49,51,51,57,53,50,48,0,1,0,233,165,139,246,14,2,1,161,233,165,139,246,14,0,1,161,233,165,139,246,14,1,1,129,233,165,139,246,14,3,1,72,203,184,221,173,11,16,1,118,1,2,105,100,119,36,99,97,49,50,50,99,48,52,45,100,55,98,51,45,52,102,55,48,45,57,57,53,49,45,57,54,98,102,100,97,57,98,54,98,50,52,161,203,184,221,173,11,21,1,161,203,184,221,173,11,22,1,39,0,203,184,221,173,11,1,36,99,97,49,50,50,99,48,52,45,100,55,98,51,45,52,102,55,48,45,57,57,53,49,45,57,54,98,102,100,97,57,98,54,98,50,52,1,40,0,233,165,139,246,14,10,2,105,100,1,119,36,99,97,49,50,50,99,48,52,45,100,55,98,51,45,52,102,55,48,45,57,57,53,49,45,57,54,98,102,100,97,57,98,54,98,50,52,40,0,233,165,139,246,14,10,4,110,97,109,101,1,119,8,85,110,116,105,116,108,101,100,40,0,233,165,139,246,14,10,3,98,105,100,1,119,36,57,101,101,98,101,97,48,51,45,51,101,100,53,45,52,50,57,56,45,56,54,98,50,45,97,55,102,55,55,56,53,54,100,52,56,98,40,0,233,165,139,246,14,10,4,100,101,115,99,1,119,0,40,0,233,165,139,246,14,10,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,0,40,0,233,165,139,246,14,10,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,101,241,120,204,39,0,203,184,221,173,11,4,36,99,97,49,50,50,99,48,52,45,100,55,98,51,45,52,102,55,48,45,57,57,53,49,45,57,54,98,102,100,97,57,98,54,98,50,52,0,40,0,233,165,139,246,14,10,4,105,99,111,110,1,119,0,40,0,233,165,139,246,14,10,10,99,114,101,97,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,33,0,233,165,139,246,14,10,16,108,97,115,116,95,101,100,105,116,101,100,95,116,105,109,101,1,33,0,233,165,139,246,14,10,14,108,97,115,116,95,101,100,105,116,101,100,95,98,121,2,161,233,165,139,246,14,20,1,129,233,165,139,246,14,6,1,161,233,165,139,246,14,22,1,161,233,165,139,246,14,23,1,129,233,165,139,246,14,24,1,161,233,165,139,246,14,25,1,161,233,165,139,246,14,26,1,129,233,165,139,246,14,27,1,161,233,165,139,246,14,28,1,161,233,165,139,246,14,29,1,129,233,165,139,246,14,30,1,161,233,165,139,246,14,31,1,161,233,165,139,246,14,32,1,129,233,165,139,246,14,33,1,161,233,165,139,246,14,34,1,161,233,165,139,246,14,35,1,129,233,165,139,246,14,36,1,161,233,165,139,246,14,37,1,161,233,165,139,246,14,38,1,129,233,165,139,246,14,39,1,3,145,159,164,217,14,0,161,135,232,133,203,9,12,1,161,135,232,133,203,9,13,1,129,135,232,133,203,9,14,1,12,154,243,157,196,14,0,161,145,159,164,217,14,0,1,161,145,159,164,217,14,1,1,129,145,159,164,217,14,2,1,161,154,243,157,196,14,0,1,161,154,243,157,196,14,1,1,129,154,243,157,196,14,2,1,161,154,243,157,196,14,3,1,161,154,243,157,196,14,4,1,129,154,243,157,196,14,5,1,161,154,243,157,196,14,6,1,161,154,243,157,196,14,7,1,129,154,243,157,196,14,8,1,2,149,161,132,184,14,0,161,235,178,165,206,5,68,219,2,161,149,161,132,184,14,218,2,2,1,173,187,245,170,14,0,161,164,203,250,235,13,6,46,3,252,218,241,167,14,0,161,251,189,220,155,14,0,1,161,251,189,220,155,14,1,1,168,251,189,220,155,14,2,1,119,9,231,186,170,229,191,181,231,137,136,3,251,189,220,155,14,0,161,210,228,153,221,12,0,1,161,210,228,153,221,12,1,1,161,210,228,153,221,12,2,1,17,201,191,159,147,14,0,161,149,189,189,215,8,0,1,161,149,189,189,215,8,1,1,129,149,189,189,215,8,2,1,161,213,161,242,209,13,31,1,161,133,159,138,205,12,124,1,161,133,159,138,205,12,125,1,129,201,191,159,147,14,2,1,161,201,191,159,147,14,4,1,161,201,191,159,147,14,5,1,129,201,191,159,147,14,6,1,161,201,191,159,147,14,0,1,161,201,191,159,147,14,1,1,129,201,191,159,147,14,9,1,161,201,191,159,147,14,3,1,161,201,191,159,147,14,10,1,161,201,191,159,147,14,11,1,129,201,191,159,147,14,12,1,1,139,240,196,145,14,0,161,200,203,236,184,2,34,138,2,1,164,203,250,235,13,0,33,1,4,109,101,116,97,12,108,97,115,116,95,115,121,110,99,95,97,116,7,7,180,230,210,212,13,0,161,175,225,172,150,8,8,1,161,175,225,172,150,8,9,1,136,175,225,172,150,8,10,1,118,2,2,105,100,119,36,100,48,52,57,54,51,50,52,45,53,53,55,48,45,52,48,48,54,45,98,52,101,97,45,100,98,55,53,49,54,100,50,49,50,102,100,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,41,200,235,161,175,225,172,150,8,7,1,161,175,225,172,150,8,4,1,161,175,225,172,150,8,5,1,129,180,230,210,212,13,2,1,34,213,161,242,209,13,0,161,133,159,138,205,12,128,1,1,161,133,159,138,205,12,129,1,1,129,133,159,138,205,12,130,1,1,136,133,159,138,205,12,108,1,118,1,2,105,100,119,36,55,101,98,54,57,55,99,100,45,54,97,53,53,45,52,48,98,98,45,57,54,97,99,45,48,100,52,97,51,98,99,57,50,52,98,50,161,133,159,138,205,12,109,1,161,213,161,242,209,13,1,1,39,0,203,184,221,173,11,1,36,55,101,98,54,57,55,99,100,45,54,97,53,53,45,52,48,98,98,45,57,54,97,99,45,48,100,52,97,51,98,99,57,50,52,98,50,1,40,0,213,161,242,209,13,6,2,105,100,1,119,36,55,101,98,54,57,55,99,100,45,54,97,53,53,45,52,48,98,98,45,57,54,97,99,45,48,100,52,97,51,98,99,57,50,52,98,50,33,0,213,161,242,209,13,6,4,110,97,109,101,1,40,0,213,161,242,209,13,6,3,98,105,100,1,119,36,55,100,50,49,52,56,102,99,45,99,97,99,101,45,52,52,53,50,45,57,99,53,99,45,57,54,101,53,50,101,54,98,102,56,98,53,40,0,213,161,242,209,13,6,4,100,101,115,99,1,119,0,40,0,213,161,242,209,13,6,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,1,40,0,213,161,242,209,13,6,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,48,178,5,39,0,203,184,221,173,11,4,36,55,101,98,54,57,55,99,100,45,54,97,53,53,45,52,48,98,98,45,57,54,97,99,45,48,100,52,97,51,98,99,57,50,52,98,50,0,40,0,213,161,242,209,13,6,4,105,99,111,110,1,119,0,40,0,213,161,242,209,13,6,10,99,114,101,97,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,33,0,213,161,242,209,13,6,16,108,97,115,116,95,101,100,105,116,101,100,95,116,105,109,101,1,33,0,213,161,242,209,13,6,14,108,97,115,116,95,101,100,105,116,101,100,95,98,121,2,161,213,161,242,209,13,16,1,161,213,161,242,209,13,8,1,161,213,161,242,209,13,18,1,161,213,161,242,209,13,19,1,168,213,161,242,209,13,20,1,119,4,71,114,105,100,168,213,161,242,209,13,21,1,122,4,56,115,160,190,64,16,0,168,213,161,242,209,13,22,1,122,0,0,0,0,102,48,178,16,136,152,158,185,230,10,6,1,118,2,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,48,178,16,2,105,100,119,36,55,101,98,54,57,55,99,100,45,54,97,53,53,45,52,48,98,98,45,57,54,97,99,45,48,100,52,97,51,98,99,57,50,52,98,50,161,133,159,138,205,12,127,1,161,133,159,138,205,12,53,1,161,133,159,138,205,12,58,1,129,213,161,242,209,13,2,1,161,213,161,242,209,13,27,1,161,213,161,242,209,13,0,1,161,213,161,242,209,13,5,1,129,213,161,242,209,13,30,1,64,173,252,148,184,13,0,161,156,148,170,169,10,12,1,161,156,148,170,169,10,9,1,161,156,148,170,169,10,10,1,129,156,148,170,169,10,15,1,161,173,252,148,184,13,0,1,161,164,155,139,169,7,0,1,161,164,155,139,169,7,1,1,129,164,155,139,169,7,34,1,161,173,252,148,184,13,4,1,161,161,178,132,150,11,58,1,161,161,178,132,150,11,59,1,129,173,252,148,184,13,7,1,161,173,252,148,184,13,8,1,161,164,155,139,169,7,32,1,161,164,155,139,169,7,33,1,129,173,252,148,184,13,11,1,161,173,252,148,184,13,12,1,161,164,155,139,169,7,8,1,161,164,155,139,169,7,9,1,129,173,252,148,184,13,15,1,161,173,252,148,184,13,16,1,161,173,252,148,184,13,13,1,161,173,252,148,184,13,14,1,129,173,252,148,184,13,19,1,161,173,252,148,184,13,20,1,161,173,252,148,184,13,9,1,161,173,252,148,184,13,10,1,129,173,252,148,184,13,23,1,161,173,252,148,184,13,24,1,161,173,252,148,184,13,21,1,161,173,252,148,184,13,22,1,129,173,252,148,184,13,27,1,161,173,252,148,184,13,28,1,161,164,155,139,169,7,28,1,161,164,155,139,169,7,29,1,129,173,252,148,184,13,31,1,161,173,252,148,184,13,32,1,161,173,252,148,184,13,29,1,161,173,252,148,184,13,30,1,129,173,252,148,184,13,35,1,136,207,228,238,162,8,22,1,118,1,2,105,100,119,36,100,100,98,57,51,98,97,55,45,48,54,99,55,45,52,49,55,54,45,57,56,50,97,45,100,55,52,50,51,101,48,57,98,52,52,49,161,173,252,148,184,13,33,1,161,173,252,148,184,13,34,1,168,130,180,254,251,6,6,1,119,36,50,54,100,53,99,56,99,49,45,49,99,54,54,45,52,53,57,99,45,98,99,54,99,45,102,52,100,97,49,97,54,54,51,51,52,56,161,173,252,148,184,13,36,1,161,173,252,148,184,13,41,1,161,173,252,148,184,13,42,1,129,173,252,148,184,13,39,1,161,173,252,148,184,13,44,1,161,173,252,148,184,13,37,1,161,173,252,148,184,13,38,1,129,173,252,148,184,13,47,1,161,173,252,148,184,13,48,1,161,173,252,148,184,13,25,1,161,173,252,148,184,13,26,1,129,173,252,148,184,13,51,1,161,173,252,148,184,13,52,1,161,173,252,148,184,13,49,1,161,173,252,148,184,13,50,1,129,173,252,148,184,13,55,1,161,173,252,148,184,13,56,1,161,173,252,148,184,13,5,1,161,173,252,148,184,13,6,1,129,173,252,148,184,13,59,1,30,243,239,182,181,13,0,136,240,149,229,225,6,0,1,118,1,2,105,100,119,36,53,54,54,56,57,101,52,50,45,49,102,101,56,45,52,97,97,102,45,56,50,99,53,45,99,51,100,99,98,102,99,51,98,50,53,52,161,240,149,229,225,6,1,1,161,240,149,229,225,6,2,1,39,0,203,184,221,173,11,1,36,53,54,54,56,57,101,52,50,45,49,102,101,56,45,52,97,97,102,45,56,50,99,53,45,99,51,100,99,98,102,99,51,98,50,53,52,1,40,0,243,239,182,181,13,3,2,105,100,1,119,36,53,54,54,56,57,101,52,50,45,49,102,101,56,45,52,97,97,102,45,56,50,99,53,45,99,51,100,99,98,102,99,51,98,50,53,52,40,0,243,239,182,181,13,3,4,110,97,109,101,1,119,0,40,0,243,239,182,181,13,3,3,98,105,100,1,119,36,57,101,101,98,101,97,48,51,45,51,101,100,53,45,52,50,57,56,45,56,54,98,50,45,97,55,102,55,55,56,53,54,100,52,56,98,40,0,243,239,182,181,13,3,4,100,101,115,99,1,119,0,40,0,243,239,182,181,13,3,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,0,40,0,243,239,182,181,13,3,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,101,241,141,103,39,0,203,184,221,173,11,4,36,53,54,54,56,57,101,52,50,45,49,102,101,56,45,52,97,97,102,45,56,50,99,53,45,99,51,100,99,98,102,99,51,98,50,53,52,0,40,0,243,239,182,181,13,3,4,105,99,111,110,1,119,0,40,0,243,239,182,181,13,3,10,99,114,101,97,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,40,0,243,239,182,181,13,3,16,108,97,115,116,95,101,100,105,116,101,100,95,116,105,109,101,1,122,0,0,0,0,101,241,141,103,40,0,243,239,182,181,13,3,14,108,97,115,116,95,101,100,105,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,136,243,239,182,181,13,0,1,118,1,2,105,100,119,36,51,53,100,51,57,57,98,57,45,49,57,55,101,45,52,57,99,54,45,97,98,102,56,45,101,51,57,51,49,101,48,97,55,51,49,51,161,243,239,182,181,13,1,1,161,243,239,182,181,13,2,1,39,0,203,184,221,173,11,1,36,51,53,100,51,57,57,98,57,45,49,57,55,101,45,52,57,99,54,45,97,98,102,56,45,101,51,57,51,49,101,48,97,55,51,49,51,1,40,0,243,239,182,181,13,18,2,105,100,1,119,36,51,53,100,51,57,57,98,57,45,49,57,55,101,45,52,57,99,54,45,97,98,102,56,45,101,51,57,51,49,101,48,97,55,51,49,51,40,0,243,239,182,181,13,18,4,110,97,109,101,1,119,0,40,0,243,239,182,181,13,18,3,98,105,100,1,119,36,57,101,101,98,101,97,48,51,45,51,101,100,53,45,52,50,57,56,45,56,54,98,50,45,97,55,102,55,55,56,53,54,100,52,56,98,40,0,243,239,182,181,13,18,4,100,101,115,99,1,119,0,40,0,243,239,182,181,13,18,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,0,40,0,243,239,182,181,13,18,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,101,241,141,156,39,0,203,184,221,173,11,4,36,51,53,100,51,57,57,98,57,45,49,57,55,101,45,52,57,99,54,45,97,98,102,56,45,101,51,57,51,49,101,48,97,55,51,49,51,0,40,0,243,239,182,181,13,18,4,105,99,111,110,1,119,0,40,0,243,239,182,181,13,18,10,99,114,101,97,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,40,0,243,239,182,181,13,18,16,108,97,115,116,95,101,100,105,116,101,100,95,116,105,109,101,1,122,0,0,0,0,101,241,141,156,40,0,243,239,182,181,13,18,14,108,97,115,116,95,101,100,105,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,1,161,239,241,154,13,0,161,158,184,218,165,3,37,106,72,166,201,221,141,13,0,161,182,172,247,194,5,0,1,161,182,172,247,194,5,1,1,129,182,172,247,194,5,2,1,161,170,255,211,105,34,1,161,245,220,194,52,9,1,161,245,220,194,52,10,1,136,166,201,221,141,13,2,1,118,2,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,85,132,208,2,105,100,119,36,55,100,50,49,52,56,102,99,45,99,97,99,101,45,52,52,53,50,45,57,99,53,99,45,57,54,101,53,50,101,54,98,102,56,98,53,161,166,201,221,141,13,4,1,161,166,201,221,141,13,5,1,136,166,201,221,141,13,6,1,118,2,2,105,100,119,36,55,100,50,49,52,56,102,99,45,99,97,99,101,45,52,52,53,50,45,57,99,53,99,45,57,54,101,53,50,101,54,98,102,56,98,53,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,85,132,209,161,166,201,221,141,13,7,1,161,166,201,221,141,13,8,1,136,166,201,221,141,13,9,1,118,2,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,85,134,151,2,105,100,119,36,55,100,50,49,52,56,102,99,45,99,97,99,101,45,52,52,53,50,45,57,99,53,99,45,57,54,101,53,50,101,54,98,102,56,98,53,161,166,201,221,141,13,10,1,161,166,201,221,141,13,11,1,136,166,201,221,141,13,12,1,118,2,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,85,134,152,2,105,100,119,36,55,100,50,49,52,56,102,99,45,99,97,99,101,45,52,52,53,50,45,57,99,53,99,45,57,54,101,53,50,101,54,98,102,56,98,53,39,0,203,184,221,173,11,1,36,98,98,101,55,102,100,54,99,45,99,99,56,102,45,53,48,55,57,45,98,48,52,53,45,54,49,57,55,52,99,48,57,57,98,54,100,1,40,0,166,201,221,141,13,16,2,105,100,1,119,36,98,98,101,55,102,100,54,99,45,99,99,56,102,45,53,48,55,57,45,98,48,52,53,45,54,49,57,55,52,99,48,57,57,98,54,100,40,0,166,201,221,141,13,16,4,110,97,109,101,1,119,8,85,110,116,105,116,108,101,100,40,0,166,201,221,141,13,16,3,98,105,100,1,119,36,98,98,101,55,102,100,54,99,45,99,99,56,102,45,53,48,55,57,45,98,48,52,53,45,54,49,57,55,52,99,48,57,57,98,54,100,40,0,166,201,221,141,13,16,4,100,101,115,99,1,119,0,40,0,166,201,221,141,13,16,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,0,40,0,166,201,221,141,13,16,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,85,140,82,39,0,203,184,221,173,11,4,36,98,98,101,55,102,100,54,99,45,99,99,56,102,45,53,48,55,57,45,98,48,52,53,45,54,49,57,55,52,99,48,57,57,98,54,100,0,40,0,166,201,221,141,13,16,4,105,99,111,110,1,119,0,40,0,166,201,221,141,13,16,10,99,114,101,97,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,33,0,166,201,221,141,13,16,16,108,97,115,116,95,101,100,105,116,101,100,95,116,105,109,101,1,33,0,166,201,221,141,13,16,14,108,97,115,116,95,101,100,105,116,101,100,95,98,121,1,168,166,201,221,141,13,27,1,122,4,56,115,160,190,64,16,0,168,166,201,221,141,13,26,1,122,0,0,0,0,102,85,140,82,40,0,166,201,221,141,13,16,5,101,120,116,114,97,1,119,36,123,34,99,111,118,101,114,34,58,123,34,116,121,112,101,34,58,34,110,111,110,101,34,44,34,118,97,108,117,101,34,58,34,34,125,125,161,166,201,221,141,13,0,1,161,166,201,221,141,13,1,1,136,166,201,221,141,13,15,1,118,2,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,85,167,96,2,105,100,119,36,48,101,55,99,100,101,102,50,45,49,48,99,50,45,52,52,101,99,45,56,98,54,49,45,49,97,100,101,48,98,102,53,100,51,102,102,161,166,201,221,141,13,3,1,161,166,201,221,141,13,31,1,161,166,201,221,141,13,32,1,136,166,201,221,141,13,33,1,118,2,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,85,167,97,2,105,100,119,36,48,101,55,99,100,101,102,50,45,49,48,99,50,45,52,52,101,99,45,56,98,54,49,45,49,97,100,101,48,98,102,53,100,51,102,102,161,166,201,221,141,13,35,1,161,166,201,221,141,13,36,1,161,245,220,194,52,38,1,161,166,201,221,141,13,38,1,161,166,201,221,141,13,39,1,161,166,201,221,141,13,40,1,136,140,152,206,145,6,100,1,118,1,2,105,100,119,36,50,97,54,101,53,101,50,49,45,97,57,51,56,45,52,53,97,53,45,97,52,52,53,45,100,48,98,55,49,52,57,53,98,48,55,55,161,140,152,206,145,6,101,1,161,245,220,194,52,7,1,39,0,203,184,221,173,11,1,36,50,97,54,101,53,101,50,49,45,97,57,51,56,45,52,53,97,53,45,97,52,52,53,45,100,48,98,55,49,52,57,53,98,48,55,55,1,40,0,166,201,221,141,13,47,2,105,100,1,119,36,50,97,54,101,53,101,50,49,45,97,57,51,56,45,52,53,97,53,45,97,52,52,53,45,100,48,98,55,49,52,57,53,98,48,55,55,40,0,166,201,221,141,13,47,4,110,97,109,101,1,119,8,85,110,116,105,116,108,101,100,40,0,166,201,221,141,13,47,3,98,105,100,1,119,36,50,54,100,53,99,56,99,49,45,49,99,54,54,45,52,53,57,99,45,98,99,54,99,45,102,52,100,97,49,97,54,54,51,51,52,56,40,0,166,201,221,141,13,47,4,100,101,115,99,1,119,0,40,0,166,201,221,141,13,47,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,0,40,0,166,201,221,141,13,47,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,86,193,40,39,0,203,184,221,173,11,4,36,50,97,54,101,53,101,50,49,45,97,57,51,56,45,52,53,97,53,45,97,52,52,53,45,100,48,98,55,49,52,57,53,98,48,55,55,0,40,0,166,201,221,141,13,47,4,105,99,111,110,1,119,0,40,0,166,201,221,141,13,47,10,99,114,101,97,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,33,0,166,201,221,141,13,47,16,108,97,115,116,95,101,100,105,116,101,100,95,116,105,109,101,1,33,0,166,201,221,141,13,47,14,108,97,115,116,95,101,100,105,116,101,100,95,98,121,1,161,166,201,221,141,13,34,1,161,166,201,221,141,13,58,1,161,166,201,221,141,13,57,1,129,166,201,221,141,13,37,1,161,166,201,221,141,13,60,1,161,166,201,221,141,13,61,1,129,166,201,221,141,13,62,1,161,166,201,221,141,13,63,1,161,166,201,221,141,13,64,1,40,0,166,201,221,141,13,47,5,101,120,116,114,97,1,119,36,123,34,99,111,118,101,114,34,58,123,34,116,121,112,101,34,58,34,110,111,110,101,34,44,34,118,97,108,117,101,34,58,34,34,125,125,161,166,201,221,141,13,66,1,161,166,201,221,141,13,67,1,129,166,201,221,141,13,65,1,2,180,205,189,133,13,0,161,200,142,208,241,4,152,1,16,161,180,205,189,133,13,15,4,1,248,210,237,129,13,0,161,223,209,193,147,11,80,2,7,211,202,217,232,12,0,161,141,245,194,142,11,4,1,161,141,245,194,142,11,5,1,129,141,245,194,142,11,6,1,161,141,245,194,142,11,3,1,161,141,245,194,142,11,0,1,161,141,245,194,142,11,1,1,129,211,202,217,232,12,2,1,3,210,228,153,221,12,0,161,202,160,246,212,1,3,1,161,202,160,246,212,1,4,1,161,202,160,246,212,1,5,1,2,234,156,130,211,12,0,161,219,220,239,171,8,9,11,168,234,156,130,211,12,10,1,122,0,0,0,0,102,97,116,231,1,181,175,219,209,12,0,161,200,159,185,206,9,7,10,131,1,133,159,138,205,12,0,161,255,140,248,220,6,0,1,161,255,140,248,220,6,1,1,129,255,140,248,220,6,2,1,161,180,230,210,212,13,3,1,161,161,178,132,150,11,34,1,161,161,178,132,150,11,35,1,129,133,159,138,205,12,2,1,8,0,229,154,128,197,12,16,1,118,1,2,105,100,119,36,98,54,51,52,55,97,99,98,45,51,49,55,52,45,52,102,48,101,45,57,56,101,57,45,100,99,99,101,48,55,101,53,100,98,102,55,168,229,154,128,197,12,15,1,122,0,0,0,0,102,48,108,125,161,180,230,210,212,13,1,1,39,0,203,184,221,173,11,1,36,98,54,51,52,55,97,99,98,45,51,49,55,52,45,52,102,48,101,45,57,56,101,57,45,100,99,99,101,48,55,101,53,100,98,102,55,1,40,0,133,159,138,205,12,10,2,105,100,1,119,36,98,54,51,52,55,97,99,98,45,51,49,55,52,45,52,102,48,101,45,57,56,101,57,45,100,99,99,101,48,55,101,53,100,98,102,55,40,0,133,159,138,205,12,10,4,110,97,109,101,1,119,8,85,110,116,105,116,108,101,100,40,0,133,159,138,205,12,10,3,98,105,100,1,119,36,100,48,52,57,54,51,50,52,45,53,53,55,48,45,52,48,48,54,45,98,52,101,97,45,100,98,55,53,49,54,100,50,49,50,102,100,40,0,133,159,138,205,12,10,4,100,101,115,99,1,119,0,40,0,133,159,138,205,12,10,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,1,40,0,133,159,138,205,12,10,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,48,108,125,39,0,203,184,221,173,11,4,36,98,54,51,52,55,97,99,98,45,51,49,55,52,45,52,102,48,101,45,57,56,101,57,45,100,99,99,101,48,55,101,53,100,98,102,55,0,40,0,133,159,138,205,12,10,4,105,99,111,110,1,119,0,40,0,133,159,138,205,12,10,10,99,114,101,97,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,33,0,133,159,138,205,12,10,16,108,97,115,116,95,101,100,105,116,101,100,95,116,105,109,101,1,33,0,133,159,138,205,12,10,14,108,97,115,116,95,101,100,105,116,101,100,95,98,121,1,161,133,159,138,205,12,3,1,161,133,159,138,205,12,21,1,161,133,159,138,205,12,20,1,129,133,159,138,205,12,6,1,161,133,159,138,205,12,23,1,161,133,159,138,205,12,24,1,129,133,159,138,205,12,25,1,72,229,154,128,197,12,6,1,118,1,2,105,100,119,36,102,51,53,50,55,48,99,55,45,99,54,54,99,45,52,54,99,101,45,56,101,49,97,45,51,102,54,51,57,102,55,98,48,48,48,100,168,229,154,128,197,12,7,1,122,0,0,0,0,102,48,108,128,168,229,154,128,197,12,8,1,122,0,0,0,0,102,48,108,128,39,0,203,184,221,173,11,1,36,102,51,53,50,55,48,99,55,45,99,54,54,99,45,52,54,99,101,45,56,101,49,97,45,51,102,54,51,57,102,55,98,48,48,48,100,1,40,0,133,159,138,205,12,32,2,105,100,1,119,36,102,51,53,50,55,48,99,55,45,99,54,54,99,45,52,54,99,101,45,56,101,49,97,45,51,102,54,51,57,102,55,98,48,48,48,100,40,0,133,159,138,205,12,32,4,110,97,109,101,1,119,8,85,110,116,105,116,108,101,100,40,0,133,159,138,205,12,32,3,98,105,100,1,119,36,57,101,101,98,101,97,48,51,45,51,101,100,53,45,52,50,57,56,45,56,54,98,50,45,97,55,102,55,55,56,53,54,100,52,56,98,40,0,133,159,138,205,12,32,4,100,101,115,99,1,119,0,40,0,133,159,138,205,12,32,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,0,40,0,133,159,138,205,12,32,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,48,108,128,39,0,203,184,221,173,11,4,36,102,51,53,50,55,48,99,55,45,99,54,54,99,45,52,54,99,101,45,56,101,49,97,45,51,102,54,51,57,102,55,98,48,48,48,100,0,40,0,133,159,138,205,12,32,4,105,99,111,110,1,119,0,40,0,133,159,138,205,12,32,10,99,114,101,97,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,33,0,133,159,138,205,12,32,16,108,97,115,116,95,101,100,105,116,101,100,95,116,105,109,101,1,33,0,133,159,138,205,12,32,14,108,97,115,116,95,101,100,105,116,101,100,95,98,121,1,161,133,159,138,205,12,22,1,161,133,159,138,205,12,43,1,161,133,159,138,205,12,42,1,129,133,159,138,205,12,28,1,161,133,159,138,205,12,44,1,161,133,159,138,205,12,26,1,161,133,159,138,205,12,27,1,129,133,159,138,205,12,47,1,161,133,159,138,205,12,48,1,161,133,159,138,205,12,0,1,161,133,159,138,205,12,1,1,129,133,159,138,205,12,51,1,136,173,252,148,184,13,40,1,118,1,2,105,100,119,36,55,100,50,49,52,56,102,99,45,99,97,99,101,45,52,52,53,50,45,57,99,53,99,45,57,54,101,53,50,101,54,98,102,56,98,53,161,207,228,238,162,8,23,1,161,133,159,138,205,12,54,1,39,0,203,184,221,173,11,1,36,55,100,50,49,52,56,102,99,45,99,97,99,101,45,52,52,53,50,45,57,99,53,99,45,57,54,101,53,50,101,54,98,102,56,98,53,1,40,0,133,159,138,205,12,59,2,105,100,1,119,36,55,100,50,49,52,56,102,99,45,99,97,99,101,45,52,52,53,50,45,57,99,53,99,45,57,54,101,53,50,101,54,98,102,56,98,53,33,0,133,159,138,205,12,59,4,110,97,109,101,1,40,0,133,159,138,205,12,59,3,98,105,100,1,119,36,50,54,100,53,99,56,99,49,45,49,99,54,54,45,52,53,57,99,45,98,99,54,99,45,102,52,100,97,49,97,54,54,51,51,52,56,40,0,133,159,138,205,12,59,4,100,101,115,99,1,119,0,40,0,133,159,138,205,12,59,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,1,33,0,133,159,138,205,12,59,10,99,114,101,97,116,101,100,95,97,116,1,39,0,203,184,221,173,11,4,36,55,100,50,49,52,56,102,99,45,99,97,99,101,45,52,52,53,50,45,57,99,53,99,45,57,54,101,53,50,101,54,98,102,56,98,53,0,33,0,133,159,138,205,12,59,4,105,99,111,110,1,40,0,133,159,138,205,12,59,10,99,114,101,97,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,33,0,133,159,138,205,12,59,16,108,97,115,116,95,101,100,105,116,101,100,95,116,105,109,101,1,33,0,133,159,138,205,12,59,14,108,97,115,116,95,101,100,105,116,101,100,95,98,121,1,161,133,159,138,205,12,52,1,161,133,159,138,205,12,70,1,161,133,159,138,205,12,69,1,129,133,159,138,205,12,55,1,161,133,159,138,205,12,72,1,161,133,159,138,205,12,73,1,129,133,159,138,205,12,74,1,161,133,159,138,205,12,75,1,161,133,159,138,205,12,76,1,161,133,159,138,205,12,61,1,161,133,159,138,205,12,78,1,161,133,159,138,205,12,79,1,161,133,159,138,205,12,80,1,161,201,129,238,197,4,27,1,161,201,129,238,197,4,28,1,168,133,159,138,205,12,67,1,119,23,123,34,116,121,34,58,48,44,34,118,97,108,117,101,34,58,34,240,159,141,182,34,125,161,133,159,138,205,12,84,1,161,133,159,138,205,12,85,1,161,133,159,138,205,12,83,1,161,133,159,138,205,12,87,1,161,133,159,138,205,12,88,1,161,133,159,138,205,12,89,1,8,0,133,159,138,205,12,66,1,118,1,2,105,100,119,36,101,57,55,56,55,55,102,53,45,99,51,54,53,45,52,48,50,53,45,57,101,54,97,45,101,53,57,48,99,52,98,49,57,100,98,98,161,133,159,138,205,12,65,1,161,133,159,138,205,12,91,1,39,0,203,184,221,173,11,1,36,101,57,55,56,55,55,102,53,45,99,51,54,53,45,52,48,50,53,45,57,101,54,97,45,101,53,57,48,99,52,98,49,57,100,98,98,1,40,0,133,159,138,205,12,96,2,105,100,1,119,36,101,57,55,56,55,55,102,53,45,99,51,54,53,45,52,48,50,53,45,57,101,54,97,45,101,53,57,48,99,52,98,49,57,100,98,98,40,0,133,159,138,205,12,96,4,110,97,109,101,1,119,5,66,111,97,114,100,40,0,133,159,138,205,12,96,3,98,105,100,1,119,36,55,100,50,49,52,56,102,99,45,99,97,99,101,45,52,52,53,50,45,57,99,53,99,45,57,54,101,53,50,101,54,98,102,56,98,53,40,0,133,159,138,205,12,96,4,100,101,115,99,1,119,0,40,0,133,159,138,205,12,96,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,2,40,0,133,159,138,205,12,96,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,48,177,159,39,0,203,184,221,173,11,4,36,101,57,55,56,55,55,102,53,45,99,51,54,53,45,52,48,50,53,45,57,101,54,97,45,101,53,57,48,99,52,98,49,57,100,98,98,0,40,0,133,159,138,205,12,96,4,105,99,111,110,1,119,0,40,0,133,159,138,205,12,96,10,99,114,101,97,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,33,0,133,159,138,205,12,96,16,108,97,115,116,95,101,100,105,116,101,100,95,116,105,109,101,1,33,0,133,159,138,205,12,96,14,108,97,115,116,95,101,100,105,116,101,100,95,98,121,1,136,133,159,138,205,12,93,1,118,1,2,105,100,119,36,102,48,99,53,57,57,50,49,45,48,52,101,101,45,52,57,55,49,45,57,57,53,99,45,55,57,98,55,102,100,56,99,48,48,101,50,161,133,159,138,205,12,94,1,161,133,159,138,205,12,95,1,39,0,203,184,221,173,11,1,36,102,48,99,53,57,57,50,49,45,48,52,101,101,45,52,57,55,49,45,57,57,53,99,45,55,57,98,55,102,100,56,99,48,48,101,50,1,40,0,133,159,138,205,12,111,2,105,100,1,119,36,102,48,99,53,57,57,50,49,45,48,52,101,101,45,52,57,55,49,45,57,57,53,99,45,55,57,98,55,102,100,56,99,48,48,101,50,40,0,133,159,138,205,12,111,4,110,97,109,101,1,119,8,67,97,108,101,110,100,97,114,40,0,133,159,138,205,12,111,3,98,105,100,1,119,36,55,100,50,49,52,56,102,99,45,99,97,99,101,45,52,52,53,50,45,57,99,53,99,45,57,54,101,53,50,101,54,98,102,56,98,53,40,0,133,159,138,205,12,111,4,100,101,115,99,1,119,0,40,0,133,159,138,205,12,111,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,3,40,0,133,159,138,205,12,111,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,48,177,162,39,0,203,184,221,173,11,4,36,102,48,99,53,57,57,50,49,45,48,52,101,101,45,52,57,55,49,45,57,57,53,99,45,55,57,98,55,102,100,56,99,48,48,101,50,0,40,0,133,159,138,205,12,111,4,105,99,111,110,1,119,0,40,0,133,159,138,205,12,111,10,99,114,101,97,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,33,0,133,159,138,205,12,111,16,108,97,115,116,95,101,100,105,116,101,100,95,116,105,109,101,1,33,0,133,159,138,205,12,111,14,108,97,115,116,95,101,100,105,116,101,100,95,98,121,1,161,133,159,138,205,12,71,1,161,133,159,138,205,12,4,1,161,133,159,138,205,12,5,1,129,201,129,238,197,4,29,1,161,133,159,138,205,12,123,1,161,133,159,138,205,12,90,1,161,133,159,138,205,12,110,1,129,133,159,138,205,12,126,1,23,229,154,128,197,12,0,161,248,153,216,10,0,1,161,248,153,216,10,1,1,129,248,153,216,10,2,1,161,229,154,128,197,12,0,1,161,229,154,128,197,12,1,1,129,229,154,128,197,12,2,1,72,158,156,181,152,10,6,1,118,1,2,105,100,119,36,100,48,52,57,54,51,50,52,45,53,53,55,48,45,52,48,48,54,45,98,52,101,97,45,100,98,55,53,49,54,100,50,49,50,102,100,161,176,154,159,227,1,2,1,161,176,154,159,227,1,3,1,39,0,203,184,221,173,11,1,36,100,48,52,57,54,51,50,52,45,53,53,55,48,45,52,48,48,54,45,98,52,101,97,45,100,98,55,53,49,54,100,50,49,50,102,100,1,40,0,229,154,128,197,12,9,2,105,100,1,119,36,100,48,52,57,54,51,50,52,45,53,53,55,48,45,52,48,48,54,45,98,52,101,97,45,100,98,55,53,49,54,100,50,49,50,102,100,40,0,229,154,128,197,12,9,4,110,97,109,101,1,119,8,85,110,116,105,116,108,101,100,40,0,229,154,128,197,12,9,3,98,105,100,1,119,36,57,101,101,98,101,97,48,51,45,51,101,100,53,45,52,50,57,56,45,56,54,98,50,45,97,55,102,55,55,56,53,54,100,52,56,98,40,0,229,154,128,197,12,9,4,100,101,115,99,1,119,0,40,0,229,154,128,197,12,9,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,0,33,0,229,154,128,197,12,9,10,99,114,101,97,116,101,100,95,97,116,1,39,0,203,184,221,173,11,4,36,100,48,52,57,54,51,50,52,45,53,53,55,48,45,52,48,48,54,45,98,52,101,97,45,100,98,55,53,49,54,100,50,49,50,102,100,0,40,0,229,154,128,197,12,9,4,105,99,111,110,1,119,0,40,0,229,154,128,197,12,9,10,99,114,101,97,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,33,0,229,154,128,197,12,9,16,108,97,115,116,95,101,100,105,116,101,100,95,116,105,109,101,1,33,0,229,154,128,197,12,9,14,108,97,115,116,95,101,100,105,116,101,100,95,98,121,2,161,229,154,128,197,12,19,1,129,229,154,128,197,12,5,1,16,149,129,169,191,12,0,161,186,197,166,179,15,3,1,161,188,171,136,250,8,0,1,161,188,171,136,250,8,1,1,129,188,171,136,250,8,2,1,161,149,129,169,191,12,0,1,161,200,205,214,172,10,27,1,161,200,205,214,172,10,28,1,129,149,129,169,191,12,3,1,161,149,129,169,191,12,4,1,161,149,129,169,191,12,1,1,161,149,129,169,191,12,2,1,129,149,129,169,191,12,7,1,161,149,129,169,191,12,8,1,161,186,197,166,179,15,0,1,161,186,197,166,179,15,1,1,129,149,129,169,191,12,11,1,10,177,161,136,243,11,0,161,241,155,213,233,1,49,1,161,241,155,213,233,1,50,1,129,214,139,213,136,8,65,1,161,214,139,213,136,8,63,1,161,214,139,213,136,8,64,1,129,177,161,136,243,11,2,1,161,241,155,213,233,1,21,1,161,177,161,136,243,11,3,1,161,177,161,136,243,11,4,1,129,177,161,136,243,11,5,1,1,205,149,231,236,11,0,161,175,147,217,214,1,32,68,3,162,159,252,196,11,0,161,233,247,183,159,1,1,1,161,233,247,183,159,1,2,1,129,233,247,183,159,1,3,1,37,203,184,221,173,11,0,39,1,4,100,97,116,97,6,102,111,108,100,101,114,1,39,0,203,184,221,173,11,0,5,118,105,101,119,115,1,39,0,203,184,221,173,11,0,7,115,101,99,116,105,111,110,1,39,0,203,184,221,173,11,0,4,109,101,116,97,1,39,0,203,184,221,173,11,0,8,114,101,108,97,116,105,111,110,1,39,0,203,184,221,173,11,2,8,102,97,118,111,114,105,116,101,1,39,0,203,184,221,173,11,2,6,114,101,99,101,110,116,1,39,0,203,184,221,173,11,2,5,116,114,97,115,104,1,39,0,203,184,221,173,11,1,36,57,101,101,98,101,97,48,51,45,51,101,100,53,45,52,50,57,56,45,56,54,98,50,45,97,55,102,55,55,56,53,54,100,52,56,98,1,40,0,203,184,221,173,11,8,2,105,100,1,119,36,57,101,101,98,101,97,48,51,45,51,101,100,53,45,52,50,57,56,45,56,54,98,50,45,97,55,102,55,55,56,53,54,100,52,56,98,40,0,203,184,221,173,11,8,4,110,97,109,101,1,119,9,87,111,114,107,115,112,97,99,101,40,0,203,184,221,173,11,8,3,98,105,100,1,119,0,40,0,203,184,221,173,11,8,4,100,101,115,99,1,119,0,40,0,203,184,221,173,11,8,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,0,33,0,203,184,221,173,11,8,10,99,114,101,97,116,101,100,95,97,116,1,39,0,203,184,221,173,11,4,36,57,101,101,98,101,97,48,51,45,51,101,100,53,45,52,50,57,56,45,56,54,98,50,45,97,55,102,55,55,56,53,54,100,52,56,98,0,8,0,203,184,221,173,11,15,1,118,1,2,105,100,119,36,50,54,100,53,99,56,99,49,45,49,99,54,54,45,52,53,57,99,45,98,99,54,99,45,102,52,100,97,49,97,54,54,51,51,52,56,40,0,203,184,221,173,11,8,4,105,99,111,110,1,119,0,40,0,203,184,221,173,11,8,10,99,114,101,97,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,33,0,203,184,221,173,11,8,16,108,97,115,116,95,101,100,105,116,101,100,95,116,105,109,101,1,40,0,203,184,221,173,11,8,14,108,97,115,116,95,101,100,105,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,161,203,184,221,173,11,14,1,161,203,184,221,173,11,19,1,39,0,203,184,221,173,11,1,36,50,54,100,53,99,56,99,49,45,49,99,54,54,45,52,53,57,99,45,98,99,54,99,45,102,52,100,97,49,97,54,54,51,51,52,56,1,40,0,203,184,221,173,11,23,2,105,100,1,119,36,50,54,100,53,99,56,99,49,45,49,99,54,54,45,52,53,57,99,45,98,99,54,99,45,102,52,100,97,49,97,54,54,51,51,52,56,40,0,203,184,221,173,11,23,4,110,97,109,101,1,119,15,71,101,116,116,105,110,103,32,115,116,97,114,116,101,100,40,0,203,184,221,173,11,23,3,98,105,100,1,119,36,57,101,101,98,101,97,48,51,45,51,101,100,53,45,52,50,57,56,45,56,54,98,50,45,97,55,102,55,55,56,53,54,100,52,56,98,40,0,203,184,221,173,11,23,4,100,101,115,99,1,119,0,40,0,203,184,221,173,11,23,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,0,33,0,203,184,221,173,11,23,10,99,114,101,97,116,101,100,95,97,116,1,39,0,203,184,221,173,11,4,36,50,54,100,53,99,56,99,49,45,49,99,54,54,45,52,53,57,99,45,98,99,54,99,45,102,52,100,97,49,97,54,54,51,51,52,56,0,40,0,203,184,221,173,11,23,4,105,99,111,110,1,119,25,123,34,116,121,34,58,48,44,34,118,97,108,117,101,34,58,34,226,173,144,239,184,143,34,125,40,0,203,184,221,173,11,23,10,99,114,101,97,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,33,0,203,184,221,173,11,23,16,108,97,115,116,95,101,100,105,116,101,100,95,116,105,109,101,1,33,0,203,184,221,173,11,23,14,108,97,115,116,95,101,100,105,116,101,100,95,98,121,1,40,0,203,184,221,173,11,3,17,99,117,114,114,101,110,116,95,119,111,114,107,115,112,97,99,101,1,119,36,57,101,101,98,101,97,48,51,45,51,101,100,53,45,52,50,57,56,45,56,54,98,50,45,97,55,102,55,55,56,53,54,100,52,56,98,33,0,203,184,221,173,11,3,12,99,117,114,114,101,110,116,95,118,105,101,119,1,3,155,165,205,152,11,0,161,179,252,154,44,0,1,161,179,252,154,44,1,1,129,179,252,154,44,2,1,101,161,178,132,150,11,0,161,167,131,133,162,9,8,1,161,167,131,133,162,9,9,1,129,167,131,133,162,9,10,1,161,167,131,133,162,9,7,1,161,252,218,241,167,14,0,1,161,252,218,241,167,14,1,1,129,161,178,132,150,11,2,1,161,161,178,132,150,11,3,1,161,161,178,132,150,11,0,1,161,161,178,132,150,11,1,1,129,161,178,132,150,11,6,1,161,161,178,132,150,11,7,1,161,130,180,254,251,6,15,1,161,130,180,254,251,6,16,1,129,161,178,132,150,11,10,1,8,0,130,180,254,251,6,10,1,118,1,2,105,100,119,36,52,56,99,53,50,99,102,55,45,98,102,57,56,45,52,51,102,97,45,57,54,97,100,45,98,51,49,97,97,100,101,57,98,48,55,49,168,130,180,254,251,6,9,1,122,0,0,0,0,102,32,220,196,161,161,178,132,150,11,13,1,39,0,203,184,221,173,11,1,36,52,56,99,53,50,99,102,55,45,98,102,57,56,45,52,51,102,97,45,57,54,97,100,45,98,51,49,97,97,100,101,57,98,48,55,49,1,40,0,161,178,132,150,11,18,2,105,100,1,119,36,52,56,99,53,50,99,102,55,45,98,102,57,56,45,52,51,102,97,45,57,54,97,100,45,98,51,49,97,97,100,101,57,98,48,55,49,40,0,161,178,132,150,11,18,4,110,97,109,101,1,119,8,85,110,116,105,116,108,101,100,40,0,161,178,132,150,11,18,3,98,105,100,1,119,36,100,100,98,57,51,98,97,55,45,48,54,99,55,45,52,49,55,54,45,57,56,50,97,45,100,55,52,50,51,101,48,57,98,52,52,49,40,0,161,178,132,150,11,18,4,100,101,115,99,1,119,0,40,0,161,178,132,150,11,18,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,1,40,0,161,178,132,150,11,18,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,32,220,196,39,0,203,184,221,173,11,4,36,52,56,99,53,50,99,102,55,45,98,102,57,56,45,52,51,102,97,45,57,54,97,100,45,98,51,49,97,97,100,101,57,98,48,55,49,0,40,0,161,178,132,150,11,18,4,105,99,111,110,1,119,0,40,0,161,178,132,150,11,18,10,99,114,101,97,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,33,0,161,178,132,150,11,18,16,108,97,115,116,95,101,100,105,116,101,100,95,116,105,109,101,1,33,0,161,178,132,150,11,18,14,108,97,115,116,95,101,100,105,116,101,100,95,98,121,1,161,161,178,132,150,11,11,1,161,161,178,132,150,11,29,1,161,161,178,132,150,11,28,1,129,161,178,132,150,11,14,1,161,161,178,132,150,11,31,1,161,161,178,132,150,11,32,1,129,161,178,132,150,11,33,1,161,161,178,132,150,11,30,1,161,161,178,132,150,11,8,1,161,161,178,132,150,11,9,1,129,161,178,132,150,11,36,1,161,161,178,132,150,11,37,1,161,167,131,133,162,9,4,1,161,167,131,133,162,9,5,1,129,161,178,132,150,11,40,1,161,161,178,132,150,11,41,1,161,161,178,132,150,11,38,1,161,161,178,132,150,11,39,1,129,161,178,132,150,11,44,1,161,161,178,132,150,11,45,1,161,161,178,132,150,11,42,1,161,161,178,132,150,11,43,1,129,161,178,132,150,11,48,1,161,161,178,132,150,11,49,1,161,161,178,132,150,11,46,1,161,161,178,132,150,11,47,1,129,161,178,132,150,11,52,1,161,161,178,132,150,11,53,1,161,161,178,132,150,11,4,1,161,161,178,132,150,11,5,1,129,161,178,132,150,11,56,1,161,161,178,132,150,11,57,1,161,161,178,132,150,11,54,1,161,161,178,132,150,11,55,1,129,161,178,132,150,11,60,1,161,161,178,132,150,11,61,1,161,161,178,132,150,11,50,1,161,161,178,132,150,11,51,1,129,161,178,132,150,11,64,1,161,161,178,132,150,11,65,1,161,161,178,132,150,11,62,1,161,161,178,132,150,11,63,1,129,161,178,132,150,11,68,1,161,161,178,132,150,11,69,1,161,161,178,132,150,11,66,1,161,161,178,132,150,11,67,1,129,161,178,132,150,11,72,1,161,161,178,132,150,11,73,1,161,161,178,132,150,11,70,1,161,161,178,132,150,11,71,1,129,161,178,132,150,11,76,1,161,161,178,132,150,11,77,1,161,161,178,132,150,11,74,1,161,161,178,132,150,11,75,1,129,161,178,132,150,11,80,1,161,161,178,132,150,11,81,1,161,161,178,132,150,11,78,1,161,161,178,132,150,11,79,1,129,161,178,132,150,11,84,1,161,161,178,132,150,11,85,1,161,161,178,132,150,11,82,1,161,161,178,132,150,11,83,1,136,161,178,132,150,11,88,1,118,2,2,105,100,119,36,99,97,49,50,50,99,48,52,45,100,55,98,51,45,52,102,55,48,45,57,57,53,49,45,57,54,98,102,100,97,57,98,54,98,50,52,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,33,243,102,161,161,178,132,150,11,89,1,161,161,178,132,150,11,86,1,161,161,178,132,150,11,87,1,129,161,178,132,150,11,92,1,161,161,178,132,150,11,93,1,161,167,131,133,162,9,0,1,161,167,131,133,162,9,1,1,129,161,178,132,150,11,96,1,1,223,209,193,147,11,0,161,213,255,156,145,1,1,81,7,141,245,194,142,11,0,161,158,182,250,251,9,12,1,161,158,182,250,251,9,13,1,129,158,182,250,251,9,14,1,161,158,182,250,251,9,11,1,161,158,182,250,251,9,8,1,161,158,182,250,251,9,9,1,129,141,245,194,142,11,2,1,3,253,205,145,137,11,0,161,180,205,189,133,13,15,1,161,180,205,189,133,13,19,5,161,253,205,145,137,11,5,4,3,142,130,192,134,11,0,161,207,228,238,162,8,47,1,161,207,228,238,162,8,48,1,161,207,228,238,162,8,46,1,1,139,152,215,249,10,0,161,231,189,134,196,8,76,34,1,255,255,147,249,10,0,161,182,143,233,195,4,110,3,2,152,158,185,230,10,0,39,0,203,184,221,173,11,7,18,51,48,52,49,50,48,49,48,57,48,55,49,51,51,57,53,50,48,0,8,0,152,158,185,230,10,0,6,118,2,2,105,100,119,36,101,48,102,101,54,56,54,55,45,50,48,56,102,45,52,51,57,57,45,97,56,56,97,45,101,57,98,97,102,52,56,97,99,48,53,101,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,101,241,153,125,118,2,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,101,241,153,129,2,105,100,119,36,53,54,54,56,57,101,52,50,45,49,102,101,56,45,52,97,97,102,45,56,50,99,53,45,99,51,100,99,98,102,99,51,98,50,53,52,118,2,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,101,241,153,131,2,105,100,119,36,51,53,100,51,57,57,98,57,45,49,57,55,101,45,52,57,99,54,45,97,98,102,56,45,101,51,57,51,49,101,48,97,55,51,49,51,118,2,2,105,100,119,36,50,57,55,48,52,49,52,101,45,99,99,50,48,45,52,51,50,102,45,97,100,49,54,45,52,50,101,99,55,49,55,51,54,100,53,57,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,101,241,153,133,118,2,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,101,241,153,136,2,105,100,119,36,102,57,101,48,48,54,53,49,45,49,53,48,101,45,52,50,56,56,45,56,48,55,57,45,50,50,100,48,54,97,50,55,54,97,55,49,118,2,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,101,241,153,140,2,105,100,119,36,56,54,54,57,52,102,97,100,45,54,55,52,97,45,52,54,55,52,45,56,52,100,48,45,51,50,99,52,56,54,97,55,55,48,98,52,1,145,190,137,224,10,0,161,155,159,180,195,15,5,2,3,178,162,190,217,10,0,161,184,201,188,172,10,0,1,161,184,201,188,172,10,1,1,161,184,201,188,172,10,2,1,3,197,254,154,201,10,0,161,244,226,228,149,2,1,1,161,244,226,228,149,2,2,1,129,244,226,228,149,2,29,1,1,248,196,187,185,10,0,161,252,171,209,175,15,3,31,1,135,240,136,178,10,0,161,184,231,170,67,5,45,30,200,205,214,172,10,0,161,161,178,132,150,11,12,1,161,161,178,132,150,11,17,1,129,186,197,166,179,15,6,1,161,200,205,214,172,10,0,1,161,200,205,214,172,10,1,1,129,200,205,214,172,10,2,1,161,186,197,166,179,15,4,1,161,186,197,166,179,15,5,1,129,200,205,214,172,10,5,1,161,200,205,214,172,10,3,1,161,200,205,214,172,10,4,1,129,200,205,214,172,10,8,1,161,200,205,214,172,10,9,1,161,200,205,214,172,10,10,1,129,200,205,214,172,10,11,1,161,200,205,214,172,10,6,1,161,200,205,214,172,10,7,1,129,200,205,214,172,10,14,1,161,200,205,214,172,10,12,1,161,200,205,214,172,10,13,1,129,200,205,214,172,10,17,1,161,200,205,214,172,10,15,1,161,200,205,214,172,10,16,1,129,200,205,214,172,10,20,1,161,200,205,214,172,10,18,1,161,200,205,214,172,10,19,1,129,200,205,214,172,10,23,1,161,200,205,214,172,10,21,1,161,200,205,214,172,10,22,1,129,200,205,214,172,10,26,1,3,184,201,188,172,10,0,161,142,130,192,134,11,0,1,161,142,130,192,134,11,1,1,161,142,130,192,134,11,2,1,16,156,148,170,169,10,0,161,211,202,217,232,12,3,1,161,211,202,217,232,12,0,1,161,211,202,217,232,12,1,1,129,211,202,217,232,12,6,1,161,156,148,170,169,10,0,1,161,252,218,241,167,14,0,1,161,252,218,241,167,14,1,1,129,156,148,170,169,10,3,1,161,156,148,170,169,10,4,1,161,156,148,170,169,10,1,1,161,156,148,170,169,10,2,1,129,156,148,170,169,10,7,1,161,156,148,170,169,10,8,1,161,167,131,133,162,9,0,1,161,167,131,133,162,9,1,1,129,156,148,170,169,10,11,1,48,158,156,181,152,10,0,161,206,220,129,131,4,18,1,161,206,220,129,131,4,19,1,129,206,220,129,131,4,20,1,161,206,220,129,131,4,0,1,161,206,220,129,131,4,1,1,129,158,156,181,152,10,2,1,72,206,220,129,131,4,3,1,118,1,2,105,100,119,36,100,49,52,50,51,57,101,57,45,50,98,102,55,45,52,56,50,52,45,57,51,101,99,45,52,102,51,99,99,53,54,49,54,55,50,48,161,206,220,129,131,4,4,1,161,206,220,129,131,4,5,1,39,0,203,184,221,173,11,1,36,100,49,52,50,51,57,101,57,45,50,98,102,55,45,52,56,50,52,45,57,51,101,99,45,52,102,51,99,99,53,54,49,54,55,50,48,1,40,0,158,156,181,152,10,9,2,105,100,1,119,36,100,49,52,50,51,57,101,57,45,50,98,102,55,45,52,56,50,52,45,57,51,101,99,45,52,102,51,99,99,53,54,49,54,55,50,48,40,0,158,156,181,152,10,9,4,110,97,109,101,1,119,8,85,110,116,105,116,108,101,100,40,0,158,156,181,152,10,9,3,98,105,100,1,119,36,57,101,101,98,101,97,48,51,45,51,101,100,53,45,52,50,57,56,45,56,54,98,50,45,97,55,102,55,55,56,53,54,100,52,56,98,40,0,158,156,181,152,10,9,4,100,101,115,99,1,119,0,40,0,158,156,181,152,10,9,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,0,33,0,158,156,181,152,10,9,10,99,114,101,97,116,101,100,95,97,116,1,39,0,203,184,221,173,11,4,36,100,49,52,50,51,57,101,57,45,50,98,102,55,45,52,56,50,52,45,57,51,101,99,45,52,102,51,99,99,53,54,49,54,55,50,48,0,40,0,158,156,181,152,10,9,4,105,99,111,110,1,119,0,40,0,158,156,181,152,10,9,10,99,114,101,97,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,33,0,158,156,181,152,10,9,16,108,97,115,116,95,101,100,105,116,101,100,95,116,105,109,101,1,33,0,158,156,181,152,10,9,14,108,97,115,116,95,101,100,105,116,101,100,95,98,121,2,161,158,156,181,152,10,19,1,129,158,156,181,152,10,5,1,161,233,165,139,246,14,4,1,161,233,165,139,246,14,5,1,129,158,156,181,152,10,23,1,8,0,203,184,221,173,11,30,1,118,1,2,105,100,119,36,98,53,56,48,55,51,52,53,45,53,100,97,55,45,52,53,98,100,45,97,56,97,101,45,101,97,53,54,56,99,55,99,57,49,49,97,161,203,184,221,173,11,29,1,161,158,156,181,152,10,25,1,39,0,203,184,221,173,11,1,36,98,53,56,48,55,51,52,53,45,53,100,97,55,45,52,53,98,100,45,97,56,97,101,45,101,97,53,54,56,99,55,99,57,49,49,97,1,40,0,158,156,181,152,10,30,2,105,100,1,119,36,98,53,56,48,55,51,52,53,45,53,100,97,55,45,52,53,98,100,45,97,56,97,101,45,101,97,53,54,56,99,55,99,57,49,49,97,33,0,158,156,181,152,10,30,4,110,97,109,101,1,40,0,158,156,181,152,10,30,3,98,105,100,1,119,36,50,54,100,53,99,56,99,49,45,49,99,54,54,45,52,53,57,99,45,98,99,54,99,45,102,52,100,97,49,97,54,54,51,51,52,56,40,0,158,156,181,152,10,30,4,100,101,115,99,1,119,0,40,0,158,156,181,152,10,30,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,0,40,0,158,156,181,152,10,30,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,101,241,129,30,39,0,203,184,221,173,11,4,36,98,53,56,48,55,51,52,53,45,53,100,97,55,45,52,53,98,100,45,97,56,97,101,45,101,97,53,54,56,99,55,99,57,49,49,97,0,40,0,158,156,181,152,10,30,4,105,99,111,110,1,119,0,40,0,158,156,181,152,10,30,10,99,114,101,97,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,33,0,158,156,181,152,10,30,16,108,97,115,116,95,101,100,105,116,101,100,95,116,105,109,101,1,33,0,158,156,181,152,10,30,14,108,97,115,116,95,101,100,105,116,101,100,95,98,121,1,161,203,184,221,173,11,36,1,161,158,156,181,152,10,41,1,161,158,156,181,152,10,40,1,129,158,156,181,152,10,26,1,161,158,156,181,152,10,43,1,161,158,156,181,152,10,44,1,129,158,156,181,152,10,45,1,9,154,193,208,134,10,0,161,229,154,128,197,12,3,1,161,229,154,128,197,12,4,1,129,162,159,252,196,11,2,1,161,154,193,208,134,10,0,1,161,154,193,208,134,10,1,1,129,154,193,208,134,10,2,1,161,154,193,208,134,10,3,1,161,154,193,208,134,10,4,1,129,154,193,208,134,10,5,1,3,235,225,184,133,10,0,161,233,165,139,246,14,40,1,161,233,165,139,246,14,41,1,129,233,165,139,246,14,42,1,15,158,182,250,251,9,0,161,229,154,128,197,12,3,1,161,229,154,128,197,12,4,1,129,162,159,252,196,11,2,1,161,233,247,183,159,1,0,1,161,158,156,181,152,10,3,1,161,158,156,181,152,10,4,1,129,158,182,250,251,9,2,1,161,158,182,250,251,9,3,1,161,158,182,250,251,9,0,1,161,158,182,250,251,9,1,1,129,158,182,250,251,9,6,1,161,158,182,250,251,9,7,1,161,171,204,155,217,8,0,1,161,171,204,155,217,8,1,1,129,158,182,250,251,9,10,1,31,170,140,240,234,9,0,161,201,191,159,147,14,14,1,161,201,191,159,147,14,15,1,129,201,191,159,147,14,16,1,161,201,191,159,147,14,13,1,161,201,191,159,147,14,7,1,161,201,191,159,147,14,8,1,129,170,140,240,234,9,2,1,161,170,140,240,234,9,4,1,161,170,140,240,234,9,5,1,129,170,140,240,234,9,6,1,161,170,140,240,234,9,0,1,161,170,140,240,234,9,1,1,129,170,140,240,234,9,9,1,161,170,140,240,234,9,3,1,161,170,140,240,234,9,10,1,161,170,140,240,234,9,11,1,129,170,140,240,234,9,12,1,161,201,129,238,197,4,56,1,161,201,129,238,197,4,55,1,129,170,140,240,234,9,16,1,161,170,140,240,234,9,13,1,161,170,140,240,234,9,17,1,161,170,140,240,234,9,18,1,129,170,140,240,234,9,19,1,161,170,140,240,234,9,14,1,161,170,140,240,234,9,15,1,129,170,140,240,234,9,23,1,161,170,140,240,234,9,20,1,161,170,140,240,234,9,24,1,161,170,140,240,234,9,25,1,129,170,140,240,234,9,26,1,1,191,157,147,233,9,0,161,190,139,191,155,1,1,32,1,200,159,185,206,9,0,161,231,139,244,188,8,12,8,15,135,232,133,203,9,0,161,248,136,168,181,1,0,1,161,248,136,168,181,1,1,1,129,248,136,168,181,1,2,1,161,135,232,133,203,9,0,1,161,135,232,133,203,9,1,1,129,135,232,133,203,9,2,1,161,135,232,133,203,9,3,1,161,135,232,133,203,9,4,1,129,135,232,133,203,9,5,1,161,135,232,133,203,9,6,1,161,135,232,133,203,9,7,1,129,135,232,133,203,9,8,1,161,135,232,133,203,9,9,1,161,135,232,133,203,9,10,1,129,135,232,133,203,9,11,1,11,167,131,133,162,9,0,161,211,202,217,232,12,4,1,161,211,202,217,232,12,5,1,129,141,205,220,149,4,2,1,161,211,202,217,232,12,3,1,161,158,182,250,251,9,4,1,161,158,182,250,251,9,5,1,129,167,131,133,162,9,2,1,161,167,131,133,162,9,3,1,161,214,168,149,214,3,8,1,161,214,168,149,214,3,9,1,129,167,131,133,162,9,6,1,1,234,182,182,157,9,0,161,161,239,241,154,13,105,8,21,188,171,136,250,8,0,161,200,205,214,172,10,24,1,161,200,205,214,172,10,25,1,129,200,205,214,172,10,29,1,161,188,171,136,250,8,0,1,161,188,171,136,250,8,1,1,129,188,171,136,250,8,2,1,161,164,155,139,169,7,4,1,161,164,155,139,169,7,5,1,129,164,155,139,169,7,6,1,161,164,155,139,169,7,12,1,161,164,155,139,169,7,13,1,129,164,155,139,169,7,14,1,161,188,171,136,250,8,9,1,161,188,171,136,250,8,10,1,129,188,171,136,250,8,11,1,161,188,171,136,250,8,12,1,161,188,171,136,250,8,13,1,129,188,171,136,250,8,14,1,161,164,155,139,169,7,16,1,161,164,155,139,169,7,17,1,129,164,155,139,169,7,18,1,70,187,220,199,239,8,0,168,137,164,190,210,1,36,1,122,4,56,115,160,190,64,16,0,161,137,164,190,210,1,37,1,136,137,164,190,210,1,38,1,118,2,2,105,100,119,36,50,54,100,53,99,56,99,49,45,49,99,54,54,45,52,53,57,99,45,98,99,54,99,45,102,52,100,97,49,97,54,54,51,51,52,56,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,97,115,51,161,137,164,190,210,1,29,1,161,137,164,190,210,1,30,1,136,187,220,199,239,8,2,1,118,2,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,97,115,57,2,105,100,119,36,55,100,50,49,52,56,102,99,45,99,97,99,101,45,52,52,53,50,45,57,99,53,99,45,57,54,101,53,50,101,54,98,102,56,98,53,161,137,164,190,210,1,35,1,168,187,220,199,239,8,3,1,122,4,56,115,160,190,64,16,0,168,187,220,199,239,8,4,1,122,0,0,0,0,102,97,115,57,136,187,220,199,239,8,5,1,118,2,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,97,115,57,2,105,100,119,36,55,100,50,49,52,56,102,99,45,99,97,99,101,45,52,52,53,50,45,57,99,53,99,45,57,54,101,53,50,101,54,98,102,56,98,53,136,247,200,243,247,14,0,1,118,1,2,105,100,119,36,55,102,50,51,51,98,101,52,45,49,98,52,100,45,52,54,98,50,45,98,99,102,99,45,102,51,52,49,98,56,100,55,53,50,54,55,168,247,200,243,247,14,1,1,122,0,0,0,0,102,97,115,63,168,187,220,199,239,8,1,1,122,0,0,0,0,102,97,115,63,39,0,203,184,221,173,11,1,36,55,102,50,51,51,98,101,52,45,49,98,52,100,45,52,54,98,50,45,98,99,102,99,45,102,51,52,49,98,56,100,55,53,50,54,55,1,40,0,187,220,199,239,8,13,2,105,100,1,119,36,55,102,50,51,51,98,101,52,45,49,98,52,100,45,52,54,98,50,45,98,99,102,99,45,102,51,52,49,98,56,100,55,53,50,54,55,33,0,187,220,199,239,8,13,4,110,97,109,101,1,40,0,187,220,199,239,8,13,3,98,105,100,1,119,36,50,54,100,53,99,56,99,49,45,49,99,54,54,45,52,53,57,99,45,98,99,54,99,45,102,52,100,97,49,97,54,54,51,51,52,56,40,0,187,220,199,239,8,13,4,100,101,115,99,1,119,0,40,0,187,220,199,239,8,13,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,1,33,0,187,220,199,239,8,13,10,99,114,101,97,116,101,100,95,97,116,1,39,0,203,184,221,173,11,4,36,55,102,50,51,51,98,101,52,45,49,98,52,100,45,52,54,98,50,45,98,99,102,99,45,102,51,52,49,98,56,100,55,53,50,54,55,0,40,0,187,220,199,239,8,13,4,105,99,111,110,1,119,0,40,0,187,220,199,239,8,13,10,99,114,101,97,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,33,0,187,220,199,239,8,13,16,108,97,115,116,95,101,100,105,116,101,100,95,116,105,109,101,1,33,0,187,220,199,239,8,13,14,108,97,115,116,95,101,100,105,116,101,100,95,98,121,1,161,187,220,199,239,8,6,1,161,187,220,199,239,8,24,1,161,187,220,199,239,8,23,1,129,187,220,199,239,8,9,1,161,187,220,199,239,8,26,1,161,187,220,199,239,8,27,1,129,187,220,199,239,8,28,1,161,187,220,199,239,8,29,1,161,187,220,199,239,8,30,1,129,187,220,199,239,8,31,1,161,187,220,199,239,8,32,1,161,187,220,199,239,8,33,1,161,187,220,199,239,8,15,1,161,187,220,199,239,8,35,1,161,187,220,199,239,8,36,1,168,187,220,199,239,8,37,1,119,10,70,105,108,116,101,114,71,114,105,100,8,0,187,220,199,239,8,20,1,118,1,2,105,100,119,36,97,55,51,52,97,48,54,56,45,101,55,51,100,45,52,98,52,98,45,56,53,51,99,45,52,100,97,102,102,101,97,51,56,57,99,48,168,187,220,199,239,8,19,1,122,0,0,0,0,102,97,115,248,161,187,220,199,239,8,39,1,39,0,203,184,221,173,11,1,36,97,55,51,52,97,48,54,56,45,101,55,51,100,45,52,98,52,98,45,56,53,51,99,45,52,100,97,102,102,101,97,51,56,57,99,48,1,40,0,187,220,199,239,8,44,2,105,100,1,119,36,97,55,51,52,97,48,54,56,45,101,55,51,100,45,52,98,52,98,45,56,53,51,99,45,52,100,97,102,102,101,97,51,56,57,99,48,40,0,187,220,199,239,8,44,4,110,97,109,101,1,119,4,71,114,105,100,40,0,187,220,199,239,8,44,3,98,105,100,1,119,36,55,102,50,51,51,98,101,52,45,49,98,52,100,45,52,54,98,50,45,98,99,102,99,45,102,51,52,49,98,56,100,55,53,50,54,55,40,0,187,220,199,239,8,44,4,100,101,115,99,1,119,0,40,0,187,220,199,239,8,44,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,1,40,0,187,220,199,239,8,44,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,97,115,248,39,0,203,184,221,173,11,4,36,97,55,51,52,97,48,54,56,45,101,55,51,100,45,52,98,52,98,45,56,53,51,99,45,52,100,97,102,102,101,97,51,56,57,99,48,0,40,0,187,220,199,239,8,44,4,105,99,111,110,1,119,0,40,0,187,220,199,239,8,44,10,99,114,101,97,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,40,0,187,220,199,239,8,44,16,108,97,115,116,95,101,100,105,116,101,100,95,116,105,109,101,1,122,0,0,0,0,102,97,115,248,40,0,187,220,199,239,8,44,14,108,97,115,116,95,101,100,105,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,161,247,200,243,247,14,55,1,161,247,200,243,247,14,56,1,129,187,220,199,239,8,34,1,161,187,220,199,239,8,25,1,168,187,220,199,239,8,56,1,122,4,56,115,160,190,64,16,0,168,187,220,199,239,8,57,1,122,0,0,0,0,102,97,116,215,136,187,220,199,239,8,58,1,118,2,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,97,116,215,2,105,100,119,36,55,57,100,48,54,51,50,100,45,97,53,97,56,45,52,53,52,48,45,97,100,99,100,45,54,101,101,57,50,50,100,56,54,55,101,100,161,187,220,199,239,8,38,1,161,187,220,199,239,8,43,1,129,187,220,199,239,8,62,1,168,187,220,199,239,8,59,1,119,36,55,102,50,51,51,98,101,52,45,49,98,52,100,45,52,54,98,50,45,98,99,102,99,45,102,51,52,49,98,56,100,55,53,50,54,55,168,187,220,199,239,8,63,1,122,4,56,115,160,190,64,16,0,168,187,220,199,239,8,64,1,122,0,0,0,0,102,97,116,215,136,187,220,199,239,8,65,1,118,2,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,97,116,215,2,105,100,119,36,55,102,50,51,51,98,101,52,45,49,98,52,100,45,52,54,98,50,45,98,99,102,99,45,102,51,52,49,98,56,100,55,53,50,54,55,1,236,229,225,232,8,0,161,219,227,140,137,6,33,116,3,171,204,155,217,8,0,161,229,154,128,197,12,21,1,161,229,154,128,197,12,22,1,129,229,154,128,197,12,23,1,3,149,189,189,215,8,0,161,160,192,253,131,5,0,1,161,160,192,253,131,5,1,1,129,160,192,253,131,5,2,1,1,231,189,134,196,8,0,161,153,130,203,161,6,96,77,3,195,242,227,194,8,0,161,155,165,205,152,11,0,1,161,155,165,205,152,11,1,1,129,155,165,205,152,11,2,1,1,231,139,244,188,8,0,161,128,252,161,128,4,32,13,1,219,220,239,171,8,0,161,248,196,187,185,10,30,10,198,1,154,244,246,165,8,0,161,174,151,139,93,176,2,1,161,174,151,139,93,177,2,1,129,174,151,139,93,178,2,1,161,174,151,139,93,140,2,1,161,174,151,139,93,141,2,1,129,154,244,246,165,8,2,1,161,174,151,139,93,175,2,1,161,154,244,246,165,8,3,1,161,154,244,246,165,8,4,1,129,154,244,246,165,8,5,1,161,154,244,246,165,8,0,1,161,154,244,246,165,8,1,1,129,154,244,246,165,8,9,1,161,154,244,246,165,8,6,1,161,154,244,246,165,8,10,1,161,154,244,246,165,8,11,1,129,154,244,246,165,8,12,1,161,154,244,246,165,8,7,1,161,154,244,246,165,8,8,1,129,154,244,246,165,8,16,1,161,154,244,246,165,8,13,1,161,154,244,246,165,8,17,1,161,154,244,246,165,8,18,1,129,154,244,246,165,8,19,1,136,213,161,242,209,13,3,1,118,1,2,105,100,119,36,49,51,53,54,49,53,102,97,45,54,54,102,55,45,52,52,53,49,45,57,98,53,52,45,100,55,101,57,57,52,52,53,102,99,97,52,168,213,161,242,209,13,4,1,122,0,0,0,0,102,79,7,13,161,154,244,246,165,8,15,1,39,0,203,184,221,173,11,1,36,49,51,53,54,49,53,102,97,45,54,54,102,55,45,52,52,53,49,45,57,98,53,52,45,100,55,101,57,57,52,52,53,102,99,97,52,1,40,0,154,244,246,165,8,27,2,105,100,1,119,36,49,51,53,54,49,53,102,97,45,54,54,102,55,45,52,52,53,49,45,57,98,53,52,45,100,55,101,57,57,52,52,53,102,99,97,52,40,0,154,244,246,165,8,27,4,110,97,109,101,1,119,12,86,105,101,119,32,111,102,32,71,114,105,100,40,0,154,244,246,165,8,27,3,98,105,100,1,119,36,55,100,50,49,52,56,102,99,45,99,97,99,101,45,52,52,53,50,45,57,99,53,99,45,57,54,101,53,50,101,54,98,102,56,98,53,40,0,154,244,246,165,8,27,4,100,101,115,99,1,119,0,40,0,154,244,246,165,8,27,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,1,40,0,154,244,246,165,8,27,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,79,7,13,39,0,203,184,221,173,11,4,36,49,51,53,54,49,53,102,97,45,54,54,102,55,45,52,52,53,49,45,57,98,53,52,45,100,55,101,57,57,52,52,53,102,99,97,52,0,40,0,154,244,246,165,8,27,4,105,99,111,110,1,119,0,40,0,154,244,246,165,8,27,10,99,114,101,97,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,33,0,154,244,246,165,8,27,16,108,97,115,116,95,101,100,105,116,101,100,95,116,105,109,101,1,33,0,154,244,246,165,8,27,14,108,97,115,116,95,101,100,105,116,101,100,95,98,121,1,8,0,165,139,157,171,15,10,1,118,1,2,105,100,119,36,98,52,101,55,55,50,48,51,45,53,99,56,98,45,52,56,100,102,45,98,98,99,53,45,50,101,49,49,52,51,101,98,48,101,54,49,168,165,139,157,171,15,9,1,122,0,0,0,0,102,79,7,25,168,174,151,139,93,170,2,1,122,0,0,0,0,102,79,7,25,39,0,203,184,221,173,11,1,36,98,52,101,55,55,50,48,51,45,53,99,56,98,45,52,56,100,102,45,98,98,99,53,45,50,101,49,49,52,51,101,98,48,101,54,49,1,40,0,154,244,246,165,8,42,2,105,100,1,119,36,98,52,101,55,55,50,48,51,45,53,99,56,98,45,52,56,100,102,45,98,98,99,53,45,50,101,49,49,52,51,101,98,48,101,54,49,40,0,154,244,246,165,8,42,4,110,97,109,101,1,119,22,86,105,101,119,32,111,102,32,66,111,97,114,100,32,99,104,101,99,107,98,111,120,40,0,154,244,246,165,8,42,3,98,105,100,1,119,36,97,53,53,54,54,101,52,57,45,102,49,53,54,45,52,49,54,56,45,57,98,50,100,45,49,55,57,50,54,99,53,100,97,51,50,57,40,0,154,244,246,165,8,42,4,100,101,115,99,1,119,0,40,0,154,244,246,165,8,42,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,2,40,0,154,244,246,165,8,42,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,79,7,25,39,0,203,184,221,173,11,4,36,98,52,101,55,55,50,48,51,45,53,99,56,98,45,52,56,100,102,45,98,98,99,53,45,50,101,49,49,52,51,101,98,48,101,54,49,0,40,0,154,244,246,165,8,42,4,105,99,111,110,1,119,0,40,0,154,244,246,165,8,42,10,99,114,101,97,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,40,0,154,244,246,165,8,42,16,108,97,115,116,95,101,100,105,116,101,100,95,116,105,109,101,1,122,0,0,0,0,102,79,7,25,40,0,154,244,246,165,8,42,14,108,97,115,116,95,101,100,105,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,8,0,201,129,238,197,4,67,1,118,1,2,105,100,119,36,97,54,97,102,51,49,49,102,45,99,98,99,56,45,52,50,99,50,45,98,56,48,49,45,55,49,49,53,54,49,57,99,51,55,55,54,168,201,129,238,197,4,66,1,122,0,0,0,0,102,79,7,32,161,174,151,139,93,106,1,39,0,203,184,221,173,11,1,36,97,54,97,102,51,49,49,102,45,99,98,99,56,45,52,50,99,50,45,98,56,48,49,45,55,49,49,53,54,49,57,99,51,55,55,54,1,40,0,154,244,246,165,8,57,2,105,100,1,119,36,97,54,97,102,51,49,49,102,45,99,98,99,56,45,52,50,99,50,45,98,56,48,49,45,55,49,49,53,54,49,57,99,51,55,55,54,40,0,154,244,246,165,8,57,4,110,97,109,101,1,119,16,86,105,101,119,32,111,102,32,67,97,108,101,110,100,97,114,40,0,154,244,246,165,8,57,3,98,105,100,1,119,36,50,49,52,51,101,57,53,100,45,53,100,99,98,45,52,101,48,102,45,98,98,50,99,45,53,48,57,52,52,101,54,101,48,49,57,102,40,0,154,244,246,165,8,57,4,100,101,115,99,1,119,0,40,0,154,244,246,165,8,57,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,3,40,0,154,244,246,165,8,57,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,79,7,32,39,0,203,184,221,173,11,4,36,97,54,97,102,51,49,49,102,45,99,98,99,56,45,52,50,99,50,45,98,56,48,49,45,55,49,49,53,54,49,57,99,51,55,55,54,0,40,0,154,244,246,165,8,57,4,105,99,111,110,1,119,0,40,0,154,244,246,165,8,57,10,99,114,101,97,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,40,0,154,244,246,165,8,57,16,108,97,115,116,95,101,100,105,116,101,100,95,116,105,109,101,1,122,0,0,0,0,102,79,7,32,40,0,154,244,246,165,8,57,14,108,97,115,116,95,101,100,105,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,136,174,151,139,93,177,1,1,118,1,2,105,100,119,36,50,98,102,53,48,99,48,51,45,102,52,49,102,45,52,51,54,51,45,98,53,98,49,45,49,48,49,50,49,54,97,54,99,53,99,99,168,174,151,139,93,178,1,1,122,0,0,0,0,102,79,7,50,161,174,151,139,93,179,1,1,39,0,203,184,221,173,11,1,36,50,98,102,53,48,99,48,51,45,102,52,49,102,45,52,51,54,51,45,98,53,98,49,45,49,48,49,50,49,54,97,54,99,53,99,99,1,40,0,154,244,246,165,8,72,2,105,100,1,119,36,50,98,102,53,48,99,48,51,45,102,52,49,102,45,52,51,54,51,45,98,53,98,49,45,49,48,49,50,49,54,97,54,99,53,99,99,40,0,154,244,246,165,8,72,4,110,97,109,101,1,119,16,86,105,101,119,32,111,102,32,67,97,108,101,110,100,97,114,40,0,154,244,246,165,8,72,3,98,105,100,1,119,36,101,101,51,97,101,56,99,101,45,57,53,57,97,45,52,100,102,51,45,56,55,51,52,45,52,48,98,53,51,53,102,102,56,56,101,51,40,0,154,244,246,165,8,72,4,100,101,115,99,1,119,0,40,0,154,244,246,165,8,72,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,3,40,0,154,244,246,165,8,72,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,79,7,50,39,0,203,184,221,173,11,4,36,50,98,102,53,48,99,48,51,45,102,52,49,102,45,52,51,54,51,45,98,53,98,49,45,49,48,49,50,49,54,97,54,99,53,99,99,0,40,0,154,244,246,165,8,72,4,105,99,111,110,1,119,0,40,0,154,244,246,165,8,72,10,99,114,101,97,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,33,0,154,244,246,165,8,72,16,108,97,115,116,95,101,100,105,116,101,100,95,116,105,109,101,1,33,0,154,244,246,165,8,72,14,108,97,115,116,95,101,100,105,116,101,100,95,98,121,1,161,154,244,246,165,8,20,1,168,154,244,246,165,8,83,1,122,4,56,115,160,190,64,16,0,168,154,244,246,165,8,82,1,122,0,0,0,0,102,79,23,79,136,154,244,246,165,8,23,1,118,2,2,105,100,119,36,50,98,102,53,48,99,48,51,45,102,52,49,102,45,52,51,54,51,45,98,53,98,49,45,49,48,49,50,49,54,97,54,99,53,99,99,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,79,23,79,161,154,244,246,165,8,84,1,161,174,151,139,93,174,1,1,161,154,244,246,165,8,71,1,129,154,244,246,165,8,87,1,161,154,244,246,165,8,89,1,161,154,244,246,165,8,90,1,129,154,244,246,165,8,91,1,161,154,244,246,165,8,21,1,161,154,244,246,165,8,22,1,129,154,244,246,165,8,94,1,161,154,244,246,165,8,88,1,161,154,244,246,165,8,95,1,161,154,244,246,165,8,96,1,129,154,244,246,165,8,97,1,161,154,244,246,165,8,98,1,161,154,244,246,165,8,38,1,161,154,244,246,165,8,37,1,129,154,244,246,165,8,101,1,161,154,244,246,165,8,14,1,161,154,244,246,165,8,26,1,129,154,244,246,165,8,105,1,161,154,244,246,165,8,102,1,161,154,244,246,165,8,106,1,161,154,244,246,165,8,107,1,129,154,244,246,165,8,108,1,161,154,244,246,165,8,99,1,161,154,244,246,165,8,100,1,129,154,244,246,165,8,112,1,161,154,244,246,165,8,109,1,161,154,244,246,165,8,113,1,161,154,244,246,165,8,114,1,129,154,244,246,165,8,115,1,161,154,244,246,165,8,103,1,161,154,244,246,165,8,104,1,129,154,244,246,165,8,119,1,161,154,244,246,165,8,116,1,168,154,244,246,165,8,120,1,122,4,56,115,160,190,64,16,0,168,154,244,246,165,8,121,1,122,0,0,0,0,102,79,28,199,136,154,244,246,165,8,122,1,118,2,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,79,28,199,2,105,100,119,36,49,51,53,54,49,53,102,97,45,54,54,102,55,45,52,52,53,49,45,57,98,53,52,45,100,55,101,57,57,52,52,53,102,99,97,52,161,154,244,246,165,8,123,1,161,174,151,139,93,105,1,161,154,244,246,165,8,56,1,129,154,244,246,165,8,126,1,161,154,244,246,165,8,128,1,1,161,154,244,246,165,8,129,1,1,129,154,244,246,165,8,130,1,1,161,154,244,246,165,8,92,1,161,154,244,246,165,8,93,1,129,154,244,246,165,8,133,1,1,161,154,244,246,165,8,127,1,161,154,244,246,165,8,134,1,1,161,154,244,246,165,8,135,1,1,129,154,244,246,165,8,136,1,1,161,154,244,246,165,8,131,1,1,161,154,244,246,165,8,132,1,1,129,154,244,246,165,8,140,1,1,161,154,244,246,165,8,137,1,1,168,154,244,246,165,8,141,1,1,122,4,56,115,160,190,64,16,0,168,154,244,246,165,8,142,1,1,122,0,0,0,0,102,80,7,127,136,154,244,246,165,8,143,1,1,118,2,2,105,100,119,36,50,49,52,51,101,57,53,100,45,53,100,99,98,45,52,101,48,102,45,98,98,50,99,45,53,48,57,52,52,101,54,101,48,49,57,102,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,80,7,127,161,154,244,246,165,8,138,1,1,161,154,244,246,165,8,139,1,1,129,154,244,246,165,8,147,1,1,161,154,244,246,165,8,144,1,1,161,154,244,246,165,8,148,1,1,161,154,244,246,165,8,149,1,1,129,154,244,246,165,8,150,1,1,161,154,244,246,165,8,110,1,161,154,244,246,165,8,111,1,129,154,244,246,165,8,154,1,1,161,154,244,246,165,8,151,1,1,161,154,244,246,165,8,155,1,1,161,154,244,246,165,8,156,1,1,129,154,244,246,165,8,157,1,1,161,174,151,139,93,153,1,1,161,174,151,139,93,154,1,1,129,154,244,246,165,8,161,1,1,161,154,244,246,165,8,158,1,1,161,154,244,246,165,8,162,1,1,161,154,244,246,165,8,163,1,1,129,154,244,246,165,8,164,1,1,161,154,244,246,165,8,152,1,1,161,154,244,246,165,8,153,1,1,129,154,244,246,165,8,168,1,1,161,154,244,246,165,8,165,1,1,161,154,244,246,165,8,169,1,1,161,154,244,246,165,8,170,1,1,129,154,244,246,165,8,171,1,1,161,154,244,246,165,8,117,1,161,154,244,246,165,8,118,1,129,154,244,246,165,8,175,1,1,161,154,244,246,165,8,172,1,1,161,154,244,246,165,8,176,1,1,161,154,244,246,165,8,177,1,1,129,154,244,246,165,8,178,1,1,39,0,203,184,221,173,11,1,36,53,55,98,56,49,55,55,100,45,57,100,52,50,45,53,49,55,56,45,56,99,98,50,45,102,99,53,101,54,57,51,48,102,51,48,97,1,40,0,154,244,246,165,8,183,1,2,105,100,1,119,36,53,55,98,56,49,55,55,100,45,57,100,52,50,45,53,49,55,56,45,56,99,98,50,45,102,99,53,101,54,57,51,48,102,51,48,97,40,0,154,244,246,165,8,183,1,4,110,97,109,101,1,119,8,85,110,116,105,116,108,101,100,40,0,154,244,246,165,8,183,1,3,98,105,100,1,119,36,53,55,98,56,49,55,55,100,45,57,100,52,50,45,53,49,55,56,45,56,99,98,50,45,102,99,53,101,54,57,51,48,102,51,48,97,40,0,154,244,246,165,8,183,1,4,100,101,115,99,1,119,0,40,0,154,244,246,165,8,183,1,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,0,40,0,154,244,246,165,8,183,1,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,80,7,153,39,0,203,184,221,173,11,4,36,53,55,98,56,49,55,55,100,45,57,100,52,50,45,53,49,55,56,45,56,99,98,50,45,102,99,53,101,54,57,51,48,102,51,48,97,0,40,0,154,244,246,165,8,183,1,4,105,99,111,110,1,119,0,40,0,154,244,246,165,8,183,1,10,99,114,101,97,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,33,0,154,244,246,165,8,183,1,16,108,97,115,116,95,101,100,105,116,101,100,95,116,105,109,101,1,33,0,154,244,246,165,8,183,1,14,108,97,115,116,95,101,100,105,116,101,100,95,98,121,1,168,154,244,246,165,8,194,1,1,122,4,56,115,160,190,64,16,0,168,154,244,246,165,8,193,1,1,122,0,0,0,0,102,80,7,153,40,0,154,244,246,165,8,183,1,5,101,120,116,114,97,1,119,36,123,34,99,111,118,101,114,34,58,123,34,116,121,112,101,34,58,34,110,111,110,101,34,44,34,118,97,108,117,101,34,58,34,34,125,125,50,207,228,238,162,8,0,136,158,156,181,152,10,27,1,118,1,2,105,100,119,36,100,97,53,54,102,102,97,48,45,53,51,53,54,45,52,50,56,99,45,98,54,49,98,45,53,55,52,97,49,49,57,99,54,57,57,101,161,158,156,181,152,10,28,1,161,158,156,181,152,10,29,1,39,0,203,184,221,173,11,1,36,100,97,53,54,102,102,97,48,45,53,51,53,54,45,52,50,56,99,45,98,54,49,98,45,53,55,52,97,49,49,57,99,54,57,57,101,1,40,0,207,228,238,162,8,3,2,105,100,1,119,36,100,97,53,54,102,102,97,48,45,53,51,53,54,45,52,50,56,99,45,98,54,49,98,45,53,55,52,97,49,49,57,99,54,57,57,101,40,0,207,228,238,162,8,3,4,110,97,109,101,1,119,8,85,110,116,105,116,108,101,100,40,0,207,228,238,162,8,3,3,98,105,100,1,119,36,50,54,100,53,99,56,99,49,45,49,99,54,54,45,52,53,57,99,45,98,99,54,99,45,102,52,100,97,49,97,54,54,51,51,52,56,40,0,207,228,238,162,8,3,4,100,101,115,99,1,119,0,40,0,207,228,238,162,8,3,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,0,40,0,207,228,238,162,8,3,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,101,242,129,194,39,0,203,184,221,173,11,4,36,100,97,53,54,102,102,97,48,45,53,51,53,54,45,52,50,56,99,45,98,54,49,98,45,53,55,52,97,49,49,57,99,54,57,57,101,0,40,0,207,228,238,162,8,3,4,105,99,111,110,1,119,0,40,0,207,228,238,162,8,3,10,99,114,101,97,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,33,0,207,228,238,162,8,3,16,108,97,115,116,95,101,100,105,116,101,100,95,116,105,109,101,1,33,0,207,228,238,162,8,3,14,108,97,115,116,95,101,100,105,116,101,100,95,98,121,1,161,158,156,181,152,10,42,1,161,207,228,238,162,8,14,1,161,207,228,238,162,8,13,1,129,158,156,181,152,10,48,1,161,207,228,238,162,8,16,1,161,207,228,238,162,8,17,1,129,207,228,238,162,8,18,1,136,207,228,238,162,8,0,1,118,1,2,105,100,119,36,52,52,51,53,101,53,55,98,45,99,50,54,51,45,52,101,55,102,45,97,52,51,53,45,50,48,56,55,57,97,54,50,101,54,100,97,161,207,228,238,162,8,1,1,161,207,228,238,162,8,2,1,39,0,203,184,221,173,11,1,36,52,52,51,53,101,53,55,98,45,99,50,54,51,45,52,101,55,102,45,97,52,51,53,45,50,48,56,55,57,97,54,50,101,54,100,97,1,40,0,207,228,238,162,8,25,2,105,100,1,119,36,52,52,51,53,101,53,55,98,45,99,50,54,51,45,52,101,55,102,45,97,52,51,53,45,50,48,56,55,57,97,54,50,101,54,100,97,40,0,207,228,238,162,8,25,4,110,97,109,101,1,119,8,85,110,116,105,116,108,101,100,40,0,207,228,238,162,8,25,3,98,105,100,1,119,36,50,54,100,53,99,56,99,49,45,49,99,54,54,45,52,53,57,99,45,98,99,54,99,45,102,52,100,97,49,97,54,54,51,51,52,56,40,0,207,228,238,162,8,25,4,100,101,115,99,1,119,0,40,0,207,228,238,162,8,25,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,0,40,0,207,228,238,162,8,25,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,101,242,129,219,39,0,203,184,221,173,11,4,36,52,52,51,53,101,53,55,98,45,99,50,54,51,45,52,101,55,102,45,97,52,51,53,45,50,48,56,55,57,97,54,50,101,54,100,97,0,40,0,207,228,238,162,8,25,4,105,99,111,110,1,119,0,40,0,207,228,238,162,8,25,10,99,114,101,97,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,33,0,207,228,238,162,8,25,16,108,97,115,116,95,101,100,105,116,101,100,95,116,105,109,101,1,33,0,207,228,238,162,8,25,14,108,97,115,116,95,101,100,105,116,101,100,95,98,121,1,161,207,228,238,162,8,15,1,161,207,228,238,162,8,36,1,161,207,228,238,162,8,35,1,129,207,228,238,162,8,21,1,161,207,228,238,162,8,38,1,161,207,228,238,162,8,39,1,129,207,228,238,162,8,40,1,161,158,156,181,152,10,46,1,161,158,156,181,152,10,47,1,161,158,156,181,152,10,32,1,161,207,228,238,162,8,44,1,161,207,228,238,162,8,45,1,129,207,228,238,162,8,43,1,1,183,226,184,158,8,0,161,240,253,240,229,1,78,8,11,175,225,172,150,8,0,161,173,252,148,184,13,61,1,161,173,252,148,184,13,62,1,129,173,252,148,184,13,63,1,161,173,252,148,184,13,60,1,161,173,252,148,184,13,57,1,161,173,252,148,184,13,58,1,129,175,225,172,150,8,2,1,161,175,225,172,150,8,3,1,161,175,225,172,150,8,0,1,161,175,225,172,150,8,1,1,136,175,225,172,150,8,6,1,118,2,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,41,200,233,2,105,100,119,36,100,48,52,57,54,51,50,52,45,53,53,55,48,45,52,48,48,54,45,98,52,101,97,45,100,98,55,53,49,54,100,50,49,50,102,100,66,214,139,213,136,8,0,161,201,129,238,197,4,9,1,161,201,129,238,197,4,10,1,136,149,249,242,175,4,13,1,118,2,2,105,100,119,36,102,51,53,50,55,48,99,55,45,99,54,54,99,45,52,54,99,101,45,56,101,49,97,45,51,102,54,51,57,102,55,98,48,48,48,100,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,77,78,7,168,214,139,213,136,8,0,1,122,4,56,115,160,190,64,16,0,168,214,139,213,136,8,1,1,122,0,0,0,0,102,77,78,7,161,180,230,210,212,13,0,1,161,133,159,138,205,12,9,1,136,214,139,213,136,8,2,1,118,2,2,105,100,119,36,100,48,52,57,54,51,50,52,45,53,53,55,48,45,52,48,48,54,45,98,52,101,97,45,100,98,55,53,49,54,100,50,49,50,102,100,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,77,78,9,168,214,139,213,136,8,5,1,122,4,56,115,160,190,64,16,0,168,214,139,213,136,8,6,1,122,0,0,0,0,102,77,78,9,161,201,129,238,197,4,12,1,161,201,129,238,197,4,13,1,136,214,139,213,136,8,7,1,118,2,2,105,100,119,36,100,49,52,50,51,57,101,57,45,50,98,102,55,45,52,56,50,52,45,57,51,101,99,45,52,102,51,99,99,53,54,49,54,55,50,48,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,77,78,10,168,214,139,213,136,8,10,1,122,4,56,115,160,190,64,16,0,168,214,139,213,136,8,11,1,122,0,0,0,0,102,77,78,10,161,158,156,181,152,10,0,1,161,158,156,181,152,10,1,1,136,214,139,213,136,8,12,1,118,2,2,105,100,119,36,99,55,52,55,53,49,50,51,45,56,50,51,57,45,52,98,98,49,45,56,100,102,53,45,54,56,49,52,56,48,102,101,57,53,52,99,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,77,78,11,168,214,139,213,136,8,15,1,122,4,56,115,160,190,64,16,0,168,214,139,213,136,8,16,1,122,0,0,0,0,102,77,78,12,161,161,178,132,150,11,90,1,161,161,178,132,150,11,91,1,136,214,139,213,136,8,17,1,118,2,2,105,100,119,36,99,97,49,50,50,99,48,52,45,100,55,98,51,45,52,102,55,48,45,57,57,53,49,45,57,54,98,102,100,97,57,98,54,98,50,52,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,77,78,12,168,214,139,213,136,8,20,1,122,4,56,115,160,190,64,16,0,168,214,139,213,136,8,21,1,122,0,0,0,0,102,77,78,12,161,222,205,223,235,7,7,1,161,222,205,223,235,7,8,1,136,214,139,213,136,8,22,1,118,2,2,105,100,119,36,98,53,56,48,55,51,52,53,45,53,100,97,55,45,52,53,98,100,45,97,56,97,101,45,101,97,53,54,56,99,55,99,57,49,49,97,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,77,78,29,168,214,139,213,136,8,25,1,122,4,56,115,160,190,64,16,0,168,214,139,213,136,8,26,1,122,0,0,0,0,102,77,78,30,161,207,228,238,162,8,19,1,161,207,228,238,162,8,20,1,136,214,139,213,136,8,27,1,118,2,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,77,78,30,2,105,100,119,36,100,97,53,54,102,102,97,48,45,53,51,53,54,45,52,50,56,99,45,98,54,49,98,45,53,55,52,97,49,49,57,99,54,57,57,101,168,214,139,213,136,8,30,1,122,4,56,115,160,190,64,16,0,168,214,139,213,136,8,31,1,122,0,0,0,0,102,77,78,30,161,173,252,148,184,13,17,1,161,173,252,148,184,13,18,1,136,214,139,213,136,8,32,1,118,2,2,105,100,119,36,52,52,51,53,101,53,55,98,45,99,50,54,51,45,52,101,55,102,45,97,52,51,53,45,50,48,56,55,57,97,54,50,101,54,100,97,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,77,78,31,168,214,139,213,136,8,35,1,122,4,56,115,160,190,64,16,0,168,214,139,213,136,8,36,1,122,0,0,0,0,102,77,78,31,161,225,248,138,176,2,7,1,161,225,248,138,176,2,8,1,136,214,139,213,136,8,37,1,118,2,2,105,100,119,36,100,100,98,57,51,98,97,55,45,48,54,99,55,45,52,49,55,54,45,57,56,50,97,45,100,55,52,50,51,101,48,57,98,52,52,49,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,77,78,41,168,214,139,213,136,8,40,1,122,4,56,115,160,190,64,16,0,168,214,139,213,136,8,41,1,122,0,0,0,0,102,77,78,41,161,165,139,157,171,15,91,1,161,165,139,157,171,15,92,1,136,214,139,213,136,8,42,1,118,2,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,77,78,44,2,105,100,119,36,50,99,49,101,101,57,53,97,45,49,98,48,57,45,52,97,49,102,45,56,100,53,101,45,53,48,49,98,99,52,56,54,49,97,57,100,168,214,139,213,136,8,45,1,122,4,56,115,160,190,64,16,0,168,214,139,213,136,8,46,1,122,0,0,0,0,102,77,78,44,161,234,153,236,158,4,3,1,161,234,153,236,158,4,4,1,136,214,139,213,136,8,47,1,118,2,2,105,100,119,36,49,98,48,101,51,50,50,100,45,52,57,48,57,45,52,99,54,51,45,57,49,52,97,45,100,48,51,52,102,99,51,54,51,48,57,55,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,77,78,45,168,214,139,213,136,8,50,1,122,4,56,115,160,190,64,16,0,168,214,139,213,136,8,51,1,122,0,0,0,0,102,77,78,45,161,165,139,157,171,15,58,1,161,165,139,157,171,15,59,1,136,214,139,213,136,8,52,1,118,2,2,105,100,119,36,54,53,98,48,54,100,98,56,45,55,48,54,49,45,52,98,102,54,45,98,51,49,53,45,55,53,56,99,48,100,100,50,53,99,100,102,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,77,78,49,168,214,139,213,136,8,55,1,122,4,56,115,160,190,64,16,0,168,214,139,213,136,8,56,1,122,0,0,0,0,102,77,78,49,161,241,155,213,233,1,0,1,161,241,155,213,233,1,1,1,129,241,155,213,233,1,24,1,161,214,139,213,136,8,60,1,161,214,139,213,136,8,61,1,129,214,139,213,136,8,62,1,31,222,205,223,235,7,0,161,137,226,192,199,6,17,1,161,137,226,192,199,6,18,1,129,137,226,192,199,6,19,1,161,201,129,238,197,4,39,1,161,201,129,238,197,4,40,1,129,222,205,223,235,7,2,1,161,137,226,192,199,6,13,1,161,222,205,223,235,7,3,1,161,222,205,223,235,7,4,1,136,222,205,223,235,7,5,1,118,2,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,67,55,119,2,105,100,119,36,98,53,56,48,55,51,52,53,45,53,100,97,55,45,52,53,98,100,45,97,56,97,101,45,101,97,53,54,56,99,55,99,57,49,49,97,161,222,205,223,235,7,0,1,161,222,205,223,235,7,1,1,129,222,205,223,235,7,9,1,161,222,205,223,235,7,6,1,161,222,205,223,235,7,10,1,161,222,205,223,235,7,11,1,129,222,205,223,235,7,12,1,161,137,226,192,199,6,7,1,161,137,226,192,199,6,8,1,129,222,205,223,235,7,16,1,161,222,205,223,235,7,13,1,161,222,205,223,235,7,17,1,161,222,205,223,235,7,18,1,129,222,205,223,235,7,19,1,161,222,205,223,235,7,14,1,161,222,205,223,235,7,15,1,129,222,205,223,235,7,23,1,161,222,205,223,235,7,20,1,161,222,205,223,235,7,24,1,161,222,205,223,235,7,25,1,129,222,205,223,235,7,26,1,3,162,238,198,212,7,0,161,245,220,194,52,6,1,161,247,200,243,247,14,2,1,136,247,200,243,247,14,57,1,118,2,2,105,100,119,36,50,54,100,53,99,56,99,49,45,49,99,54,54,45,52,53,57,99,45,98,99,54,99,45,102,52,100,97,49,97,54,54,51,51,52,56,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,87,58,53,12,241,130,161,205,7,0,161,226,212,179,248,2,12,1,161,226,212,179,248,2,13,1,129,226,212,179,248,2,14,1,161,234,153,236,158,4,61,1,161,234,153,236,158,4,62,1,129,241,130,161,205,7,2,1,161,241,130,161,205,7,0,1,161,241,130,161,205,7,1,1,129,241,130,161,205,7,5,1,168,234,153,236,158,4,39,1,122,4,56,115,160,190,64,16,0,168,234,153,236,158,4,38,1,122,0,0,0,0,102,78,192,25,136,174,151,139,93,243,1,1,118,2,2,105,100,119,36,101,52,99,56,57,52,50,49,45,49,50,98,50,45,52,100,48,50,45,56,54,51,100,45,50,48,57,52,57,101,101,99,57,50,55,49,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,78,192,25,1,229,153,197,202,7,0,161,239,199,189,146,3,23,241,6,1,250,198,166,187,7,0,161,152,252,186,192,1,1,2,35,164,155,139,169,7,0,161,149,129,169,191,12,13,1,161,149,129,169,191,12,14,1,129,149,129,169,191,12,15,1,161,149,129,169,191,12,12,1,161,149,129,169,191,12,9,1,161,149,129,169,191,12,10,1,129,164,155,139,169,7,2,1,161,164,155,139,169,7,3,1,161,207,228,238,162,8,41,1,161,207,228,238,162,8,42,1,129,188,171,136,250,8,8,1,161,164,155,139,169,7,7,1,161,188,171,136,250,8,6,1,161,188,171,136,250,8,7,1,129,164,155,139,169,7,10,1,161,164,155,139,169,7,11,1,161,149,129,169,191,12,5,1,161,149,129,169,191,12,6,1,129,188,171,136,250,8,17,1,161,164,155,139,169,7,15,1,161,188,171,136,250,8,15,1,161,188,171,136,250,8,16,1,129,164,155,139,169,7,18,1,161,164,155,139,169,7,19,1,161,164,155,139,169,7,16,1,161,164,155,139,169,7,17,1,129,164,155,139,169,7,22,1,161,164,155,139,169,7,23,1,161,164,155,139,169,7,20,1,161,164,155,139,169,7,21,1,129,188,171,136,250,8,20,1,161,164,155,139,169,7,27,1,161,188,171,136,250,8,18,1,161,188,171,136,250,8,19,1,129,164,155,139,169,7,30,1,1,177,219,160,167,7,0,161,135,240,136,178,10,44,4,2,135,193,208,135,7,0,161,135,167,156,250,14,15,12,161,135,193,208,135,7,11,4,17,130,180,254,251,6,0,129,252,163,130,200,6,15,1,161,252,163,130,200,6,16,1,161,252,163,130,200,6,17,1,39,0,203,184,221,173,11,1,36,100,100,98,57,51,98,97,55,45,48,54,99,55,45,52,49,55,54,45,57,56,50,97,45,100,55,52,50,51,101,48,57,98,52,52,49,1,40,0,130,180,254,251,6,3,2,105,100,1,119,36,100,100,98,57,51,98,97,55,45,48,54,99,55,45,52,49,55,54,45,57,56,50,97,45,100,55,52,50,51,101,48,57,98,52,52,49,33,0,130,180,254,251,6,3,4,110,97,109,101,1,33,0,130,180,254,251,6,3,3,98,105,100,1,40,0,130,180,254,251,6,3,4,100,101,115,99,1,119,0,40,0,130,180,254,251,6,3,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,0,33,0,130,180,254,251,6,3,10,99,114,101,97,116,101,100,95,97,116,1,39,0,203,184,221,173,11,4,36,100,100,98,57,51,98,97,55,45,48,54,99,55,45,52,49,55,54,45,57,56,50,97,45,100,55,52,50,51,101,48,57,98,52,52,49,0,40,0,130,180,254,251,6,3,4,105,99,111,110,1,119,0,40,0,130,180,254,251,6,3,10,99,114,101,97,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,33,0,130,180,254,251,6,3,16,108,97,115,116,95,101,100,105,116,101,100,95,116,105,109,101,1,33,0,130,180,254,251,6,3,14,108,97,115,116,95,101,100,105,116,101,100,95,98,121,2,161,130,180,254,251,6,13,1,168,130,180,254,251,6,5,1,119,5,110,105,115,104,105,27,159,156,204,250,6,0,161,149,249,242,175,4,28,1,161,149,249,242,175,4,29,1,129,149,249,242,175,4,30,1,161,159,156,204,250,6,0,1,161,159,156,204,250,6,1,1,129,159,156,204,250,6,2,1,161,143,184,153,180,6,3,1,161,143,184,153,180,6,4,1,129,143,184,153,180,6,5,1,161,159,156,204,250,6,6,1,161,159,156,204,250,6,7,1,129,159,156,204,250,6,8,1,161,159,156,204,250,6,9,1,161,159,156,204,250,6,10,1,129,159,156,204,250,6,11,1,161,159,156,204,250,6,12,1,161,159,156,204,250,6,13,1,129,159,156,204,250,6,14,1,161,159,156,204,250,6,15,1,161,159,156,204,250,6,16,1,129,159,156,204,250,6,17,1,161,159,156,204,250,6,18,1,161,159,156,204,250,6,19,1,129,159,156,204,250,6,20,1,161,159,156,204,250,6,21,1,161,159,156,204,250,6,22,1,129,159,156,204,250,6,23,1,3,135,166,246,235,6,0,161,245,220,194,52,36,1,161,245,220,194,52,37,1,136,245,220,194,52,32,1,118,2,2,105,100,119,36,48,101,55,99,100,101,102,50,45,49,48,99,50,45,52,52,101,99,45,56,98,54,49,45,49,97,100,101,48,98,102,53,100,51,102,102,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,85,170,119,1,175,205,156,228,6,0,161,183,226,184,158,8,7,10,15,240,149,229,225,6,0,136,203,184,221,173,11,16,1,118,1,2,105,100,119,36,101,48,102,101,54,56,54,55,45,50,48,56,102,45,52,51,57,57,45,97,56,56,97,45,101,57,98,97,102,52,56,97,99,48,53,101,161,158,156,181,152,10,7,1,161,158,156,181,152,10,8,1,39,0,203,184,221,173,11,1,36,101,48,102,101,54,56,54,55,45,50,48,56,102,45,52,51,57,57,45,97,56,56,97,45,101,57,98,97,102,52,56,97,99,48,53,101,1,40,0,240,149,229,225,6,3,2,105,100,1,119,36,101,48,102,101,54,56,54,55,45,50,48,56,102,45,52,51,57,57,45,97,56,56,97,45,101,57,98,97,102,52,56,97,99,48,53,101,40,0,240,149,229,225,6,3,4,110,97,109,101,1,119,0,40,0,240,149,229,225,6,3,3,98,105,100,1,119,36,57,101,101,98,101,97,48,51,45,51,101,100,53,45,52,50,57,56,45,56,54,98,50,45,97,55,102,55,55,56,53,54,100,52,56,98,40,0,240,149,229,225,6,3,4,100,101,115,99,1,119,0,40,0,240,149,229,225,6,3,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,0,40,0,240,149,229,225,6,3,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,101,241,138,214,39,0,203,184,221,173,11,4,36,101,48,102,101,54,56,54,55,45,50,48,56,102,45,52,51,57,57,45,97,56,56,97,45,101,57,98,97,102,52,56,97,99,48,53,101,0,40,0,240,149,229,225,6,3,4,105,99,111,110,1,119,0,40,0,240,149,229,225,6,3,10,99,114,101,97,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,40,0,240,149,229,225,6,3,16,108,97,115,116,95,101,100,105,116,101,100,95,116,105,109,101,1,122,0,0,0,0,101,241,138,214,40,0,240,149,229,225,6,3,14,108,97,115,116,95,101,100,105,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,3,255,140,248,220,6,0,161,180,230,210,212,13,4,1,161,180,230,210,212,13,5,1,129,180,230,210,212,13,6,1,1,157,207,243,216,6,0,161,145,190,137,224,10,1,2,30,252,163,130,200,6,0,136,143,131,148,152,6,0,1,118,1,2,105,100,119,36,102,57,101,48,48,54,53,49,45,49,53,48,101,45,52,50,56,56,45,56,48,55,57,45,50,50,100,48,54,97,50,55,54,97,55,49,161,143,131,148,152,6,1,1,161,143,131,148,152,6,2,1,39,0,203,184,221,173,11,1,36,102,57,101,48,48,54,53,49,45,49,53,48,101,45,52,50,56,56,45,56,48,55,57,45,50,50,100,48,54,97,50,55,54,97,55,49,1,40,0,252,163,130,200,6,3,2,105,100,1,119,36,102,57,101,48,48,54,53,49,45,49,53,48,101,45,52,50,56,56,45,56,48,55,57,45,50,50,100,48,54,97,50,55,54,97,55,49,40,0,252,163,130,200,6,3,4,110,97,109,101,1,119,0,40,0,252,163,130,200,6,3,3,98,105,100,1,119,36,57,101,101,98,101,97,48,51,45,51,101,100,53,45,52,50,57,56,45,56,54,98,50,45,97,55,102,55,55,56,53,54,100,52,56,98,40,0,252,163,130,200,6,3,4,100,101,115,99,1,119,0,40,0,252,163,130,200,6,3,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,0,40,0,252,163,130,200,6,3,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,101,241,149,3,39,0,203,184,221,173,11,4,36,102,57,101,48,48,54,53,49,45,49,53,48,101,45,52,50,56,56,45,56,48,55,57,45,50,50,100,48,54,97,50,55,54,97,55,49,0,40,0,252,163,130,200,6,3,4,105,99,111,110,1,119,0,40,0,252,163,130,200,6,3,10,99,114,101,97,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,40,0,252,163,130,200,6,3,16,108,97,115,116,95,101,100,105,116,101,100,95,116,105,109,101,1,122,0,0,0,0,101,241,149,3,40,0,252,163,130,200,6,3,14,108,97,115,116,95,101,100,105,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,136,252,163,130,200,6,0,1,118,1,2,105,100,119,36,56,54,54,57,52,102,97,100,45,54,55,52,97,45,52,54,55,52,45,56,52,100,48,45,51,50,99,52,56,54,97,55,55,48,98,52,161,252,163,130,200,6,1,1,161,252,163,130,200,6,2,1,39,0,203,184,221,173,11,1,36,56,54,54,57,52,102,97,100,45,54,55,52,97,45,52,54,55,52,45,56,52,100,48,45,51,50,99,52,56,54,97,55,55,48,98,52,1,40,0,252,163,130,200,6,18,2,105,100,1,119,36,56,54,54,57,52,102,97,100,45,54,55,52,97,45,52,54,55,52,45,56,52,100,48,45,51,50,99,52,56,54,97,55,55,48,98,52,40,0,252,163,130,200,6,18,4,110,97,109,101,1,119,0,40,0,252,163,130,200,6,18,3,98,105,100,1,119,36,57,101,101,98,101,97,48,51,45,51,101,100,53,45,52,50,57,56,45,56,54,98,50,45,97,55,102,55,55,56,53,54,100,52,56,98,40,0,252,163,130,200,6,18,4,100,101,115,99,1,119,0,40,0,252,163,130,200,6,18,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,0,40,0,252,163,130,200,6,18,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,101,241,149,31,39,0,203,184,221,173,11,4,36,56,54,54,57,52,102,97,100,45,54,55,52,97,45,52,54,55,52,45,56,52,100,48,45,51,50,99,52,56,54,97,55,55,48,98,52,0,40,0,252,163,130,200,6,18,4,105,99,111,110,1,119,0,40,0,252,163,130,200,6,18,10,99,114,101,97,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,40,0,252,163,130,200,6,18,16,108,97,115,116,95,101,100,105,116,101,100,95,116,105,109,101,1,122,0,0,0,0,101,241,149,31,40,0,252,163,130,200,6,18,14,108,97,115,116,95,101,100,105,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,20,137,226,192,199,6,0,161,170,140,240,234,9,28,1,161,170,140,240,234,9,29,1,129,170,140,240,234,9,30,1,161,213,161,242,209,13,28,1,161,213,161,242,209,13,29,1,129,137,226,192,199,6,2,1,161,170,140,240,234,9,27,1,161,137,226,192,199,6,3,1,161,137,226,192,199,6,4,1,129,137,226,192,199,6,5,1,161,137,226,192,199,6,0,1,161,137,226,192,199,6,1,1,129,137,226,192,199,6,9,1,161,137,226,192,199,6,6,1,161,137,226,192,199,6,10,1,161,137,226,192,199,6,11,1,129,137,226,192,199,6,12,1,161,137,226,192,199,6,14,1,161,137,226,192,199,6,15,1,129,137,226,192,199,6,16,1,3,246,185,174,192,6,0,161,135,193,208,135,7,11,1,161,135,193,208,135,7,15,87,161,246,185,174,192,6,87,2,2,196,154,250,183,6,0,161,149,161,132,184,14,218,2,1,161,149,161,132,184,14,220,2,61,6,143,184,153,180,6,0,161,164,188,201,172,1,0,1,161,164,188,201,172,1,1,1,129,164,188,201,172,1,2,1,161,143,184,153,180,6,0,1,161,143,184,153,180,6,1,1,129,143,184,153,180,6,2,1,1,198,189,216,175,6,0,161,234,187,164,181,1,23,23,1,153,130,203,161,6,0,161,248,210,237,129,13,1,97,15,143,131,148,152,6,0,136,243,239,182,181,13,15,1,118,1,2,105,100,119,36,50,57,55,48,52,49,52,101,45,99,99,50,48,45,52,51,50,102,45,97,100,49,54,45,52,50,101,99,55,49,55,51,54,100,53,57,161,243,239,182,181,13,16,1,161,243,239,182,181,13,17,1,39,0,203,184,221,173,11,1,36,50,57,55,48,52,49,52,101,45,99,99,50,48,45,52,51,50,102,45,97,100,49,54,45,52,50,101,99,55,49,55,51,54,100,53,57,1,40,0,143,131,148,152,6,3,2,105,100,1,119,36,50,57,55,48,52,49,52,101,45,99,99,50,48,45,52,51,50,102,45,97,100,49,54,45,52,50,101,99,55,49,55,51,54,100,53,57,40,0,143,131,148,152,6,3,4,110,97,109,101,1,119,0,40,0,143,131,148,152,6,3,3,98,105,100,1,119,36,57,101,101,98,101,97,48,51,45,51,101,100,53,45,52,50,57,56,45,56,54,98,50,45,97,55,102,55,55,56,53,54,100,52,56,98,40,0,143,131,148,152,6,3,4,100,101,115,99,1,119,0,40,0,143,131,148,152,6,3,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,0,40,0,143,131,148,152,6,3,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,101,241,143,252,39,0,203,184,221,173,11,4,36,50,57,55,48,52,49,52,101,45,99,99,50,48,45,52,51,50,102,45,97,100,49,54,45,52,50,101,99,55,49,55,51,54,100,53,57,0,40,0,143,131,148,152,6,3,4,105,99,111,110,1,119,0,40,0,143,131,148,152,6,3,10,99,114,101,97,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,40,0,143,131,148,152,6,3,16,108,97,115,116,95,101,100,105,116,101,100,95,116,105,109,101,1,122,0,0,0,0,101,241,143,252,40,0,143,131,148,152,6,3,14,108,97,115,116,95,101,100,105,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,1,188,237,223,145,6,0,161,221,147,167,147,15,176,2,26,165,1,140,152,206,145,6,0,161,154,244,246,165,8,180,1,1,161,154,244,246,165,8,181,1,1,129,154,244,246,165,8,182,1,1,161,140,152,206,145,6,0,1,161,140,152,206,145,6,1,1,129,140,152,206,145,6,2,1,161,140,152,206,145,6,3,1,161,140,152,206,145,6,4,1,129,140,152,206,145,6,5,1,161,154,244,246,165,8,159,1,1,161,154,244,246,165,8,160,1,1,129,140,152,206,145,6,8,1,161,154,244,246,165,8,179,1,1,161,140,152,206,145,6,9,1,161,140,152,206,145,6,10,1,129,140,152,206,145,6,11,1,161,140,152,206,145,6,12,1,161,140,152,206,145,6,6,1,161,140,152,206,145,6,7,1,129,140,152,206,145,6,15,1,161,140,152,206,145,6,17,1,161,140,152,206,145,6,18,1,129,140,152,206,145,6,19,1,161,140,152,206,145,6,20,1,161,140,152,206,145,6,21,1,129,140,152,206,145,6,22,1,161,154,244,246,165,8,166,1,1,161,154,244,246,165,8,167,1,1,129,140,152,206,145,6,25,1,161,140,152,206,145,6,16,1,161,140,152,206,145,6,26,1,161,140,152,206,145,6,27,1,129,140,152,206,145,6,28,1,161,154,244,246,165,8,173,1,1,161,154,244,246,165,8,174,1,1,129,140,152,206,145,6,32,1,161,140,152,206,145,6,29,1,161,140,152,206,145,6,33,1,161,140,152,206,145,6,34,1,129,140,152,206,145,6,35,1,161,140,152,206,145,6,13,1,161,140,152,206,145,6,14,1,129,140,152,206,145,6,39,1,161,140,152,206,145,6,36,1,161,140,152,206,145,6,40,1,161,140,152,206,145,6,41,1,129,140,152,206,145,6,42,1,161,140,152,206,145,6,43,1,161,140,152,206,145,6,23,1,161,140,152,206,145,6,24,1,129,140,152,206,145,6,46,1,161,140,152,206,145,6,48,1,161,140,152,206,145,6,49,1,129,140,152,206,145,6,50,1,161,140,152,206,145,6,44,1,161,140,152,206,145,6,45,1,129,140,152,206,145,6,53,1,161,140,152,206,145,6,47,1,161,140,152,206,145,6,54,1,161,140,152,206,145,6,55,1,136,140,152,206,145,6,56,1,118,2,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,83,230,246,2,105,100,119,36,55,100,50,49,52,56,102,99,45,99,97,99,101,45,52,52,53,50,45,57,99,53,99,45,57,54,101,53,50,101,54,98,102,56,98,53,161,140,152,206,145,6,51,1,161,140,152,206,145,6,52,1,129,140,152,206,145,6,60,1,161,140,152,206,145,6,57,1,161,140,152,206,145,6,61,1,161,140,152,206,145,6,62,1,129,140,152,206,145,6,63,1,161,140,152,206,145,6,65,1,161,140,152,206,145,6,66,1,161,149,249,242,175,4,2,1,161,140,152,206,145,6,68,1,161,140,152,206,145,6,69,1,161,140,152,206,145,6,70,1,161,140,152,206,145,6,71,1,161,140,152,206,145,6,72,1,161,140,152,206,145,6,73,1,161,140,152,206,145,6,74,1,161,140,152,206,145,6,75,1,161,140,152,206,145,6,76,1,161,140,152,206,145,6,77,1,161,140,152,206,145,6,78,1,161,140,152,206,145,6,79,1,161,140,152,206,145,6,58,1,161,140,152,206,145,6,59,1,136,140,152,206,145,6,67,1,118,2,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,83,241,108,2,105,100,119,36,55,100,50,49,52,56,102,99,45,99,97,99,101,45,52,52,53,50,45,57,99,53,99,45,57,54,101,53,50,101,54,98,102,56,98,53,161,140,152,206,145,6,64,1,161,140,152,206,145,6,83,1,161,140,152,206,145,6,84,1,136,140,152,206,145,6,85,1,118,2,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,83,241,109,2,105,100,119,36,55,100,50,49,52,56,102,99,45,99,97,99,101,45,52,52,53,50,45,57,99,53,99,45,57,54,101,53,50,101,54,98,102,56,98,53,161,140,152,206,145,6,86,1,161,140,152,206,145,6,80,1,161,140,152,206,145,6,81,1,129,140,152,206,145,6,89,1,161,140,152,206,145,6,91,1,161,140,152,206,145,6,92,1,136,140,152,206,145,6,93,1,118,2,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,83,241,111,2,105,100,119,36,50,54,100,53,99,56,99,49,45,49,99,54,54,45,52,53,57,99,45,98,99,54,99,45,102,52,100,97,49,97,54,54,51,51,52,56,161,140,152,206,145,6,94,1,161,140,152,206,145,6,95,1,161,140,152,206,145,6,82,1,136,241,155,213,233,1,6,1,118,1,2,105,100,119,36,48,101,55,99,100,101,102,50,45,49,48,99,50,45,52,52,101,99,45,56,98,54,49,45,49,97,100,101,48,98,102,53,100,51,102,102,161,241,155,213,233,1,7,1,161,140,152,206,145,6,98,1,39,0,203,184,221,173,11,1,36,48,101,55,99,100,101,102,50,45,49,48,99,50,45,52,52,101,99,45,56,98,54,49,45,49,97,100,101,48,98,102,53,100,51,102,102,1,40,0,140,152,206,145,6,103,2,105,100,1,119,36,48,101,55,99,100,101,102,50,45,49,48,99,50,45,52,52,101,99,45,56,98,54,49,45,49,97,100,101,48,98,102,53,100,51,102,102,33,0,140,152,206,145,6,103,4,110,97,109,101,1,40,0,140,152,206,145,6,103,3,98,105,100,1,119,36,50,54,100,53,99,56,99,49,45,49,99,54,54,45,52,53,57,99,45,98,99,54,99,45,102,52,100,97,49,97,54,54,51,51,52,56,40,0,140,152,206,145,6,103,4,100,101,115,99,1,119,0,40,0,140,152,206,145,6,103,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,0,40,0,140,152,206,145,6,103,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,83,241,173,39,0,203,184,221,173,11,4,36,48,101,55,99,100,101,102,50,45,49,48,99,50,45,52,52,101,99,45,56,98,54,49,45,49,97,100,101,48,98,102,53,100,51,102,102,0,33,0,140,152,206,145,6,103,4,105,99,111,110,1,40,0,140,152,206,145,6,103,10,99,114,101,97,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,33,0,140,152,206,145,6,103,16,108,97,115,116,95,101,100,105,116,101,100,95,116,105,109,101,1,33,0,140,152,206,145,6,103,14,108,97,115,116,95,101,100,105,116,101,100,95,98,121,1,161,140,152,206,145,6,90,1,161,140,152,206,145,6,114,1,161,140,152,206,145,6,113,1,129,140,152,206,145,6,96,1,161,140,152,206,145,6,116,1,161,140,152,206,145,6,117,1,129,140,152,206,145,6,118,1,161,140,152,206,145,6,119,1,161,140,152,206,145,6,120,1,33,0,140,152,206,145,6,103,5,101,120,116,114,97,1,161,140,152,206,145,6,122,1,161,140,152,206,145,6,123,1,129,140,152,206,145,6,121,1,161,140,152,206,145,6,125,1,161,140,152,206,145,6,126,1,161,140,152,206,145,6,105,1,161,140,152,206,145,6,128,1,1,161,140,152,206,145,6,129,1,1,168,140,152,206,145,6,130,1,1,119,2,104,105,161,140,152,206,145,6,131,1,1,161,140,152,206,145,6,132,1,1,161,140,152,206,145,6,124,1,161,140,152,206,145,6,134,1,1,161,140,152,206,145,6,135,1,1,161,140,152,206,145,6,136,1,1,161,140,152,206,145,6,137,1,1,161,140,152,206,145,6,138,1,1,161,140,152,206,145,6,139,1,1,161,140,152,206,145,6,140,1,1,161,140,152,206,145,6,141,1,1,161,140,152,206,145,6,142,1,1,161,140,152,206,145,6,143,1,1,161,140,152,206,145,6,144,1,1,161,140,152,206,145,6,145,1,1,161,140,152,206,145,6,146,1,1,161,140,152,206,145,6,147,1,1,161,140,152,206,145,6,148,1,1,161,140,152,206,145,6,149,1,1,161,140,152,206,145,6,150,1,1,168,140,152,206,145,6,111,1,119,23,123,34,116,121,34,58,48,44,34,118,97,108,117,101,34,58,34,240,159,154,184,34,125,161,140,152,206,145,6,97,1,161,140,152,206,145,6,102,1,136,140,152,206,145,6,127,1,118,2,2,105,100,119,36,50,54,100,53,99,56,99,49,45,49,99,54,54,45,52,53,57,99,45,98,99,54,99,45,102,52,100,97,49,97,54,54,51,51,52,56,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,83,244,231,161,140,152,206,145,6,115,1,161,140,152,206,145,6,155,1,1,161,140,152,206,145,6,156,1,1,136,140,152,206,145,6,157,1,1,118,2,2,105,100,119,36,50,54,100,53,99,56,99,49,45,49,99,54,54,45,52,53,57,99,45,98,99,54,99,45,102,52,100,97,49,97,54,54,51,51,52,56,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,83,244,231,161,140,152,206,145,6,159,1,1,161,140,152,206,145,6,160,1,1,168,140,152,206,145,6,99,1,119,225,1,123,34,99,111,118,101,114,34,58,123,34,116,121,112,101,34,58,34,117,110,115,112,108,97,115,104,34,44,34,118,97,108,117,101,34,58,34,104,116,116,112,115,58,47,47,105,109,97,103,101,115,46,117,110,115,112,108,97,115,104,46,99,111,109,47,112,104,111,116,111,45,49,55,49,52,53,48,56,56,54,50,55,56,56,45,52,52,101,52,53,99,52,51,49,53,100,48,63,99,114,111,112,61,101,110,116,114,111,112,121,38,99,115,61,116,105,110,121,115,114,103,98,38,102,105,116,61,109,97,120,38,102,109,61,106,112,103,38,105,120,105,100,61,77,51,119,49,77,84,69,49,77,122,100,56,77,72,119,120,102,72,74,104,98,109,82,118,98,88,120,56,102,72,120,56,102,72,120,56,102,68,69,51,77,84,89,51,78,122,103,121,77,84,108,56,38,105,120,108,105,98,61,114,98,45,52,46,48,46,51,38,113,61,56,48,38,119,61,49,48,56,48,34,125,125,1,219,227,140,137,6,0,161,140,228,230,243,1,1,34,1,235,178,165,206,5,0,161,255,255,147,249,10,2,72,3,182,172,247,194,5,0,161,245,220,194,52,30,1,161,245,220,194,52,31,1,129,245,220,194,52,32,1,1,145,144,146,185,5,0,161,175,205,156,228,6,9,12,3,160,192,253,131,5,0,161,195,242,227,194,8,0,1,161,195,242,227,194,8,1,1,129,195,242,227,194,8,2,1,1,200,142,208,241,4,0,161,196,154,250,183,6,61,157,1,1,154,235,215,240,4,0,161,173,187,245,170,14,45,13,1,211,166,203,229,4,0,161,234,182,182,157,9,7,195,1,1,141,171,170,217,4,0,161,191,157,147,233,9,31,2,84,201,129,238,197,4,0,161,161,178,132,150,11,34,1,161,161,178,132,150,11,35,1,129,173,252,148,184,13,51,1,161,133,159,138,205,12,49,1,161,133,159,138,205,12,50,1,129,133,159,138,205,12,77,1,168,201,129,238,197,4,3,1,122,4,56,115,160,190,64,16,0,168,201,129,238,197,4,4,1,122,0,0,0,0,102,48,111,8,136,201,129,238,197,4,5,1,118,2,2,105,100,119,36,98,54,51,52,55,97,99,98,45,51,49,55,52,45,52,102,48,101,45,57,56,101,57,45,100,99,99,101,48,55,101,53,100,98,102,55,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,48,111,8,161,133,159,138,205,12,45,1,161,133,159,138,205,12,46,1,129,201,129,238,197,4,8,1,161,158,156,181,152,10,21,1,161,244,226,228,149,2,6,1,129,201,129,238,197,4,11,1,161,133,159,138,205,12,81,1,161,133,159,138,205,12,82,1,129,201,129,238,197,4,14,1,161,201,129,238,197,4,15,1,161,201,129,238,197,4,16,1,129,201,129,238,197,4,17,1,161,201,129,238,197,4,18,1,161,201,129,238,197,4,19,1,129,201,129,238,197,4,20,1,161,201,129,238,197,4,21,1,161,201,129,238,197,4,22,1,129,201,129,238,197,4,23,1,161,201,129,238,197,4,24,1,161,201,129,238,197,4,25,1,129,201,129,238,197,4,26,1,161,201,129,238,197,4,27,1,161,201,129,238,197,4,28,1,129,201,129,238,197,4,29,1,161,133,159,138,205,12,90,1,161,133,159,138,205,12,91,1,129,201,129,238,197,4,32,1,161,201,129,238,197,4,33,1,161,201,129,238,197,4,34,1,129,201,129,238,197,4,35,1,161,173,252,148,184,13,53,1,161,173,252,148,184,13,54,1,129,201,129,238,197,4,38,1,8,0,133,159,138,205,12,66,1,118,1,2,105,100,119,36,101,52,49,48,55,52,55,98,45,53,102,50,102,45,52,53,97,48,45,98,50,102,55,45,56,57,48,97,100,51,48,48,49,51,53,53,161,133,159,138,205,12,65,1,161,201,129,238,197,4,37,1,39,0,203,184,221,173,11,1,36,101,52,49,48,55,52,55,98,45,53,102,50,102,45,52,53,97,48,45,98,50,102,55,45,56,57,48,97,100,51,48,48,49,51,53,53,1,40,0,201,129,238,197,4,45,2,105,100,1,119,36,101,52,49,48,55,52,55,98,45,53,102,50,102,45,52,53,97,48,45,98,50,102,55,45,56,57,48,97,100,51,48,48,49,51,53,53,40,0,201,129,238,197,4,45,4,110,97,109,101,1,119,5,66,111,97,114,100,40,0,201,129,238,197,4,45,3,98,105,100,1,119,36,55,100,50,49,52,56,102,99,45,99,97,99,101,45,52,52,53,50,45,57,99,53,99,45,57,54,101,53,50,101,54,98,102,56,98,53,40,0,201,129,238,197,4,45,4,100,101,115,99,1,119,0,40,0,201,129,238,197,4,45,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,2,33,0,201,129,238,197,4,45,10,99,114,101,97,116,101,100,95,97,116,1,39,0,203,184,221,173,11,4,36,101,52,49,48,55,52,55,98,45,53,102,50,102,45,52,53,97,48,45,98,50,102,55,45,56,57,48,97,100,51,48,48,49,51,53,53,0,40,0,201,129,238,197,4,45,4,105,99,111,110,1,119,0,40,0,201,129,238,197,4,45,10,99,114,101,97,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,33,0,201,129,238,197,4,45,16,108,97,115,116,95,101,100,105,116,101,100,95,116,105,109,101,1,33,0,201,129,238,197,4,45,14,108,97,115,116,95,101,100,105,116,101,100,95,98,121,1,136,201,129,238,197,4,42,1,118,1,2,105,100,119,36,50,49,52,51,101,57,53,100,45,53,100,99,98,45,52,101,48,102,45,98,98,50,99,45,53,48,57,52,52,101,54,101,48,49,57,102,161,201,129,238,197,4,43,1,161,201,129,238,197,4,44,1,39,0,203,184,221,173,11,1,36,50,49,52,51,101,57,53,100,45,53,100,99,98,45,52,101,48,102,45,98,98,50,99,45,53,48,57,52,52,101,54,101,48,49,57,102,1,40,0,201,129,238,197,4,60,2,105,100,1,119,36,50,49,52,51,101,57,53,100,45,53,100,99,98,45,52,101,48,102,45,98,98,50,99,45,53,48,57,52,52,101,54,101,48,49,57,102,40,0,201,129,238,197,4,60,4,110,97,109,101,1,119,8,67,97,108,101,110,100,97,114,40,0,201,129,238,197,4,60,3,98,105,100,1,119,36,55,100,50,49,52,56,102,99,45,99,97,99,101,45,52,52,53,50,45,57,99,53,99,45,57,54,101,53,50,101,54,98,102,56,98,53,40,0,201,129,238,197,4,60,4,100,101,115,99,1,119,0,40,0,201,129,238,197,4,60,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,3,33,0,201,129,238,197,4,60,10,99,114,101,97,116,101,100,95,97,116,1,39,0,203,184,221,173,11,4,36,50,49,52,51,101,57,53,100,45,53,100,99,98,45,52,101,48,102,45,98,98,50,99,45,53,48,57,52,52,101,54,101,48,49,57,102,0,40,0,201,129,238,197,4,60,4,105,99,111,110,1,119,0,40,0,201,129,238,197,4,60,10,99,114,101,97,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,33,0,201,129,238,197,4,60,16,108,97,115,116,95,101,100,105,116,101,100,95,116,105,109,101,1,33,0,201,129,238,197,4,60,14,108,97,115,116,95,101,100,105,116,101,100,95,98,121,1,161,201,129,238,197,4,36,1,161,201,129,238,197,4,59,1,161,133,159,138,205,12,92,1,161,201,129,238,197,4,72,1,161,201,129,238,197,4,73,1,161,201,129,238,197,4,74,1,161,201,129,238,197,4,75,1,161,201,129,238,197,4,76,1,161,201,129,238,197,4,77,1,161,201,129,238,197,4,78,1,161,201,129,238,197,4,79,1,168,201,129,238,197,4,80,1,119,4,71,114,105,100,1,182,143,233,195,4,0,161,205,149,231,236,11,67,111,1,178,203,205,182,4,0,161,227,209,197,253,2,13,1,31,149,249,242,175,4,0,161,213,161,242,209,13,28,1,161,213,161,242,209,13,29,1,33,0,203,184,221,173,11,23,5,101,120,116,114,97,1,161,149,249,242,175,4,0,1,161,149,249,242,175,4,1,1,129,201,191,159,147,14,2,1,161,133,159,138,205,12,122,1,161,133,159,138,205,12,121,1,136,213,161,242,209,13,26,1,118,2,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,60,201,181,2,105,100,119,36,102,48,99,53,57,57,50,49,45,48,52,101,101,45,52,57,55,49,45,57,57,53,99,45,55,57,98,55,102,100,56,99,48,48,101,50,168,149,249,242,175,4,6,1,122,4,56,115,160,190,64,16,0,168,149,249,242,175,4,7,1,122,0,0,0,0,102,60,201,181,161,133,159,138,205,12,107,1,161,133,159,138,205,12,106,1,136,149,249,242,175,4,8,1,118,2,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,60,201,183,2,105,100,119,36,101,57,55,56,55,55,102,53,45,99,51,54,53,45,52,48,50,53,45,57,101,54,97,45,101,53,57,48,99,52,98,49,57,100,98,98,168,149,249,242,175,4,11,1,122,4,56,115,160,190,64,16,0,168,149,249,242,175,4,12,1,122,0,0,0,0,102,60,201,183,161,201,191,159,147,14,0,1,161,201,191,159,147,14,1,1,129,149,249,242,175,4,5,1,161,149,249,242,175,4,16,1,161,149,249,242,175,4,17,1,129,149,249,242,175,4,18,1,161,149,249,242,175,4,19,1,161,149,249,242,175,4,20,1,129,149,249,242,175,4,21,1,161,149,249,242,175,4,22,1,161,149,249,242,175,4,23,1,129,149,249,242,175,4,24,1,161,149,249,242,175,4,25,1,161,149,249,242,175,4,26,1,129,149,249,242,175,4,27,1,12,163,236,177,169,4,0,161,154,243,157,196,14,9,1,161,154,243,157,196,14,10,1,129,154,243,157,196,14,11,1,161,163,236,177,169,4,0,1,161,163,236,177,169,4,1,1,129,163,236,177,169,4,2,1,161,163,236,177,169,4,3,1,161,163,236,177,169,4,4,1,129,163,236,177,169,4,5,1,161,163,236,177,169,4,6,1,161,163,236,177,169,4,7,1,129,163,236,177,169,4,8,1,73,234,153,236,158,4,0,161,165,139,157,171,15,100,1,161,165,139,157,171,15,101,1,129,165,139,157,171,15,102,1,161,165,139,157,171,15,88,1,161,165,139,157,171,15,89,1,129,234,153,236,158,4,2,1,136,165,139,157,171,15,18,1,118,1,2,105,100,119,36,48,99,101,49,51,52,49,53,45,54,99,99,101,45,52,52,57,55,45,57,52,99,54,45,52,55,53,97,100,57,54,99,50,52,57,101,161,165,139,157,171,15,19,1,161,165,139,157,171,15,20,1,39,0,203,184,221,173,11,1,36,48,99,101,49,51,52,49,53,45,54,99,99,101,45,52,52,57,55,45,57,52,99,54,45,52,55,53,97,100,57,54,99,50,52,57,101,1,40,0,234,153,236,158,4,9,2,105,100,1,119,36,48,99,101,49,51,52,49,53,45,54,99,99,101,45,52,52,57,55,45,57,52,99,54,45,52,55,53,97,100,57,54,99,50,52,57,101,33,0,234,153,236,158,4,9,4,110,97,109,101,1,40,0,234,153,236,158,4,9,3,98,105,100,1,119,36,50,54,100,53,99,56,99,49,45,49,99,54,54,45,52,53,57,99,45,98,99,54,99,45,102,52,100,97,49,97,54,54,51,51,52,56,40,0,234,153,236,158,4,9,4,100,101,115,99,1,119,0,40,0,234,153,236,158,4,9,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,2,33,0,234,153,236,158,4,9,10,99,114,101,97,116,101,100,95,97,116,1,39,0,203,184,221,173,11,4,36,48,99,101,49,51,52,49,53,45,54,99,99,101,45,52,52,57,55,45,57,52,99,54,45,52,55,53,97,100,57,54,99,50,52,57,101,0,40,0,234,153,236,158,4,9,4,105,99,111,110,1,119,0,40,0,234,153,236,158,4,9,10,99,114,101,97,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,33,0,234,153,236,158,4,9,16,108,97,115,116,95,101,100,105,116,101,100,95,116,105,109,101,1,33,0,234,153,236,158,4,9,14,108,97,115,116,95,101,100,105,116,101,100,95,98,121,1,161,165,139,157,171,15,33,1,161,234,153,236,158,4,20,1,161,234,153,236,158,4,19,1,129,234,153,236,158,4,5,1,8,0,234,153,236,158,4,16,1,118,1,2,105,100,119,36,101,52,99,56,57,52,50,49,45,49,50,98,50,45,52,100,48,50,45,56,54,51,100,45,50,48,57,52,57,101,101,99,57,50,55,49,168,234,153,236,158,4,15,1,122,0,0,0,0,102,76,39,166,161,234,153,236,158,4,23,1,39,0,203,184,221,173,11,1,36,101,52,99,56,57,52,50,49,45,49,50,98,50,45,52,100,48,50,45,56,54,51,100,45,50,48,57,52,57,101,101,99,57,50,55,49,1,40,0,234,153,236,158,4,28,2,105,100,1,119,36,101,52,99,56,57,52,50,49,45,49,50,98,50,45,52,100,48,50,45,56,54,51,100,45,50,48,57,52,57,101,101,99,57,50,55,49,40,0,234,153,236,158,4,28,4,110,97,109,101,1,119,8,85,110,116,105,116,108,101,100,40,0,234,153,236,158,4,28,3,98,105,100,1,119,36,48,99,101,49,51,52,49,53,45,54,99,99,101,45,52,52,57,55,45,57,52,99,54,45,52,55,53,97,100,57,54,99,50,52,57,101,40,0,234,153,236,158,4,28,4,100,101,115,99,1,119,0,40,0,234,153,236,158,4,28,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,1,40,0,234,153,236,158,4,28,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,76,39,166,39,0,203,184,221,173,11,4,36,101,52,99,56,57,52,50,49,45,49,50,98,50,45,52,100,48,50,45,56,54,51,100,45,50,48,57,52,57,101,101,99,57,50,55,49,0,40,0,234,153,236,158,4,28,4,105,99,111,110,1,119,0,40,0,234,153,236,158,4,28,10,99,114,101,97,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,33,0,234,153,236,158,4,28,16,108,97,115,116,95,101,100,105,116,101,100,95,116,105,109,101,1,33,0,234,153,236,158,4,28,14,108,97,115,116,95,101,100,105,116,101,100,95,98,121,1,161,234,153,236,158,4,22,1,161,234,153,236,158,4,27,1,129,234,153,236,158,4,24,1,161,234,153,236,158,4,40,1,161,234,153,236,158,4,41,1,168,234,153,236,158,4,11,1,119,14,99,104,101,99,107,98,111,120,32,98,111,97,114,100,161,234,153,236,158,4,43,1,161,234,153,236,158,4,44,1,129,234,153,236,158,4,42,1,161,234,153,236,158,4,46,1,161,234,153,236,158,4,47,1,129,234,153,236,158,4,48,1,161,234,153,236,158,4,49,1,161,234,153,236,158,4,50,1,129,234,153,236,158,4,51,1,161,165,139,157,171,15,94,1,161,165,139,157,171,15,95,1,129,234,153,236,158,4,54,1,161,234,153,236,158,4,55,1,161,234,153,236,158,4,56,1,129,234,153,236,158,4,57,1,161,234,153,236,158,4,0,1,161,234,153,236,158,4,1,1,129,234,153,236,158,4,60,1,161,234,153,236,158,4,58,1,161,234,153,236,158,4,59,1,129,234,153,236,158,4,63,1,161,234,153,236,158,4,64,1,161,234,153,236,158,4,65,1,129,234,153,236,158,4,66,1,161,234,153,236,158,4,67,1,161,234,153,236,158,4,68,1,129,234,153,236,158,4,69,1,3,141,205,220,149,4,0,161,214,168,149,214,3,12,1,161,214,168,149,214,3,13,1,129,214,168,149,214,3,14,1,17,193,249,142,142,4,0,161,141,216,158,150,1,0,1,161,141,216,158,150,1,1,1,129,141,216,158,150,1,2,1,161,225,248,138,176,2,45,1,161,222,205,223,235,7,21,1,161,225,248,138,176,2,19,1,129,193,249,142,142,4,2,1,161,193,249,142,142,4,4,1,161,193,249,142,142,4,5,1,129,193,249,142,142,4,6,1,161,193,249,142,142,4,0,1,161,193,249,142,142,4,1,1,129,193,249,142,142,4,9,1,161,193,249,142,142,4,3,1,161,193,249,142,142,4,10,1,161,193,249,142,142,4,11,1,129,193,249,142,142,4,12,1,20,206,220,129,131,4,0,161,235,225,184,133,10,0,1,161,235,225,184,133,10,1,1,129,235,225,184,133,10,2,1,72,233,165,139,246,14,7,1,118,1,2,105,100,119,36,99,55,52,55,53,49,50,51,45,56,50,51,57,45,52,98,98,49,45,56,100,102,53,45,54,56,49,52,56,48,102,101,57,53,52,99,161,233,165,139,246,14,8,1,161,233,165,139,246,14,9,1,39,0,203,184,221,173,11,1,36,99,55,52,55,53,49,50,51,45,56,50,51,57,45,52,98,98,49,45,56,100,102,53,45,54,56,49,52,56,48,102,101,57,53,52,99,1,40,0,206,220,129,131,4,6,2,105,100,1,119,36,99,55,52,55,53,49,50,51,45,56,50,51,57,45,52,98,98,49,45,56,100,102,53,45,54,56,49,52,56,48,102,101,57,53,52,99,40,0,206,220,129,131,4,6,4,110,97,109,101,1,119,8,85,110,116,105,116,108,101,100,40,0,206,220,129,131,4,6,3,98,105,100,1,119,36,57,101,101,98,101,97,48,51,45,51,101,100,53,45,52,50,57,56,45,56,54,98,50,45,97,55,102,55,55,56,53,54,100,52,56,98,40,0,206,220,129,131,4,6,4,100,101,115,99,1,119,0,40,0,206,220,129,131,4,6,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,0,40,0,206,220,129,131,4,6,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,101,241,122,107,39,0,203,184,221,173,11,4,36,99,55,52,55,53,49,50,51,45,56,50,51,57,45,52,98,98,49,45,56,100,102,53,45,54,56,49,52,56,48,102,101,57,53,52,99,0,40,0,206,220,129,131,4,6,4,105,99,111,110,1,119,0,40,0,206,220,129,131,4,6,10,99,114,101,97,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,33,0,206,220,129,131,4,6,16,108,97,115,116,95,101,100,105,116,101,100,95,116,105,109,101,1,33,0,206,220,129,131,4,6,14,108,97,115,116,95,101,100,105,116,101,100,95,98,121,2,161,206,220,129,131,4,16,1,129,206,220,129,131,4,2,1,1,128,252,161,128,4,0,161,232,207,157,148,2,1,33,3,169,197,188,221,3,0,161,154,193,208,134,10,3,1,161,154,193,208,134,10,4,1,129,154,193,208,134,10,5,1,15,214,168,149,214,3,0,161,169,197,188,221,3,0,1,161,169,197,188,221,3,1,1,129,169,197,188,221,3,2,1,161,233,247,183,159,1,0,1,161,158,156,181,152,10,3,1,161,158,156,181,152,10,4,1,129,154,193,208,134,10,8,1,161,214,168,149,214,3,3,1,161,154,193,208,134,10,6,1,161,154,193,208,134,10,7,1,129,214,168,149,214,3,6,1,161,214,168,149,214,3,7,1,161,171,204,155,217,8,0,1,161,171,204,155,217,8,1,1,129,214,168,149,214,3,10,1,1,158,184,218,165,3,0,161,139,152,215,249,10,33,38,1,239,199,189,146,3,0,161,154,235,215,240,4,12,24,1,227,209,197,253,2,0,161,181,175,219,209,12,9,14,15,226,212,179,248,2,0,161,214,139,213,136,8,63,1,161,214,139,213,136,8,64,1,129,214,139,213,136,8,65,1,161,226,212,179,248,2,0,1,161,226,212,179,248,2,1,1,129,226,212,179,248,2,2,1,161,226,212,179,248,2,3,1,161,226,212,179,248,2,4,1,129,226,212,179,248,2,5,1,161,193,249,142,142,4,7,1,161,241,155,213,233,1,8,1,129,226,212,179,248,2,8,1,161,226,212,179,248,2,6,1,161,226,212,179,248,2,7,1,129,226,212,179,248,2,11,1,3,157,240,144,231,2,0,161,213,161,242,209,13,32,1,161,213,161,242,209,13,33,1,129,213,161,242,209,13,34,1,1,128,211,179,216,2,0,161,236,229,225,232,8,115,10,1,138,202,240,189,2,0,161,229,153,197,202,7,240,6,134,1,1,200,203,236,184,2,0,161,138,202,240,189,2,133,1,35,49,225,248,138,176,2,0,161,171,142,166,254,1,3,1,161,171,142,166,254,1,4,1,129,171,142,166,254,1,5,1,161,222,205,223,235,7,27,1,161,173,252,148,184,13,45,1,161,173,252,148,184,13,46,1,129,225,248,138,176,2,2,1,161,225,248,138,176,2,4,1,161,225,248,138,176,2,5,1,136,225,248,138,176,2,6,1,118,2,2,105,100,119,36,100,100,98,57,51,98,97,55,45,48,54,99,55,45,52,49,55,54,45,57,56,50,97,45,100,55,52,50,51,101,48,57,98,52,52,49,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,68,43,241,161,170,140,240,234,9,7,1,161,170,140,240,234,9,8,1,136,225,248,138,176,2,9,1,118,2,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,68,43,243,2,105,100,119,36,52,56,99,53,50,99,102,55,45,98,102,57,56,45,52,51,102,97,45,57,54,97,100,45,98,51,49,97,97,100,101,57,98,48,55,49,161,225,248,138,176,2,3,1,168,225,248,138,176,2,10,1,122,4,56,115,160,190,64,16,0,168,225,248,138,176,2,11,1,122,0,0,0,0,102,68,43,244,136,225,248,138,176,2,12,1,118,2,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,68,43,244,2,105,100,119,36,52,56,99,53,50,99,102,55,45,98,102,57,56,45,52,51,102,97,45,57,54,97,100,45,98,51,49,97,97,100,101,57,98,48,55,49,136,133,159,138,205,12,56,1,118,1,2,105,100,119,36,50,99,49,101,101,57,53,97,45,49,98,48,57,45,52,97,49,102,45,56,100,53,101,45,53,48,49,98,99,52,56,54,49,97,57,100,161,133,159,138,205,12,57,1,161,222,205,223,235,7,22,1,39,0,203,184,221,173,11,1,36,50,99,49,101,101,57,53,97,45,49,98,48,57,45,52,97,49,102,45,56,100,53,101,45,53,48,49,98,99,52,56,54,49,97,57,100,1,40,0,225,248,138,176,2,20,2,105,100,1,119,36,50,99,49,101,101,57,53,97,45,49,98,48,57,45,52,97,49,102,45,56,100,53,101,45,53,48,49,98,99,52,56,54,49,97,57,100,40,0,225,248,138,176,2,20,4,110,97,109,101,1,119,8,85,110,116,105,116,108,101,100,40,0,225,248,138,176,2,20,3,98,105,100,1,119,36,50,54,100,53,99,56,99,49,45,49,99,54,54,45,52,53,57,99,45,98,99,54,99,45,102,52,100,97,49,97,54,54,51,51,52,56,40,0,225,248,138,176,2,20,4,100,101,115,99,1,119,0,40,0,225,248,138,176,2,20,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,1,33,0,225,248,138,176,2,20,10,99,114,101,97,116,101,100,95,97,116,1,39,0,203,184,221,173,11,4,36,50,99,49,101,101,57,53,97,45,49,98,48,57,45,52,97,49,102,45,56,100,53,101,45,53,48,49,98,99,52,56,54,49,97,57,100,0,40,0,225,248,138,176,2,20,4,105,99,111,110,1,119,0,40,0,225,248,138,176,2,20,10,99,114,101,97,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,33,0,225,248,138,176,2,20,16,108,97,115,116,95,101,100,105,116,101,100,95,116,105,109,101,1,33,0,225,248,138,176,2,20,14,108,97,115,116,95,101,100,105,116,101,100,95,98,121,1,161,225,248,138,176,2,13,1,161,225,248,138,176,2,31,1,161,225,248,138,176,2,30,1,129,225,248,138,176,2,16,1,161,225,248,138,176,2,33,1,161,225,248,138,176,2,34,1,129,225,248,138,176,2,35,1,161,225,248,138,176,2,36,1,161,225,248,138,176,2,37,1,129,225,248,138,176,2,38,1,161,225,248,138,176,2,0,1,161,225,248,138,176,2,1,1,129,225,248,138,176,2,41,1,161,225,248,138,176,2,32,1,161,225,248,138,176,2,42,1,161,225,248,138,176,2,43,1,129,225,248,138,176,2,44,1,29,244,226,228,149,2,0,39,0,203,184,221,173,11,2,7,112,114,105,118,97,116,101,1,161,158,156,181,152,10,24,1,161,207,228,238,162,8,24,1,129,207,228,238,162,8,49,1,8,0,158,156,181,152,10,16,1,118,1,2,105,100,119,36,48,53,51,51,50,98,97,52,45,97,54,57,48,45,52,50,57,51,45,57,56,54,54,45,56,52,100,97,99,55,102,101,50,97,101,97,168,158,156,181,152,10,15,1,122,0,0,0,0,101,251,252,70,161,158,156,181,152,10,22,1,39,0,203,184,221,173,11,1,36,48,53,51,51,50,98,97,52,45,97,54,57,48,45,52,50,57,51,45,57,56,54,54,45,56,52,100,97,99,55,102,101,50,97,101,97,1,40,0,244,226,228,149,2,7,2,105,100,1,119,36,48,53,51,51,50,98,97,52,45,97,54,57,48,45,52,50,57,51,45,57,56,54,54,45,56,52,100,97,99,55,102,101,50,97,101,97,40,0,244,226,228,149,2,7,4,110,97,109,101,1,119,8,85,110,116,105,116,108,101,100,40,0,244,226,228,149,2,7,3,98,105,100,1,119,36,100,49,52,50,51,57,101,57,45,50,98,102,55,45,52,56,50,52,45,57,51,101,99,45,52,102,51,99,99,53,54,49,54,55,50,48,40,0,244,226,228,149,2,7,4,100,101,115,99,1,119,0,40,0,244,226,228,149,2,7,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,1,40,0,244,226,228,149,2,7,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,101,251,252,70,39,0,203,184,221,173,11,4,36,48,53,51,51,50,98,97,52,45,97,54,57,48,45,52,50,57,51,45,57,56,54,54,45,56,52,100,97,99,55,102,101,50,97,101,97,0,40,0,244,226,228,149,2,7,4,105,99,111,110,1,119,0,40,0,244,226,228,149,2,7,10,99,114,101,97,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,33,0,244,226,228,149,2,7,16,108,97,115,116,95,101,100,105,116,101,100,95,116,105,109,101,1,33,0,244,226,228,149,2,7,14,108,97,115,116,95,101,100,105,116,101,100,95,98,121,2,161,244,226,228,149,2,17,1,39,0,244,226,228,149,2,0,18,51,48,52,49,50,48,49,48,57,48,55,49,51,51,57,53,50,48,0,8,0,244,226,228,149,2,21,1,118,2,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,101,251,252,70,2,105,100,119,36,48,53,51,51,50,98,97,52,45,97,54,57,48,45,52,50,57,51,45,57,56,54,54,45,56,52,100,97,99,55,102,101,50,97,101,97,161,207,228,238,162,8,37,1,161,244,226,228,149,2,19,1,161,244,226,228,149,2,20,1,129,244,226,228,149,2,3,1,161,244,226,228,149,2,24,1,161,244,226,228,149,2,25,1,129,244,226,228,149,2,26,1,1,232,207,157,148,2,0,161,141,171,170,217,4,1,2,6,171,142,166,254,1,0,161,143,184,153,180,6,3,1,161,143,184,153,180,6,4,1,129,143,184,153,180,6,5,1,161,171,142,166,254,1,0,1,161,171,142,166,254,1,1,1,129,171,142,166,254,1,2,1,1,140,228,230,243,1,0,161,250,198,166,187,7,1,2,3,147,206,229,235,1,0,161,229,154,128,197,12,3,1,161,229,154,128,197,12,4,1,129,162,159,252,196,11,2,1,52,241,155,213,233,1,0,161,234,153,236,158,4,70,1,161,234,153,236,158,4,71,1,129,234,153,236,158,4,72,1,161,234,153,236,158,4,52,1,161,234,153,236,158,4,53,1,129,241,155,213,233,1,2,1,136,234,153,236,158,4,6,1,118,1,2,105,100,119,36,101,101,51,97,101,56,99,101,45,57,53,57,97,45,52,100,102,51,45,56,55,51,52,45,52,48,98,53,51,53,102,102,56,56,101,51,161,234,153,236,158,4,7,1,161,234,153,236,158,4,8,1,39,0,203,184,221,173,11,1,36,101,101,51,97,101,56,99,101,45,57,53,57,97,45,52,100,102,51,45,56,55,51,52,45,52,48,98,53,51,53,102,102,56,56,101,51,1,40,0,241,155,213,233,1,9,2,105,100,1,119,36,101,101,51,97,101,56,99,101,45,57,53,57,97,45,52,100,102,51,45,56,55,51,52,45,52,48,98,53,51,53,102,102,56,56,101,51,33,0,241,155,213,233,1,9,4,110,97,109,101,1,40,0,241,155,213,233,1,9,3,98,105,100,1,119,36,50,54,100,53,99,56,99,49,45,49,99,54,54,45,52,53,57,99,45,98,99,54,99,45,102,52,100,97,49,97,54,54,51,51,52,56,40,0,241,155,213,233,1,9,4,100,101,115,99,1,119,0,40,0,241,155,213,233,1,9,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,3,33,0,241,155,213,233,1,9,10,99,114,101,97,116,101,100,95,97,116,1,39,0,203,184,221,173,11,4,36,101,101,51,97,101,56,99,101,45,57,53,57,97,45,52,100,102,51,45,56,55,51,52,45,52,48,98,53,51,53,102,102,56,56,101,51,0,40,0,241,155,213,233,1,9,4,105,99,111,110,1,119,0,40,0,241,155,213,233,1,9,10,99,114,101,97,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,33,0,241,155,213,233,1,9,16,108,97,115,116,95,101,100,105,116,101,100,95,116,105,109,101,1,33,0,241,155,213,233,1,9,14,108,97,115,116,95,101,100,105,116,101,100,95,98,121,1,161,234,153,236,158,4,21,1,161,241,155,213,233,1,20,1,161,241,155,213,233,1,19,1,129,241,155,213,233,1,5,1,161,241,155,213,233,1,22,1,161,241,155,213,233,1,23,1,161,241,155,213,233,1,11,1,161,241,155,213,233,1,25,1,161,241,155,213,233,1,26,1,161,241,155,213,233,1,27,1,161,241,155,213,233,1,28,1,161,241,155,213,233,1,29,1,161,241,155,213,233,1,30,1,161,241,155,213,233,1,31,1,161,241,155,213,233,1,32,1,161,241,155,213,233,1,33,1,161,241,155,213,233,1,34,1,161,241,155,213,233,1,35,1,161,241,155,213,233,1,36,1,161,241,155,213,233,1,37,1,161,241,155,213,233,1,38,1,161,241,155,213,233,1,39,1,161,241,155,213,233,1,40,1,161,241,155,213,233,1,41,1,161,241,155,213,233,1,42,1,161,241,155,213,233,1,43,1,161,241,155,213,233,1,44,1,161,241,155,213,233,1,45,1,161,241,155,213,233,1,46,1,161,241,155,213,233,1,47,1,168,241,155,213,233,1,48,1,119,8,67,97,108,101,110,100,97,114,1,240,253,240,229,1,0,161,178,203,205,182,4,0,79,20,176,154,159,227,1,0,33,0,203,184,221,173,11,2,7,112,114,105,118,97,116,101,1,136,130,180,254,251,6,0,1,118,1,2,105,100,119,36,54,53,98,48,54,100,98,56,45,55,48,54,49,45,52,98,102,54,45,98,51,49,53,45,55,53,56,99,48,100,100,50,53,99,100,102,161,130,180,254,251,6,1,1,161,130,180,254,251,6,2,1,39,0,203,184,221,173,11,1,36,54,53,98,48,54,100,98,56,45,55,48,54,49,45,52,98,102,54,45,98,51,49,53,45,55,53,56,99,48,100,100,50,53,99,100,102,1,40,0,176,154,159,227,1,4,2,105,100,1,119,36,54,53,98,48,54,100,98,56,45,55,48,54,49,45,52,98,102,54,45,98,51,49,53,45,55,53,56,99,48,100,100,50,53,99,100,102,40,0,176,154,159,227,1,4,4,110,97,109,101,1,119,0,40,0,176,154,159,227,1,4,3,98,105,100,1,119,36,57,101,101,98,101,97,48,51,45,51,101,100,53,45,52,50,57,56,45,56,54,98,50,45,97,55,102,55,55,56,53,54,100,52,56,98,40,0,176,154,159,227,1,4,4,100,101,115,99,1,119,0,40,0,176,154,159,227,1,4,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,0,40,0,176,154,159,227,1,4,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,12,194,112,39,0,203,184,221,173,11,4,36,54,53,98,48,54,100,98,56,45,55,48,54,49,45,52,98,102,54,45,98,51,49,53,45,55,53,56,99,48,100,100,50,53,99,100,102,0,40,0,176,154,159,227,1,4,4,105,99,111,110,1,119,0,40,0,176,154,159,227,1,4,10,99,114,101,97,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,33,0,176,154,159,227,1,4,16,108,97,115,116,95,101,100,105,116,101,100,95,116,105,109,101,1,33,0,176,154,159,227,1,4,14,108,97,115,116,95,101,100,105,116,101,100,95,98,121,1,161,207,228,238,162,8,37,1,161,176,154,159,227,1,15,1,161,176,154,159,227,1,14,1,129,207,228,238,162,8,49,1,2,175,147,217,214,1,0,161,246,185,174,192,6,87,1,161,246,185,174,192,6,89,32,6,202,160,246,212,1,0,161,178,162,190,217,10,0,1,161,178,162,190,217,10,1,1,161,178,162,190,217,10,2,1,161,202,160,246,212,1,0,1,161,202,160,246,212,1,1,1,161,202,160,246,212,1,2,1,39,137,164,190,210,1,0,168,166,201,221,141,13,69,1,122,4,56,115,160,190,64,16,0,168,166,201,221,141,13,70,1,122,0,0,0,0,102,86,193,57,136,166,201,221,141,13,71,1,118,2,2,105,100,119,36,50,97,54,101,53,101,50,49,45,97,57,51,56,45,52,53,97,53,45,97,52,52,53,45,100,48,98,55,49,52,57,53,98,48,55,55,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,86,193,57,161,174,151,139,93,254,1,1,161,174,151,139,93,255,1,1,136,162,238,198,212,7,2,1,118,2,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,87,240,30,2,105,100,119,36,101,52,49,48,55,52,55,98,45,53,102,50,102,45,52,53,97,48,45,98,50,102,55,45,56,57,48,97,100,51,48,48,49,51,53,53,161,247,200,243,247,14,15,1,168,137,164,190,210,1,3,1,122,4,56,115,160,190,64,16,0,168,137,164,190,210,1,4,1,122,0,0,0,0,102,87,240,30,136,137,164,190,210,1,5,1,118,2,2,105,100,119,36,101,52,49,48,55,52,55,98,45,53,102,50,102,45,52,53,97,48,45,98,50,102,55,45,56,57,48,97,100,51,48,48,49,51,53,53,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,87,240,30,39,0,203,184,221,173,11,1,36,48,51,50,55,98,54,57,52,45,50,100,101,51,45,53,101,48,48,45,98,51,54,56,45,57,56,99,97,49,56,50,51,48,102,97,57,1,40,0,137,164,190,210,1,10,2,105,100,1,119,36,48,51,50,55,98,54,57,52,45,50,100,101,51,45,53,101,48,48,45,98,51,54,56,45,57,56,99,97,49,56,50,51,48,102,97,57,40,0,137,164,190,210,1,10,4,110,97,109,101,1,119,8,85,110,116,105,116,108,101,100,40,0,137,164,190,210,1,10,3,98,105,100,1,119,36,48,51,50,55,98,54,57,52,45,50,100,101,51,45,53,101,48,48,45,98,51,54,56,45,57,56,99,97,49,56,50,51,48,102,97,57,40,0,137,164,190,210,1,10,4,100,101,115,99,1,119,0,40,0,137,164,190,210,1,10,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,0,40,0,137,164,190,210,1,10,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,87,240,32,39,0,203,184,221,173,11,4,36,48,51,50,55,98,54,57,52,45,50,100,101,51,45,53,101,48,48,45,98,51,54,56,45,57,56,99,97,49,56,50,51,48,102,97,57,0,40,0,137,164,190,210,1,10,4,105,99,111,110,1,119,0,40,0,137,164,190,210,1,10,10,99,114,101,97,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,33,0,137,164,190,210,1,10,16,108,97,115,116,95,101,100,105,116,101,100,95,116,105,109,101,1,33,0,137,164,190,210,1,10,14,108,97,115,116,95,101,100,105,116,101,100,95,98,121,1,168,137,164,190,210,1,21,1,122,4,56,115,160,190,64,16,0,168,137,164,190,210,1,20,1,122,0,0,0,0,102,87,240,32,40,0,137,164,190,210,1,10,5,101,120,116,114,97,1,119,36,123,34,99,111,118,101,114,34,58,123,34,116,121,112,101,34,58,34,110,111,110,101,34,44,34,118,97,108,117,101,34,58,34,34,125,125,161,137,164,190,210,1,6,1,161,166,201,221,141,13,13,1,161,166,201,221,141,13,14,1,136,137,164,190,210,1,9,1,118,2,2,105,100,119,36,55,100,50,49,52,56,102,99,45,99,97,99,101,45,52,52,53,50,45,57,99,53,99,45,57,54,101,53,50,101,54,98,102,56,98,53,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,88,24,223,161,137,164,190,210,1,26,1,161,137,164,190,210,1,27,1,136,137,164,190,210,1,28,1,118,2,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,88,24,223,2,105,100,119,36,55,100,50,49,52,56,102,99,45,99,97,99,101,45,52,52,53,50,45,57,99,53,99,45,57,54,101,53,50,101,54,98,102,56,98,53,161,162,238,198,212,7,0,1,161,162,238,198,212,7,1,1,136,137,164,190,210,1,31,1,118,2,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,88,31,80,2,105,100,119,36,50,54,100,53,99,56,99,49,45,49,99,54,54,45,52,53,57,99,45,98,99,54,99,45,102,52,100,97,49,97,54,54,51,51,52,56,161,137,164,190,210,1,25,1,161,137,164,190,210,1,32,1,161,137,164,190,210,1,33,1,136,137,164,190,210,1,34,1,118,2,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,88,31,80,2,105,100,119,36,50,54,100,53,99,56,99,49,45,49,99,54,54,45,52,53,57,99,45,98,99,54,99,45,102,52,100,97,49,97,54,54,51,51,52,56,1,152,252,186,192,1,0,161,211,166,203,229,4,194,1,2,3,248,136,168,181,1,0,161,197,254,154,201,10,0,1,161,197,254,154,201,10,1,1,129,197,254,154,201,10,2,1,1,234,187,164,181,1,0,161,253,205,145,137,11,5,24,3,164,188,201,172,1,0,161,222,205,223,235,7,28,1,161,222,205,223,235,7,29,1,129,222,205,223,235,7,30,1,4,233,247,183,159,1,0,161,244,226,228,149,2,23,1,161,248,153,216,10,0,1,161,248,153,216,10,1,1,129,187,173,214,176,15,2,1,1,190,139,191,155,1,0,161,157,207,243,216,6,1,2,3,141,216,158,150,1,0,161,225,248,138,176,2,46,1,161,225,248,138,176,2,47,1,129,225,248,138,176,2,48,1,1,213,255,156,145,1,0,161,177,219,160,167,7,3,2,38,170,255,211,105,0,161,140,152,206,145,6,162,1,1,161,140,152,206,145,6,163,1,1,136,140,152,206,145,6,161,1,1,118,2,2,105,100,119,36,50,54,100,53,99,56,99,49,45,49,99,54,54,45,52,53,57,99,45,98,99,54,99,45,102,52,100,97,49,97,54,54,51,51,52,56,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,83,246,42,161,245,220,194,52,3,1,161,245,220,194,52,4,1,129,245,220,194,52,11,1,161,140,152,206,145,6,158,1,1,161,170,255,211,105,3,1,161,170,255,211,105,4,1,129,170,255,211,105,5,1,161,140,152,206,145,6,37,1,161,140,152,206,145,6,38,1,129,170,255,211,105,9,1,161,170,255,211,105,6,1,161,170,255,211,105,10,1,161,170,255,211,105,11,1,129,170,255,211,105,12,1,161,245,220,194,52,18,1,161,245,220,194,52,19,1,129,170,255,211,105,16,1,161,170,255,211,105,13,1,161,170,255,211,105,17,1,161,170,255,211,105,18,1,129,170,255,211,105,19,1,161,170,255,211,105,14,1,161,170,255,211,105,15,1,129,170,255,211,105,23,1,161,170,255,211,105,20,1,168,170,255,211,105,24,1,122,4,56,115,160,190,64,16,0,168,170,255,211,105,25,1,122,0,0,0,0,102,83,252,197,136,170,255,211,105,26,1,118,2,2,105,100,119,36,101,101,51,97,101,56,99,101,45,57,53,57,97,45,52,100,102,51,45,56,55,51,52,45,52,48,98,53,51,53,102,102,56,56,101,51,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,83,252,197,161,245,220,194,52,24,1,161,245,220,194,52,25,1,129,170,255,211,105,30,1,161,170,255,211,105,27,1,161,170,255,211,105,31,1,161,170,255,211,105,32,1,129,170,255,211,105,33,1,179,2,174,151,139,93,0,161,177,161,136,243,11,7,1,161,177,161,136,243,11,8,1,129,177,161,136,243,11,9,1,161,177,161,136,243,11,0,1,161,177,161,136,243,11,1,1,129,174,151,139,93,2,1,161,177,161,136,243,11,6,1,161,174,151,139,93,3,1,161,174,151,139,93,4,1,129,174,151,139,93,5,1,161,165,139,157,171,15,14,1,161,165,139,157,171,15,13,1,129,174,151,139,93,9,1,161,174,151,139,93,6,1,161,174,151,139,93,10,1,161,174,151,139,93,11,1,129,174,151,139,93,12,1,161,174,151,139,93,13,1,161,174,151,139,93,7,1,161,174,151,139,93,8,1,129,174,151,139,93,16,1,161,174,151,139,93,18,1,161,174,151,139,93,19,1,129,174,151,139,93,20,1,161,241,155,213,233,1,3,1,161,241,155,213,233,1,4,1,129,174,151,139,93,23,1,161,174,151,139,93,17,1,161,174,151,139,93,24,1,161,174,151,139,93,25,1,129,174,151,139,93,26,1,161,174,151,139,93,21,1,161,174,151,139,93,22,1,129,174,151,139,93,30,1,161,174,151,139,93,27,1,161,174,151,139,93,31,1,161,174,151,139,93,32,1,129,174,151,139,93,33,1,161,174,151,139,93,0,1,161,174,151,139,93,1,1,129,174,151,139,93,37,1,161,174,151,139,93,34,1,161,174,151,139,93,38,1,161,174,151,139,93,39,1,129,174,151,139,93,40,1,39,0,203,184,221,173,11,1,36,102,53,54,98,100,102,48,102,45,57,48,99,56,45,53,51,102,98,45,57,55,100,57,45,97,100,53,56,54,48,100,50,98,55,97,48,1,40,0,174,151,139,93,45,2,105,100,1,119,36,102,53,54,98,100,102,48,102,45,57,48,99,56,45,53,51,102,98,45,57,55,100,57,45,97,100,53,56,54,48,100,50,98,55,97,48,40,0,174,151,139,93,45,4,110,97,109,101,1,119,8,85,110,116,105,116,108,101,100,40,0,174,151,139,93,45,3,98,105,100,1,119,36,102,53,54,98,100,102,48,102,45,57,48,99,56,45,53,51,102,98,45,57,55,100,57,45,97,100,53,56,54,48,100,50,98,55,97,48,40,0,174,151,139,93,45,4,100,101,115,99,1,119,0,40,0,174,151,139,93,45,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,0,40,0,174,151,139,93,45,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,77,101,46,39,0,203,184,221,173,11,4,36,102,53,54,98,100,102,48,102,45,57,48,99,56,45,53,51,102,98,45,57,55,100,57,45,97,100,53,56,54,48,100,50,98,55,97,48,0,40,0,174,151,139,93,45,4,105,99,111,110,1,119,0,40,0,174,151,139,93,45,10,99,114,101,97,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,33,0,174,151,139,93,45,16,108,97,115,116,95,101,100,105,116,101,100,95,116,105,109,101,1,33,0,174,151,139,93,45,14,108,97,115,116,95,101,100,105,116,101,100,95,98,121,1,168,174,151,139,93,56,1,122,4,56,115,160,190,64,16,0,168,174,151,139,93,55,1,122,0,0,0,0,102,77,101,47,40,0,174,151,139,93,45,5,101,120,116,114,97,1,119,36,123,34,99,111,118,101,114,34,58,123,34,116,121,112,101,34,58,34,110,111,110,101,34,44,34,118,97,108,117,101,34,58,34,34,125,125,161,174,151,139,93,42,1,161,174,151,139,93,43,1,129,174,151,139,93,44,1,161,174,151,139,93,60,1,161,174,151,139,93,61,1,129,174,151,139,93,62,1,161,234,153,236,158,4,61,1,161,234,153,236,158,4,62,1,129,174,151,139,93,65,1,161,174,151,139,93,41,1,161,174,151,139,93,66,1,161,174,151,139,93,67,1,129,174,151,139,93,68,1,161,174,151,139,93,63,1,161,174,151,139,93,64,1,129,174,151,139,93,72,1,161,174,151,139,93,69,1,161,174,151,139,93,73,1,161,174,151,139,93,74,1,129,174,151,139,93,75,1,161,174,151,139,93,70,1,161,174,151,139,93,71,1,129,174,151,139,93,79,1,161,174,151,139,93,76,1,161,174,151,139,93,80,1,161,174,151,139,93,81,1,129,174,151,139,93,82,1,161,174,151,139,93,83,1,161,174,151,139,93,28,1,161,174,151,139,93,29,1,129,174,151,139,93,86,1,161,174,151,139,93,87,1,161,201,129,238,197,4,71,1,161,201,129,238,197,4,70,1,129,174,151,139,93,90,1,161,174,151,139,93,88,1,161,174,151,139,93,89,1,129,174,151,139,93,94,1,161,174,151,139,93,91,1,161,174,151,139,93,35,1,161,174,151,139,93,36,1,129,174,151,139,93,97,1,161,174,151,139,93,99,1,161,174,151,139,93,100,1,129,174,151,139,93,101,1,161,174,151,139,93,92,1,161,174,151,139,93,93,1,129,174,151,139,93,104,1,161,174,151,139,93,102,1,161,174,151,139,93,103,1,129,174,151,139,93,107,1,161,174,151,139,93,108,1,161,174,151,139,93,109,1,129,174,151,139,93,110,1,39,0,203,184,221,173,11,1,36,50,49,97,100,55,99,57,48,45,98,102,53,57,45,53,100,97,56,45,57,97,98,99,45,53,98,50,50,102,56,102,102,54,99,52,101,1,40,0,174,151,139,93,114,2,105,100,1,119,36,50,49,97,100,55,99,57,48,45,98,102,53,57,45,53,100,97,56,45,57,97,98,99,45,53,98,50,50,102,56,102,102,54,99,52,101,40,0,174,151,139,93,114,4,110,97,109,101,1,119,8,85,110,116,105,116,108,101,100,40,0,174,151,139,93,114,3,98,105,100,1,119,36,50,49,97,100,55,99,57,48,45,98,102,53,57,45,53,100,97,56,45,57,97,98,99,45,53,98,50,50,102,56,102,102,54,99,52,101,40,0,174,151,139,93,114,4,100,101,115,99,1,119,0,40,0,174,151,139,93,114,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,0,40,0,174,151,139,93,114,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,77,101,175,39,0,203,184,221,173,11,4,36,50,49,97,100,55,99,57,48,45,98,102,53,57,45,53,100,97,56,45,57,97,98,99,45,53,98,50,50,102,56,102,102,54,99,52,101,0,40,0,174,151,139,93,114,4,105,99,111,110,1,119,0,40,0,174,151,139,93,114,10,99,114,101,97,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,33,0,174,151,139,93,114,16,108,97,115,116,95,101,100,105,116,101,100,95,116,105,109,101,1,33,0,174,151,139,93,114,14,108,97,115,116,95,101,100,105,116,101,100,95,98,121,1,168,174,151,139,93,125,1,122,4,56,115,160,190,64,16,0,168,174,151,139,93,124,1,122,0,0,0,0,102,77,101,175,40,0,174,151,139,93,114,5,101,120,116,114,97,1,119,36,123,34,99,111,118,101,114,34,58,123,34,116,121,112,101,34,58,34,110,111,110,101,34,44,34,118,97,108,117,101,34,58,34,34,125,125,161,174,151,139,93,111,1,161,174,151,139,93,112,1,129,174,151,139,93,113,1,161,174,151,139,93,129,1,1,161,174,151,139,93,130,1,1,129,174,151,139,93,131,1,1,161,174,151,139,93,77,1,161,174,151,139,93,78,1,129,174,151,139,93,134,1,1,161,174,151,139,93,98,1,161,174,151,139,93,135,1,1,161,174,151,139,93,136,1,1,129,174,151,139,93,137,1,1,161,174,151,139,93,132,1,1,161,174,151,139,93,133,1,1,129,174,151,139,93,141,1,1,161,174,151,139,93,138,1,1,161,174,151,139,93,142,1,1,161,174,151,139,93,143,1,1,129,174,151,139,93,144,1,1,161,174,151,139,93,95,1,161,174,151,139,93,96,1,129,174,151,139,93,148,1,1,161,174,151,139,93,145,1,1,161,174,151,139,93,149,1,1,161,174,151,139,93,150,1,1,129,174,151,139,93,151,1,1,161,174,151,139,93,146,1,1,161,174,151,139,93,147,1,1,129,174,151,139,93,155,1,1,161,174,151,139,93,152,1,1,161,174,151,139,93,156,1,1,161,174,151,139,93,157,1,1,129,174,151,139,93,158,1,1,161,174,151,139,93,139,1,1,161,174,151,139,93,140,1,1,129,174,151,139,93,162,1,1,161,174,151,139,93,159,1,1,161,174,151,139,93,163,1,1,161,174,151,139,93,164,1,1,129,174,151,139,93,165,1,1,161,174,151,139,93,160,1,1,161,174,151,139,93,161,1,1,129,174,151,139,93,169,1,1,161,174,151,139,93,166,1,1,161,174,151,139,93,170,1,1,161,174,151,139,93,171,1,1,129,174,151,139,93,172,1,1,8,0,241,155,213,233,1,16,1,118,1,2,105,100,119,36,54,54,97,54,102,51,98,99,45,99,55,56,102,45,52,102,55,52,45,97,48,57,101,45,48,56,100,52,55,49,55,98,102,49,102,100,161,241,155,213,233,1,15,1,161,174,151,139,93,175,1,1,39,0,203,184,221,173,11,1,36,54,54,97,54,102,51,98,99,45,99,55,56,102,45,52,102,55,52,45,97,48,57,101,45,48,56,100,52,55,49,55,98,102,49,102,100,1,40,0,174,151,139,93,180,1,2,105,100,1,119,36,54,54,97,54,102,51,98,99,45,99,55,56,102,45,52,102,55,52,45,97,48,57,101,45,48,56,100,52,55,49,55,98,102,49,102,100,40,0,174,151,139,93,180,1,4,110,97,109,101,1,119,4,71,114,105,100,40,0,174,151,139,93,180,1,3,98,105,100,1,119,36,101,101,51,97,101,56,99,101,45,57,53,57,97,45,52,100,102,51,45,56,55,51,52,45,52,48,98,53,51,53,102,102,56,56,101,51,40,0,174,151,139,93,180,1,4,100,101,115,99,1,119,0,40,0,174,151,139,93,180,1,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,1,40,0,174,151,139,93,180,1,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,77,165,65,39,0,203,184,221,173,11,4,36,54,54,97,54,102,51,98,99,45,99,55,56,102,45,52,102,55,52,45,97,48,57,101,45,48,56,100,52,55,49,55,98,102,49,102,100,0,40,0,174,151,139,93,180,1,4,105,99,111,110,1,119,0,40,0,174,151,139,93,180,1,10,99,114,101,97,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,40,0,174,151,139,93,180,1,16,108,97,115,116,95,101,100,105,116,101,100,95,116,105,109,101,1,122,0,0,0,0,102,77,165,65,40,0,174,151,139,93,180,1,14,108,97,115,116,95,101,100,105,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,39,0,203,184,221,173,11,1,36,49,102,98,50,53,48,49,57,45,52,51,57,52,45,53,57,54,53,45,97,49,50,98,45,49,98,48,101,52,99,57,55,55,48,55,99,1,40,0,174,151,139,93,192,1,2,105,100,1,119,36,49,102,98,50,53,48,49,57,45,52,51,57,52,45,53,57,54,53,45,97,49,50,98,45,49,98,48,101,52,99,57,55,55,48,55,99,40,0,174,151,139,93,192,1,4,110,97,109,101,1,119,8,85,110,116,105,116,108,101,100,40,0,174,151,139,93,192,1,3,98,105,100,1,119,36,49,102,98,50,53,48,49,57,45,52,51,57,52,45,53,57,54,53,45,97,49,50,98,45,49,98,48,101,52,99,57,55,55,48,55,99,40,0,174,151,139,93,192,1,4,100,101,115,99,1,119,0,40,0,174,151,139,93,192,1,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,0,40,0,174,151,139,93,192,1,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,77,166,20,39,0,203,184,221,173,11,4,36,49,102,98,50,53,48,49,57,45,52,51,57,52,45,53,57,54,53,45,97,49,50,98,45,49,98,48,101,52,99,57,55,55,48,55,99,0,40,0,174,151,139,93,192,1,4,105,99,111,110,1,119,0,40,0,174,151,139,93,192,1,10,99,114,101,97,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,33,0,174,151,139,93,192,1,16,108,97,115,116,95,101,100,105,116,101,100,95,116,105,109,101,1,33,0,174,151,139,93,192,1,14,108,97,115,116,95,101,100,105,116,101,100,95,98,121,1,168,174,151,139,93,203,1,1,122,4,56,115,160,190,64,16,0,168,174,151,139,93,202,1,1,122,0,0,0,0,102,77,166,20,40,0,174,151,139,93,192,1,5,101,120,116,114,97,1,119,36,123,34,99,111,118,101,114,34,58,123,34,116,121,112,101,34,58,34,110,111,110,101,34,44,34,118,97,108,117,101,34,58,34,34,125,125,39,0,203,184,221,173,11,1,36,54,97,102,100,54,57,56,52,45,48,51,98,52,45,53,53,57,100,45,98,49,52,54,45,52,49,100,98,97,54,49,50,49,48,98,56,1,40,0,174,151,139,93,207,1,2,105,100,1,119,36,54,97,102,100,54,57,56,52,45,48,51,98,52,45,53,53,57,100,45,98,49,52,54,45,52,49,100,98,97,54,49,50,49,48,98,56,40,0,174,151,139,93,207,1,4,110,97,109,101,1,119,8,85,110,116,105,116,108,101,100,40,0,174,151,139,93,207,1,3,98,105,100,1,119,36,54,97,102,100,54,57,56,52,45,48,51,98,52,45,53,53,57,100,45,98,49,52,54,45,52,49,100,98,97,54,49,50,49,48,98,56,40,0,174,151,139,93,207,1,4,100,101,115,99,1,119,0,40,0,174,151,139,93,207,1,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,0,40,0,174,151,139,93,207,1,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,77,183,211,39,0,203,184,221,173,11,4,36,54,97,102,100,54,57,56,52,45,48,51,98,52,45,53,53,57,100,45,98,49,52,54,45,52,49,100,98,97,54,49,50,49,48,98,56,0,40,0,174,151,139,93,207,1,4,105,99,111,110,1,119,0,40,0,174,151,139,93,207,1,10,99,114,101,97,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,33,0,174,151,139,93,207,1,16,108,97,115,116,95,101,100,105,116,101,100,95,116,105,109,101,1,33,0,174,151,139,93,207,1,14,108,97,115,116,95,101,100,105,116,101,100,95,98,121,1,168,174,151,139,93,218,1,1,122,4,56,115,160,190,64,16,0,168,174,151,139,93,217,1,1,122,0,0,0,0,102,77,183,211,40,0,174,151,139,93,207,1,5,101,120,116,114,97,1,119,36,123,34,99,111,118,101,114,34,58,123,34,116,121,112,101,34,58,34,110,111,110,101,34,44,34,118,97,108,117,101,34,58,34,34,125,125,39,0,203,184,221,173,11,1,36,53,54,53,50,101,52,54,54,45,101,52,99,101,45,53,54,57,99,45,98,49,101,54,45,102,100,56,101,48,101,55,100,99,48,100,99,1,40,0,174,151,139,93,222,1,2,105,100,1,119,36,53,54,53,50,101,52,54,54,45,101,52,99,101,45,53,54,57,99,45,98,49,101,54,45,102,100,56,101,48,101,55,100,99,48,100,99,40,0,174,151,139,93,222,1,4,110,97,109,101,1,119,8,85,110,116,105,116,108,101,100,40,0,174,151,139,93,222,1,3,98,105,100,1,119,36,53,54,53,50,101,52,54,54,45,101,52,99,101,45,53,54,57,99,45,98,49,101,54,45,102,100,56,101,48,101,55,100,99,48,100,99,40,0,174,151,139,93,222,1,4,100,101,115,99,1,119,0,40,0,174,151,139,93,222,1,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,0,40,0,174,151,139,93,222,1,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,77,187,249,39,0,203,184,221,173,11,4,36,53,54,53,50,101,52,54,54,45,101,52,99,101,45,53,54,57,99,45,98,49,101,54,45,102,100,56,101,48,101,55,100,99,48,100,99,0,40,0,174,151,139,93,222,1,4,105,99,111,110,1,119,0,40,0,174,151,139,93,222,1,10,99,114,101,97,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,33,0,174,151,139,93,222,1,16,108,97,115,116,95,101,100,105,116,101,100,95,116,105,109,101,1,33,0,174,151,139,93,222,1,14,108,97,115,116,95,101,100,105,116,101,100,95,98,121,1,168,174,151,139,93,233,1,1,122,4,56,115,160,190,64,16,0,168,174,151,139,93,232,1,1,122,0,0,0,0,102,77,187,249,40,0,174,151,139,93,222,1,5,101,120,116,114,97,1,119,36,123,34,99,111,118,101,114,34,58,123,34,116,121,112,101,34,58,34,110,111,110,101,34,44,34,118,97,108,117,101,34,58,34,34,125,125,161,174,151,139,93,167,1,1,161,174,151,139,93,168,1,1,129,174,151,139,93,176,1,1,161,174,151,139,93,173,1,1,161,174,151,139,93,237,1,1,161,174,151,139,93,238,1,1,129,174,151,139,93,239,1,1,161,174,151,139,93,241,1,1,161,174,151,139,93,242,1,1,129,241,130,161,205,7,11,1,161,174,151,139,93,244,1,1,161,174,151,139,93,245,1,1,129,174,151,139,93,246,1,1,161,241,130,161,205,7,3,1,161,241,130,161,205,7,4,1,129,174,151,139,93,249,1,1,161,174,151,139,93,240,1,1,161,174,151,139,93,250,1,1,161,174,151,139,93,251,1,1,129,174,151,139,93,252,1,1,161,174,151,139,93,247,1,1,161,174,151,139,93,248,1,1,129,174,151,139,93,128,2,1,161,174,151,139,93,253,1,1,161,174,151,139,93,129,2,1,161,174,151,139,93,130,2,1,129,174,151,139,93,131,2,1,161,226,212,179,248,2,9,1,161,226,212,179,248,2,10,1,129,174,151,139,93,135,2,1,161,174,151,139,93,132,2,1,161,174,151,139,93,136,2,1,161,174,151,139,93,137,2,1,129,174,151,139,93,138,2,1,161,174,151,139,93,133,2,1,161,174,151,139,93,134,2,1,129,174,151,139,93,142,2,1,161,174,151,139,93,139,2,1,161,174,151,139,93,143,2,1,161,174,151,139,93,144,2,1,129,174,151,139,93,145,2,1,39,0,203,184,221,173,11,1,36,51,50,48,102,56,48,97,100,45,50,51,52,101,45,53,49,55,50,45,97,49,55,50,45,102,55,56,52,54,52,55,50,55,98,100,97,1,40,0,174,151,139,93,150,2,2,105,100,1,119,36,51,50,48,102,56,48,97,100,45,50,51,52,101,45,53,49,55,50,45,97,49,55,50,45,102,55,56,52,54,52,55,50,55,98,100,97,40,0,174,151,139,93,150,2,4,110,97,109,101,1,119,8,85,110,116,105,116,108,101,100,40,0,174,151,139,93,150,2,3,98,105,100,1,119,36,51,50,48,102,56,48,97,100,45,50,51,52,101,45,53,49,55,50,45,97,49,55,50,45,102,55,56,52,54,52,55,50,55,98,100,97,40,0,174,151,139,93,150,2,4,100,101,115,99,1,119,0,40,0,174,151,139,93,150,2,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,0,40,0,174,151,139,93,150,2,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,78,228,14,39,0,203,184,221,173,11,4,36,51,50,48,102,56,48,97,100,45,50,51,52,101,45,53,49,55,50,45,97,49,55,50,45,102,55,56,52,54,52,55,50,55,98,100,97,0,40,0,174,151,139,93,150,2,4,105,99,111,110,1,119,0,40,0,174,151,139,93,150,2,10,99,114,101,97,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,33,0,174,151,139,93,150,2,16,108,97,115,116,95,101,100,105,116,101,100,95,116,105,109,101,1,33,0,174,151,139,93,150,2,14,108,97,115,116,95,101,100,105,116,101,100,95,98,121,1,168,174,151,139,93,161,2,1,122,4,56,115,160,190,64,16,0,168,174,151,139,93,160,2,1,122,0,0,0,0,102,78,228,14,40,0,174,151,139,93,150,2,5,101,120,116,114,97,1,119,36,123,34,99,111,118,101,114,34,58,123,34,116,121,112,101,34,58,34,110,111,110,101,34,44,34,118,97,108,117,101,34,58,34,34,125,125,161,174,151,139,93,14,1,161,174,151,139,93,15,1,129,174,151,139,93,149,2,1,161,174,151,139,93,146,2,1,168,174,151,139,93,165,2,1,122,4,56,115,160,190,64,16,0,161,174,151,139,93,166,2,1,136,174,151,139,93,167,2,1,118,2,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,78,229,144,2,105,100,119,36,97,53,53,54,54,101,52,57,45,102,49,53,54,45,52,49,54,56,45,57,98,50,100,45,49,55,57,50,54,99,53,100,97,51,50,57,161,174,151,139,93,147,2,1,161,174,151,139,93,148,2,1,129,174,151,139,93,171,2,1,161,174,151,139,93,168,2,1,161,174,151,139,93,172,2,1,161,174,151,139,93,173,2,1,129,174,151,139,93,174,2,1,1,184,231,170,67,0,161,145,144,146,185,5,11,6,39,245,220,194,52,0,161,209,250,203,254,15,0,1,161,209,250,203,254,15,1,1,136,209,250,203,254,15,2,1,118,2,2,105,100,119,36,50,54,100,53,99,56,99,49,45,49,99,54,54,45,52,53,57,99,45,98,99,54,99,45,102,52,100,97,49,97,54,54,51,51,52,56,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,83,246,151,161,140,152,206,145,6,152,1,1,161,140,152,206,145,6,153,1,1,129,245,220,194,52,2,1,161,245,220,194,52,0,1,161,245,220,194,52,1,1,136,245,220,194,52,5,1,118,2,2,105,100,119,36,50,54,100,53,99,56,99,49,45,49,99,54,54,45,52,53,57,99,45,98,99,54,99,45,102,52,100,97,49,97,54,54,51,51,52,56,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,83,246,202,161,140,152,206,145,6,87,1,161,140,152,206,145,6,88,1,136,245,220,194,52,8,1,118,2,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,83,246,206,2,105,100,119,36,55,100,50,49,52,56,102,99,45,99,97,99,101,45,52,52,53,50,45,57,99,53,99,45,57,54,101,53,50,101,54,98,102,56,98,53,161,245,220,194,52,3,1,161,245,220,194,52,4,1,129,245,220,194,52,11,1,161,170,255,211,105,7,1,161,170,255,211,105,8,1,161,140,152,206,145,6,151,1,1,161,245,220,194,52,15,1,161,245,220,194,52,16,1,161,245,220,194,52,17,1,161,170,255,211,105,21,1,161,170,255,211,105,22,1,161,245,220,194,52,20,1,161,245,220,194,52,21,1,161,245,220,194,52,22,1,161,245,220,194,52,23,1,161,170,255,211,105,35,1,161,170,255,211,105,36,1,129,170,255,211,105,37,1,161,245,220,194,52,27,1,161,245,220,194,52,28,1,129,245,220,194,52,29,1,161,245,220,194,52,30,1,161,245,220,194,52,31,1,161,245,220,194,52,26,1,161,245,220,194,52,33,1,161,245,220,194,52,34,1,161,245,220,194,52,35,1,6,166,203,155,46,0,161,140,152,206,145,6,30,1,161,140,152,206,145,6,31,1,129,162,238,198,212,7,2,1,168,166,203,155,46,0,1,122,4,56,115,160,190,64,16,0,168,166,203,155,46,1,1,122,0,0,0,0,102,88,52,80,136,166,203,155,46,2,1,118,2,2,105,100,119,36,48,99,101,49,51,52,49,53,45,54,99,99,101,45,52,52,57,55,45,57,52,99,54,45,52,55,53,97,100,57,54,99,50,52,57,101,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,88,52,80,3,179,252,154,44,0,161,157,240,144,231,2,0,1,161,157,240,144,231,2,1,1,129,157,240,144,231,2,2,1,3,248,153,216,10,0,161,163,236,177,169,4,9,1,161,163,236,177,169,4,10,1,129,163,236,177,169,4,11,1,158,1,128,252,161,128,4,1,0,33,130,180,254,251,6,4,0,3,5,2,9,1,13,4,135,240,136,178,10,1,0,45,135,232,133,203,9,1,0,15,137,226,192,199,6,1,0,20,138,202,240,189,2,1,0,134,1,139,240,196,145,14,1,0,138,2,140,228,230,243,1,1,0,2,139,152,215,249,10,1,0,34,141,216,158,150,1,1,0,3,142,130,192,134,11,1,0,3,143,184,153,180,6,1,0,6,145,190,137,224,10,1,0,2,145,144,146,185,5,1,0,12,140,152,206,145,6,13,0,60,61,24,86,3,90,6,97,3,101,2,105,1,111,1,113,20,134,1,20,155,1,2,158,1,3,162,1,2,135,166,246,235,6,1,0,2,147,206,229,235,1,1,0,3,137,164,190,210,1,7,3,2,6,1,20,2,25,3,29,2,32,2,35,3,152,252,186,192,1,1,0,2,153,130,203,161,6,1,0,97,154,244,246,165,8,10,0,24,26,1,37,2,56,1,71,1,82,3,88,36,127,18,148,1,35,193,1,2,156,148,170,169,10,1,0,16,157,240,144,231,2,1,0,3,158,156,181,152,10,7,0,6,7,2,15,1,19,8,28,2,32,1,40,9,159,156,204,250,6,1,0,27,160,192,253,131,5,1,0,3,161,178,132,150,11,4,0,15,17,1,28,64,93,8,158,182,250,251,9,1,0,15,163,236,177,169,4,1,0,12,164,188,201,172,1,1,0,3,158,184,218,165,3,1,0,38,162,238,198,212,7,1,0,2,170,140,240,234,9,1,0,31,171,204,155,217,8,1,0,3,171,142,166,254,1,1,0,6,173,252,148,184,13,3,0,40,41,2,44,20,176,154,159,227,1,3,0,1,2,2,14,6,178,162,190,217,10,1,0,3,179,252,154,44,1,0,3,180,230,210,212,13,2,0,2,3,4,182,172,247,194,5,1,0,3,183,226,184,158,8,1,0,8,187,220,199,239,8,9,1,1,3,2,6,1,15,1,19,1,23,17,43,1,56,4,63,3,195,242,227,194,8,1,0,3,196,154,250,183,6,1,0,62,197,254,154,201,10,1,0,3,200,142,208,241,4,1,0,157,1,202,160,246,212,1,1,0,6,203,184,221,173,11,6,14,1,19,1,21,2,29,1,33,2,36,1,206,220,129,131,4,3,0,3,4,2,16,5,207,228,238,162,8,4,1,2,13,9,23,2,35,15,209,250,203,254,15,1,0,2,210,228,153,221,12,1,0,3,211,166,203,229,4,1,0,195,1,211,202,217,232,12,1,0,7,214,168,149,214,3,1,0,15,219,220,239,171,8,1,0,10,225,248,138,176,2,6,0,9,10,2,13,1,18,2,26,1,30,19,226,212,179,248,2,1,0,15,229,154,128,197,12,4,0,6,7,2,15,1,19,5,234,182,182,157,9,1,0,8,235,178,165,206,5,1,0,72,234,156,130,211,12,1,0,11,241,130,161,205,7,1,0,9,244,226,228,149,2,4,1,3,6,1,17,4,23,7,245,220,194,52,4,0,2,3,5,9,2,12,27,247,200,243,247,14,5,1,2,5,1,13,8,22,32,55,42,248,136,168,181,1,1,0,3,248,210,237,129,13,1,0,2,250,198,166,187,7,1,0,2,248,196,187,185,10,1,0,31,252,218,241,167,14,1,0,2,255,140,248,220,6,1,0,3,128,211,179,216,2,1,0,10,133,159,138,205,12,14,0,7,9,1,20,9,42,14,57,2,61,1,65,1,67,1,69,17,87,6,94,2,106,2,109,2,121,10,135,167,156,250,14,1,0,16,135,193,208,135,7,1,0,16,141,245,194,142,11,1,0,7,141,205,220,149,4,1,0,3,143,131,148,152,6,1,1,2,141,171,170,217,4,1,0,2,145,159,164,217,14,1,0,3,149,129,169,191,12,1,0,16,149,249,242,175,4,3,0,8,11,2,16,15,149,189,189,215,8,1,0,3,149,161,132,184,14,1,0,221,2,154,243,157,196,14,1,0,12,154,193,208,134,10,1,0,9,155,165,205,152,11,1,0,3,155,159,180,195,15,1,0,6,157,207,243,216,6,1,0,2,154,235,215,240,4,1,0,13,161,239,241,154,13,1,0,106,162,159,252,196,11,1,0,3,164,155,139,169,7,1,0,35,165,139,157,171,15,11,2,1,9,1,13,5,19,2,27,1,31,6,39,1,52,5,58,12,72,1,85,18,164,203,250,235,13,1,0,7,167,131,133,162,9,1,0,11,166,201,221,141,13,11,0,6,7,2,10,2,13,2,26,2,31,2,34,3,38,6,45,2,57,11,69,3,169,197,188,221,3,1,0,3,170,255,211,105,3,0,2,3,25,31,7,166,203,155,46,1,0,3,173,187,245,170,14,1,0,46,174,151,139,93,14,0,45,55,2,60,54,124,2,129,1,48,178,1,2,202,1,2,217,1,2,232,1,2,237,1,41,160,2,2,165,2,4,170,2,1,172,2,7,175,225,172,150,8,1,0,10,175,147,217,214,1,1,0,33,177,161,136,243,11,1,0,10,178,203,205,182,4,1,0,1,175,205,156,228,6,1,0,10,180,205,189,133,13,1,0,20,181,175,219,209,12,1,0,10,182,143,233,195,4,1,0,111,177,219,160,167,7,1,0,4,184,201,188,172,10,1,0,3,184,231,170,67,1,0,6,186,197,166,179,15,1,0,7,188,171,136,250,8,1,0,21,188,237,223,145,6,1,0,26,190,139,191,155,1,1,0,2,191,157,147,233,9,1,0,32,193,249,142,142,4,1,0,17,198,189,216,175,6,1,0,23,200,205,214,172,10,1,0,30,201,129,238,197,4,8,0,6,9,33,43,2,51,1,55,2,58,2,66,1,70,13,201,191,159,147,14,1,0,17,200,203,236,184,2,1,0,35,200,159,185,206,9,1,0,8,205,149,231,236,11,1,0,68,213,161,242,209,13,5,0,3,4,2,8,1,16,7,27,8,214,139,213,136,8,13,0,2,5,2,10,2,15,2,20,2,25,2,30,2,35,2,40,2,45,2,50,2,55,2,60,6,213,255,156,145,1,1,0,2,219,227,140,137,6,1,0,34,221,147,167,147,15,1,0,177,2,222,205,223,235,7,2,0,9,10,21,223,209,193,147,11,1,0,81,227,209,197,253,2,1,0,14,229,153,197,202,7,1,0,241,6,231,139,244,188,8,1,0,13,232,207,157,148,2,1,0,2,233,165,139,246,14,4,0,2,3,4,8,2,20,23,233,247,183,159,1,1,0,4,235,225,184,133,10,1,0,3,234,153,236,158,4,8,0,6,7,2,11,1,15,1,19,6,27,1,38,7,46,27,234,187,164,181,1,1,0,24,231,189,134,196,8,1,0,77,239,199,189,146,3,1,0,24,240,149,229,225,6,1,1,2,241,155,213,233,1,5,0,6,7,2,11,1,15,1,19,32,240,253,240,229,1,1,0,79,243,239,182,181,13,2,1,2,16,2,236,229,225,232,8,1,0,116,246,185,174,192,6,1,0,90,248,153,216,10,1,0,3,251,189,220,155,14,1,0,3,252,163,130,200,6,2,1,2,16,2,253,205,145,137,11,1,0,10,252,171,209,175,15,1,0,4,255,255,147,249,10,1,0,3],"version":0,"object_id":"9eebea03-3ed5-4298-86b2-a7f77856d48b"},"code":0,"message":"Operation completed successfully."} \ No newline at end of file diff --git a/frontend/appflowy_web_app/cypress/fixtures/sign_in_success.json b/frontend/appflowy_web_app/cypress/fixtures/sign_in_success.json deleted file mode 100644 index 0679311668..0000000000 --- a/frontend/appflowy_web_app/cypress/fixtures/sign_in_success.json +++ /dev/null @@ -1,66 +0,0 @@ -{ - "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MTI4Mjk2MjAsImlhdCI6MTcxMjgyNjAyMCwic3ViIjoiY2JmZjA2MGEtMTk2ZC00MTVhLWFhODAtNzU5YzAxODg2NDY2IiwiZW1haWwiOiJsdUBhcHBmbG93eS5pbyIsInBob25lIjoiIiwiYXBwX21ldGFkYXRhIjp7InByb3ZpZGVyIjoiZ29vZ2xlIiwicHJvdmlkZXJzIjpbImdvb2dsZSJdfSwidXNlcl9tZXRhZGF0YSI6eyJhdmF0YXJfdXJsIjoiaHR0cHM6Ly9saDMuZ29vZ2xldXNlcmNvbnRlbnQuY29tL2EvQUNnOG9jTEhabVZBczRTb0ZlVFFuWG5CU2JiNTBBVXF0YktHNWx5MGllVHZCSklYZ1o3UmdRPXM5Ni1jIiwiY3VzdG9tX2NsYWltcyI6eyJoZCI6ImFwcGZsb3d5LmlvIn0sImVtYWlsIjoibHVAYXBwZmxvd3kuaW8iLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiZnVsbF9uYW1lIjoiTHUgSGUiLCJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJuYW1lIjoiTHUgSGUiLCJwaG9uZV92ZXJpZmllZCI6ZmFsc2UsInBpY3R1cmUiOiJodHRwczovL2xoMy5nb29nbGV1c2VyY29udGVudC5jb20vYS9BQ2c4b2NMSFptVkFzNFNvRmVUUW5YbkJTYmI1MEFVcXRiS0c1bHkwaWVUdkJKSVhnWjdSZ1E9czk2LWMiLCJwcm92aWRlcl9pZCI6IjEwMTE2OTI1MDgyOTU1NDAyODM4MSIsInN1YiI6IjEwMTE2OTI1MDgyOTU1NDAyODM4MSJ9LCJyb2xlIjoiIiwiYWFsIjoiYWFsMSIsImFtciI6W3sibWV0aG9kIjoicGFzc3dvcmQiLCJ0aW1lc3RhbXAiOjE3MTI4MjYwMjB9XSwic2Vzc2lvbl9pZCI6ImJmMzE5OTRlLTQwMTgtNDhjMS05Yzc0LWVmYzkyMGNjOWQ0NSJ9.QeTrRhsnBjBL1GUS3TIWOgU1SPM6RcaWwxZdMVfcFBU", - "token_type": "bearer", - "expires_in": 3600, - "expires_at": 4869016461, - "refresh_token": "71vp1jJnSAVluZKaXkhG1A", - "user": { - "id": "cbff060a-196d-415a-aa80-759c01886466", - "aud": "", - "role": "", - "email": "lu@appflowy.io", - "email_confirmed_at": "2024-03-13T10:49:53.165361Z", - "phone": "", - "confirmed_at": "2024-03-13T10:49:53.165361Z", - "last_sign_in_at": "2024-04-11T09:00:20.547468985Z", - "app_metadata": { - "provider": "google", - "providers": [ - "google" - ] - }, - "user_metadata": { - "avatar_url": "https://lh3.googleusercontent.com/a/ACg8ocLHZmVAs4SoFeTQnXnBSbb50AUqtbKG5ly0ieTvBJIXgZ7RgQ=s96-c", - "custom_claims": { - "hd": "appflowy.io" - }, - "email": "lu@appflowy.io", - "email_verified": true, - "full_name": "Lu He", - "iss": "https://accounts.google.com", - "name": "Lu He", - "phone_verified": false, - "picture": "https://lh3.googleusercontent.com/a/ACg8ocLHZmVAs4SoFeTQnXnBSbb50AUqtbKG5ly0ieTvBJIXgZ7RgQ=s96-c", - "provider_id": "101169250829554028381", - "sub": "101169250829554028381" - }, - "identities": [ - { - "identity_id": "e4cf8b69-7f80-42e9-aed2-e25132ad0178", - "id": "101169250829554028381", - "user_id": "cbff060a-196d-415a-aa80-759c01886466", - "identity_data": { - "avatar_url": "https://lh3.googleusercontent.com/a/ACg8ocLHZmVAs4SoFeTQnXnBSbb50AUqtbKG5ly0ieTvBJIXgZ7RgQ=s96-c", - "custom_claims": { - "hd": "appflowy.io" - }, - "email": "lu@appflowy.io", - "email_verified": true, - "full_name": "Lu He", - "iss": "https://accounts.google.com", - "name": "Lu He", - "phone_verified": false, - "picture": "https://lh3.googleusercontent.com/a/ACg8ocLHZmVAs4SoFeTQnXnBSbb50AUqtbKG5ly0ieTvBJIXgZ7RgQ=s96-c", - "provider_id": "101169250829554028381", - "sub": "101169250829554028381" - }, - "provider": "google", - "last_sign_in_at": "2024-03-13T07:22:43.110504Z", - "created_at": "2024-03-13T07:22:43.110543Z", - "updated_at": "2024-04-04T06:15:14.03093Z" - } - ], - "created_at": "2024-03-13T07:22:43.102586Z", - "updated_at": "2024-04-11T09:00:20.551485Z" - } -} \ No newline at end of file diff --git a/frontend/appflowy_web_app/cypress/fixtures/user.json b/frontend/appflowy_web_app/cypress/fixtures/user.json deleted file mode 100644 index 5b429dcd59..0000000000 --- a/frontend/appflowy_web_app/cypress/fixtures/user.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "data": { - "uid": 304120109071339520, - "uuid": "cbff060a-196d-415a-aa80-759c01886466", - "email": "lu@appflowy.io", - "password": "", - "name": "Kilu", - "metadata": { - "icon_url": "🇽🇰" - }, - "encryption_sign": null, - "latest_workspace_id": "fcb503f9-9287-4de4-8de0-ea191e680968", - "updated_at": 1710421586 - }, - "code": 0, - "message": "Operation completed successfully." -} \ No newline at end of file diff --git a/frontend/appflowy_web_app/cypress/fixtures/user_workspace.json b/frontend/appflowy_web_app/cypress/fixtures/user_workspace.json deleted file mode 100644 index 277b4c972c..0000000000 --- a/frontend/appflowy_web_app/cypress/fixtures/user_workspace.json +++ /dev/null @@ -1 +0,0 @@ -{"data":{"user_profile":{"uid":304120109071339520,"uuid":"cbff060a-196d-415a-aa80-759c01886466","email":"lu@appflowy.io","password":"","name":"Kilu","metadata":{"icon_url":"🇽🇰"},"encryption_sign":null,"latest_workspace_id":"9eebea03-3ed5-4298-86b2-a7f77856d48b","updated_at":1715847453},"visiting_workspace":{"workspace_id":"9eebea03-3ed5-4298-86b2-a7f77856d48b","database_storage_id":"375874be-7a4f-4b7c-8b89-1dc9a39838f4","owner_uid":304120109071339520,"owner_name":"Kilu","workspace_type":0,"workspace_name":"Kilu Works","created_at":"2024-03-13T07:23:10.275174Z","icon":"😆"},"workspaces":[{"workspace_id":"81570fa8-8be9-4b2d-9f1c-1ef4f34079a8","database_storage_id":"6c1f1a2c-e8d5-4bc2-917f-495bce862abb","owner_uid":311828434584080384,"owner_name":"Zack Zi Xiang Fu","workspace_type":0,"workspace_name":"My Workspace","created_at":"2024-04-03T13:53:18.295918Z","icon":""},{"workspace_id":"fcb503f9-9287-4de4-8de0-ea191e680968","database_storage_id":"ae1b82a5-2b93-45c7-901a-f9357c544534","owner_uid":276169796100296704,"owner_name":"Annie Anqi Wang","workspace_type":0,"workspace_name":"AppFlowy Test","created_at":"2023-12-27T04:18:36.372013Z","icon":""},{"workspace_id":"9eebea03-3ed5-4298-86b2-a7f77856d48b","database_storage_id":"375874be-7a4f-4b7c-8b89-1dc9a39838f4","owner_uid":304120109071339520,"owner_name":"Kilu","workspace_type":0,"workspace_name":"Kilu Works","created_at":"2024-03-13T07:23:10.275174Z","icon":"😆"}]},"code":0,"message":"Operation completed successfully."} \ No newline at end of file diff --git a/frontend/appflowy_web_app/cypress/fixtures/verify_token.json b/frontend/appflowy_web_app/cypress/fixtures/verify_token.json deleted file mode 100644 index 503838f0a6..0000000000 --- a/frontend/appflowy_web_app/cypress/fixtures/verify_token.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "code": 0, - "data": { - "is_new": false - } -} \ No newline at end of file diff --git a/frontend/appflowy_web_app/cypress/support/commands.ts b/frontend/appflowy_web_app/cypress/support/commands.ts index f78768001f..1b5199b01a 100644 --- a/frontend/appflowy_web_app/cypress/support/commands.ts +++ b/frontend/appflowy_web_app/cypress/support/commands.ts @@ -25,96 +25,9 @@ // -- This will overwrite an existing command -- // Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) // -import { YDoc } from '@/application/collab.type'; -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore -import { JSDatabaseService } from '@/application/services/js-services/database.service'; -import { JSDocumentService } from '@/application/services/js-services/document.service'; -import { applyYDoc } from '@/application/ydoc/apply'; -import * as Y from 'yjs'; Cypress.Commands.add('mockAPI', () => { - cy.fixture('sign_in_success').then((json) => { - cy.intercept('GET', `/api/user/verify/${json.access_token}`, { - fixture: 'verify_token', - }).as('verifyToken'); - cy.intercept('POST', '/gotrue/token?grant_type=password', json).as('loginSuccess'); - cy.intercept('POST', '/gotrue/token?grant_type=refresh_token', json).as('refreshToken'); - }); - cy.intercept('GET', '/api/user/profile', { fixture: 'user' }).as('getUserProfile'); - cy.intercept('GET', '/api/user/workspace', { fixture: 'user_workspace' }).as('getUserWorkspace'); + // Mock the API }); -// Example use: -// beforeEach(() => { -// cy.mockAPI(); -// }); - -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore -Cypress.Commands.add('mockCurrentWorkspace', () => { - cy.fixture('current_workspace').then((workspace) => { - cy.stub(JSDatabaseService.prototype, 'currentWorkspace').resolves(workspace); - }); -}); - -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore -Cypress.Commands.add('mockGetWorkspaceDatabases', () => { - cy.fixture('database/databases').then((databases) => { - cy.stub(JSDatabaseService.prototype, 'getWorkspaceDatabases').resolves(databases); - }); -}); - -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore -Cypress.Commands.add('mockDatabase', () => { - cy.mockCurrentWorkspace(); - cy.mockGetWorkspaceDatabases(); - - const ids = [ - '4c658817-20db-4f56-b7f9-0637a22dfeb6', - 'ce267d12-3b61-4ebb-bb03-d65272f5f817', - 'ad7dc45b-44b5-498f-bfa2-0f43bf05cc0d', - ]; - - const mockOpenDatabase = cy.stub(JSDatabaseService.prototype, 'openDatabase'); - - ids.forEach((id) => { - cy.fixture(`database/${id}`).then((database) => { - cy.fixture(`database/rows/${id}`).then((rows) => { - const doc = new Y.Doc(); - const rootRowsDoc = new Y.Doc(); - const rowsFolder: Y.Map = rootRowsDoc.getMap(); - const databaseState = new Uint8Array(database.data.doc_state); - - applyYDoc(doc, databaseState); - - Object.keys(rows).forEach((key) => { - const data = rows[key]; - const rowDoc = new Y.Doc(); - - applyYDoc(rowDoc, new Uint8Array(data)); - rowsFolder.set(key, rowDoc); - }); - mockOpenDatabase.withArgs(id).resolves({ - databaseDoc: doc, - rows: rowsFolder, - }); - }); - }); - }); -}); - -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore -Cypress.Commands.add('mockDocument', (id: string) => { - cy.fixture(`document/${id}`).then((subDocument) => { - const doc = new Y.Doc(); - const state = new Uint8Array(subDocument.data.doc_state); - - applyYDoc(doc, state); - - cy.stub(JSDocumentService.prototype, 'openDocument').withArgs(id).resolves(doc); - }); -}); +export {}; diff --git a/frontend/appflowy_web_app/cypress/support/component-index.html b/frontend/appflowy_web_app/cypress/support/component-index.html index b8b58ae50c..1633d91f21 100644 --- a/frontend/appflowy_web_app/cypress/support/component-index.html +++ b/frontend/appflowy_web_app/cypress/support/component-index.html @@ -7,6 +7,9 @@ Components App + + +
\ No newline at end of file diff --git a/frontend/appflowy_web_app/deploy/Dockerfile b/frontend/appflowy_web_app/deploy/Dockerfile new file mode 100644 index 0000000000..85f9d33b28 --- /dev/null +++ b/frontend/appflowy_web_app/deploy/Dockerfile @@ -0,0 +1,32 @@ +FROM oven/bun:latest + +WORKDIR /app + +RUN apt-get update && \ + apt-get install -y nginx supervisor + +RUN bun install cheerio pino pino-pretty + +COPY . . + +COPY supervisord.conf /app/supervisord.conf + +RUN addgroup --system nginx && \ + adduser --system --no-create-home --disabled-login --ingroup nginx nginx + +RUN apt-get clean && rm -rf /var/lib/apt/lists/* + + +COPY dist /usr/share/nginx/html/ + +COPY nginx.conf /etc/nginx/nginx.conf + +COPY start.sh /app/start.sh + +RUN chmod +x /app/start.sh +RUN chmod +x /app/supervisord.conf + + +EXPOSE 80 + +CMD ["supervisord", "-c", "/app/supervisord.conf"] \ No newline at end of file diff --git a/frontend/appflowy_web_app/deploy/deploy.sh b/frontend/appflowy_web_app/deploy/deploy.sh new file mode 100644 index 0000000000..772862c246 --- /dev/null +++ b/frontend/appflowy_web_app/deploy/deploy.sh @@ -0,0 +1,28 @@ +if [ -z "$1" ]; then + echo "No port number provided" + exit 1 +fi + +PORT=$1 + +echo "Starting deployment on port $PORT" + +rm -rf deploy + +tar -xzf build-output.tar.gz + +rm -rf build-output.tar.gz + +mv dist deploy/dist + +mv .env deploy/.env + +cd deploy + +docker system prune -f + +docker build -t appflowy-web-app-"$PORT" . + +docker rm -f appflowy-web-app-"$PORT" || true + +docker run -d --env-file .env -p "$PORT":80 --restart always --name appflowy-web-app-"$PORT" appflowy-web-app-"$PORT" \ No newline at end of file diff --git a/frontend/appflowy_web_app/nginx.conf b/frontend/appflowy_web_app/deploy/nginx.conf similarity index 76% rename from frontend/appflowy_web_app/nginx.conf rename to frontend/appflowy_web_app/deploy/nginx.conf index 729255a778..e0a658c310 100644 --- a/frontend/appflowy_web_app/nginx.conf +++ b/frontend/appflowy_web_app/deploy/nginx.conf @@ -40,29 +40,6 @@ http { server { listen 80; server_name localhost; - #server_name appflowy.com *.appflowy.com; - - location / { - return 301 https://$host$request_uri; - } - - } - - # Additional server block for HTTPS - server { - listen 443 ssl http2; - listen [::]:443 ssl http2; - server_name localhost; - #server_name appflowy.com *.appflowy.com; - - ssl_certificate /etc/ssl/certs/nginx-signed.crt; - ssl_certificate_key /etc/ssl/private/nginx-signed.key; - - ssl_session_cache shared:SSL:1m; - ssl_session_timeout 5m; - - ssl_ciphers HIGH:!aNULL:!MD5; - ssl_prefer_server_ciphers on; location / { proxy_pass http://localhost:3000; @@ -89,6 +66,18 @@ http { access_log off; } + location /og-image.png { + root /usr/share/nginx/html; + expires 30d; + access_log off; + } + + location /covers/ { + root /usr/share/nginx/html; + expires 30d; + access_log off; + } + error_page 404 /404.html; location = /404.html { root /usr/share/nginx/html; @@ -98,5 +87,6 @@ http { location = /50x.html { root /usr/share/nginx/html; } + } -} +} \ No newline at end of file diff --git a/frontend/appflowy_web_app/deploy/server.ts b/frontend/appflowy_web_app/deploy/server.ts new file mode 100644 index 0000000000..414cdb3e4d --- /dev/null +++ b/frontend/appflowy_web_app/deploy/server.ts @@ -0,0 +1,193 @@ +import path from 'path'; +import * as fs from 'fs'; +import pino from 'pino'; +import { type CheerioAPI, load } from 'cheerio'; +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-expect-error +import { fetch } from 'bun'; + +const distDir = path.join(__dirname, 'dist'); +const indexPath = path.join(distDir, 'index.html'); +const baseURL = process.env.AF_BASE_URL as string; +const defaultSite = 'https://appflowy.io'; + +const setOrUpdateMetaTag = ($: CheerioAPI, selector: string, attribute: string, content: string) => { + if ($(selector).length === 0) { + $('head').append(``); + } else { + $(selector).attr('content', content); + } +}; + +const logger = pino({ + transport: { + target: 'pino-pretty', + options: { + colorize: true, + translateTime: 'SYS:standard', + destination: `${__dirname}/pino-logger.log`, + }, + }, + level: 'info', +}); + +const logRequestTimer = (req: Request) => { + const start = Date.now(); + const pathname = new URL(req.url).pathname; + + logger.info(`Incoming request: ${pathname}`); + return () => { + const duration = Date.now() - start; + + logger.info(`Request for ${pathname} took ${duration}ms`); + }; +}; + +const fetchMetaData = async (url: string) => { + logger.info(`Fetching meta data from ${url}`); + try { + const response = await fetch(url, { + verbose: true, + }); + + if (!response.ok) { + throw new Error(`HTTP error! Status: ${response.status}`); + } + + return response.json(); + } catch (error) { + logger.error(`Error fetching meta data ${error}`); + return null; + } +}; + +const createServer = async (req: Request) => { + const timer = logRequestTimer(req); + const reqUrl = new URL(req.url); + const hostname = req.headers.get('host'); + + logger.info(`Request URL: ${hostname}${reqUrl.pathname}`); + + const [namespace, publishName] = reqUrl.pathname.slice(1).split('/'); + + logger.info(`Namespace: ${namespace}, Publish Name: ${publishName}`); + + if (req.method === 'GET') { + if (namespace === '' || !publishName) { + timer(); + return new Response(null, { + status: 302, + headers: { + Location: defaultSite, + }, + }); + } + + let metaData; + + try { + metaData = await fetchMetaData(`${baseURL}/api/workspace/published/${namespace}/${publishName}`); + } catch (error) { + logger.error(`Error fetching meta data: ${error}`); + } + + const htmlData = fs.readFileSync(indexPath, 'utf8'); + const $ = load(htmlData); + + const description = 'Write, share, and publish docs quickly on AppFlowy.\nGet started for free.'; + let title = 'AppFlowy'; + const url = `https://${hostname}${reqUrl.pathname}`; + let image = '/og-image.png'; + let favicon = '/appflowy.svg'; + + try { + if (metaData && metaData.view) { + const view = metaData.view; + const emoji = view.icon.value; + const titleList = []; + + if (emoji) { + const emojiCode = emoji.codePointAt(0).toString(16); // Convert emoji to hex code + const baseUrl = 'https://raw.githubusercontent.com/googlefonts/noto-emoji/main/svg/emoji_u'; + + favicon = `${baseUrl}${emojiCode}.svg`; + } + + if (view.name) { + titleList.push(view.name); + titleList.push('|'); + } + + titleList.push('AppFlowy'); + title = titleList.join(' '); + + try { + const cover = view.extra ? JSON.parse(view.extra)?.cover : null; + + if (cover) { + if (['unsplash', 'custom'].includes(cover.type)) { + image = cover.value; + } else if (cover.type === 'built_in') { + image = `/covers/m_cover_image_${cover.value}.png`; + } + } + } catch (_) { + // Do nothing + } + } + } catch (error) { + logger.error(`Error injecting meta data: ${error}`); + } + + $('title').text(title); + $('link[rel="icon"]').attr('href', favicon); + setOrUpdateMetaTag($, 'meta[name="description"]', 'name', description); + setOrUpdateMetaTag($, 'meta[property="og:title"]', 'property', title); + setOrUpdateMetaTag($, 'meta[property="og:description"]', 'property', description); + setOrUpdateMetaTag($, 'meta[property="og:image"]', 'property', image); + setOrUpdateMetaTag($, 'meta[property="og:url"]', 'property', url); + setOrUpdateMetaTag($, 'meta[property="og:site_name"]', 'property', 'AppFlowy'); + setOrUpdateMetaTag($, 'meta[property="og:type"]', 'property', 'website'); + setOrUpdateMetaTag($, 'meta[name="twitter:card"]', 'name', 'summary_large_image'); + setOrUpdateMetaTag($, 'meta[name="twitter:title"]', 'name', title); + setOrUpdateMetaTag($, 'meta[name="twitter:description"]', 'name', description); + setOrUpdateMetaTag($, 'meta[name="twitter:image"]', 'name', image); + setOrUpdateMetaTag($, 'meta[name="twitter:site"]', 'name', '@appflowy'); + + timer(); + return new Response($.html(), { + headers: { 'Content-Type': 'text/html' }, + }); + } else { + timer(); + logger.error({ message: 'Method not allowed', method: req.method }); + return new Response('Method not allowed', { status: 405 }); + } +}; + +declare const Bun: { + serve: (options: { port: number; fetch: typeof createServer; error: (err: Error) => Response }) => void; +}; + +const start = () => { + try { + // eslint-disable-next-line @typescript-eslint/no-var-requires + Bun.serve({ + port: 3000, + fetch: createServer, + error: (err) => { + logger.error(`Internal Server Error: ${err}`); + return new Response('Internal Server Error', { status: 500 }); + }, + }); + logger.info('Server is running on port 3000'); + logger.info(`Base URL: ${baseURL}`); + } catch (err) { + logger.error(err); + process.exit(1); + } +}; + +start(); + +export {}; diff --git a/frontend/appflowy_web_app/start.sh b/frontend/appflowy_web_app/deploy/start.sh similarity index 70% rename from frontend/appflowy_web_app/start.sh rename to frontend/appflowy_web_app/deploy/start.sh index b4691baa1a..eba0f53018 100644 --- a/frontend/appflowy_web_app/start.sh +++ b/frontend/appflowy_web_app/deploy/start.sh @@ -1,10 +1,12 @@ -#!/bin/bash +#!/usr/bin/env bash + -# Start the frontend server -bun run server.cjs & # Start the nginx server service nginx start +# Start the frontend server +bun run server.ts + tail -f /dev/null diff --git a/frontend/appflowy_web_app/deploy/supervisord.conf b/frontend/appflowy_web_app/deploy/supervisord.conf new file mode 100644 index 0000000000..1484fd39e5 --- /dev/null +++ b/frontend/appflowy_web_app/deploy/supervisord.conf @@ -0,0 +1,9 @@ +[supervisord] +nodaemon=true + +[program:bun] +command=sh /app/start.sh +autostart=true +autorestart=true +stderr_logfile=/var/log/bun.err.log +stdout_logfile=/var/log/bun.out.log diff --git a/frontend/appflowy_web_app/index.html b/frontend/appflowy_web_app/index.html index a71f082fc3..49615f8cb0 100644 --- a/frontend/appflowy_web_app/index.html +++ b/frontend/appflowy_web_app/index.html @@ -13,7 +13,7 @@ content="AppFlowy is an AI collaborative workspace where you achieve more without losing control of your data" /> @@ -22,15 +22,17 @@ content="AppFlowy is an AI collaborative workspace where you achieve more without losing control of your data" /> +
+ + + + diff --git a/frontend/appflowy_web_app/package.json b/frontend/appflowy_web_app/package.json index f822a6b0f9..8a9be42420 100644 --- a/frontend/appflowy_web_app/package.json +++ b/frontend/appflowy_web_app/package.json @@ -24,7 +24,7 @@ "coverage": "pnpm run test:unit && pnpm run test:components" }, "dependencies": { - "@appflowyinc/client-api-wasm": "0.0.3", + "@appflowyinc/client-api-wasm": "0.1.1", "@atlaskit/primitives": "^5.5.3", "@emoji-mart/data": "^1.1.2", "@emoji-mart/react": "^1.1.1", @@ -43,6 +43,8 @@ "colorthief": "^2.4.0", "dayjs": "^1.11.9", "decimal.js": "^10.4.3", + "dexie": "^4.0.7", + "dexie-react-hooks": "^1.1.7", "emoji-mart": "^5.5.2", "emoji-regex": "^10.2.1", "events": "^3.3.0", @@ -56,6 +58,7 @@ "katex": "^0.16.7", "lodash-es": "^4.17.21", "nanoid": "^4.0.0", + "notistack": "^3.0.1", "numeral": "^2.0.6", "prismjs": "^1.29.0", "protoc-gen-ts": "0.8.7", @@ -136,6 +139,7 @@ "autoprefixer": "^10.4.13", "babel-jest": "^29.6.2", "chalk": "^4.1.2", + "cheerio": "1.0.0-rc.12", "cross-env": "^7.0.3", "cypress": "^13.7.2", "eslint": "^8.57.0", @@ -145,6 +149,8 @@ "istanbul-lib-coverage": "^3.2.2", "jest-environment-jsdom": "^29.6.2", "nyc": "^15.1.0", + "pino": "^9.2.0", + "pino-pretty": "^11.2.1", "postcss": "^8.4.21", "prettier": "2.8.4", "prettier-plugin-tailwindcss": "^0.2.2", diff --git a/frontend/appflowy_web_app/pnpm-lock.yaml b/frontend/appflowy_web_app/pnpm-lock.yaml index 194beaa5dd..c7c9daa795 100644 --- a/frontend/appflowy_web_app/pnpm-lock.yaml +++ b/frontend/appflowy_web_app/pnpm-lock.yaml @@ -1,9 +1,13 @@ lockfileVersion: '6.0' +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + dependencies: '@appflowyinc/client-api-wasm': - specifier: 0.0.3 - version: 0.0.3 + specifier: 0.1.1 + version: 0.1.1 '@atlaskit/primitives': specifier: ^5.5.3 version: 5.7.0(@types/react@18.2.66)(react@18.2.0) @@ -58,6 +62,12 @@ dependencies: decimal.js: specifier: ^10.4.3 version: 10.4.3 + dexie: + specifier: ^4.0.7 + version: 4.0.7 + dexie-react-hooks: + specifier: ^1.1.7 + version: 1.1.7(@types/react@18.2.66)(dexie@4.0.7)(react@18.2.0) emoji-mart: specifier: ^5.5.2 version: 5.6.0 @@ -97,6 +107,9 @@ dependencies: nanoid: specifier: ^4.0.0 version: 4.0.2 + notistack: + specifier: ^3.0.1 + version: 3.0.1(csstype@3.1.3)(react-dom@18.2.0)(react@18.2.0) numeral: specifier: ^2.0.6 version: 2.0.6 @@ -333,6 +346,9 @@ devDependencies: chalk: specifier: ^4.1.2 version: 4.1.2 + cheerio: + specifier: 1.0.0-rc.12 + version: 1.0.0-rc.12 cross-env: specifier: ^7.0.3 version: 7.0.3 @@ -360,6 +376,12 @@ devDependencies: nyc: specifier: ^15.1.0 version: 15.1.0 + pino: + specifier: ^9.2.0 + version: 9.2.0 + pino-pretty: + specifier: ^11.2.1 + version: 11.2.1 postcss: specifier: ^8.4.21 version: 8.4.21 @@ -429,8 +451,8 @@ packages: '@jridgewell/gen-mapping': 0.3.5 '@jridgewell/trace-mapping': 0.3.25 - /@appflowyinc/client-api-wasm@0.0.3: - resolution: {integrity: sha512-ARjLhiDZ8MiZ9egWDbAX9VAdXXS30av+InCPLrS/iqCMYrhuuU9rxS9jQeNEB7jucFrj158gBRusimFN7P/lyw==} + /@appflowyinc/client-api-wasm@0.1.1: + resolution: {integrity: sha512-7+/TCmzMi9KrxX3HFLJv9R6ON2AO5xQavV547ii7RZM8+5bZJakuf6+pnyCzOquQX07q3ZYwJCa3MIgDvficaA==} dev: false /@atlaskit/analytics-next-stable-react-context@1.0.1(react@18.2.0): @@ -4580,6 +4602,13 @@ packages: deprecated: Use your platform's native atob() and btoa() methods instead dev: true + /abort-controller@3.0.0: + resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} + engines: {node: '>=6.5'} + dependencies: + event-target-shim: 5.0.1 + dev: true + /acorn-globals@7.0.1: resolution: {integrity: sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==} dependencies: @@ -4886,6 +4915,11 @@ packages: engines: {node: '>= 4.0.0'} dev: true + /atomic-sleep@1.0.0: + resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} + engines: {node: '>=8.0.0'} + dev: true + /autoprefixer@10.4.13(postcss@8.4.21): resolution: {integrity: sha512-49vKpMqcZYsJjwotvt4+h/BCjJVnhGwcLpDt5xkcaOG3eLrG/HUYLagrihYsQ+qrIBgIzX1Rw7a6L8I/ZA1Atg==} engines: {node: ^10 || ^12 || >=14} @@ -5180,6 +5214,13 @@ packages: ieee754: 1.2.1 dev: true + /buffer@6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + dev: true + /cachedir@2.4.0: resolution: {integrity: sha512-9EtFOZR8g22CL7BWjJ9BUx1+A/djkofnyW3aOXZORNW2kxoUpx2h+uN2cOqwPmFhnpVmxg+KW2OjOSgChTEvsQ==} engines: {node: '>=6'} @@ -5289,6 +5330,30 @@ packages: engines: {node: '>= 0.8.0'} dev: true + /cheerio-select@2.1.0: + resolution: {integrity: sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==} + dependencies: + boolbase: 1.0.0 + css-select: 5.1.0 + css-what: 6.1.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.1.0 + dev: true + + /cheerio@1.0.0-rc.12: + resolution: {integrity: sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==} + engines: {node: '>= 6'} + dependencies: + cheerio-select: 2.1.0 + dom-serializer: 2.0.0 + domhandler: 5.0.3 + domutils: 3.1.0 + htmlparser2: 8.0.2 + parse5: 7.1.2 + parse5-htmlparser2-tree-adapter: 7.0.0 + dev: true + /chokidar@3.6.0: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} @@ -5739,6 +5804,10 @@ packages: dependencies: '@babel/runtime': 7.24.1 + /dateformat@4.6.3: + resolution: {integrity: sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==} + dev: true + /dayjs@1.11.10: resolution: {integrity: sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==} dev: true @@ -5860,6 +5929,22 @@ packages: minimist: 1.2.8 dev: true + /dexie-react-hooks@1.1.7(@types/react@18.2.66)(dexie@4.0.7)(react@18.2.0): + resolution: {integrity: sha512-Lwv5W0Hk+uOW3kGnsU9GZoR1er1B7WQ5DSdonoNG+focTNeJbHW6vi6nBoX534VKI3/uwHebYzSw1fwY6a7mTw==} + peerDependencies: + '@types/react': '>=16' + dexie: ^3.2 || ^4.0.1-alpha + react: '>=16' + dependencies: + '@types/react': 18.2.66 + dexie: 4.0.7 + react: 18.2.0 + dev: false + + /dexie@4.0.7: + resolution: {integrity: sha512-M+Lo6rk4pekIfrc2T0o2tvVJwL6EAAM/B78DNfb8aaxFVoI1f8/rz5KTxuAnApkwqTSuxx7T5t0RKH7qprapGg==} + dev: false + /didyoumean@1.2.2: resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} dev: true @@ -6372,6 +6457,11 @@ packages: engines: {node: '>=0.10.0'} dev: true + /event-target-shim@5.0.1: + resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} + engines: {node: '>=6'} + dev: true + /eventemitter2@6.4.7: resolution: {integrity: sha512-tYUSVOGeQPKt/eC1ABfhHy5Xd96N3oIijJvN3O9+TsC28T5V9yX9oEfEK5faP0EFSNVOG97qtAS68GBrQB2hDg==} dev: true @@ -6455,6 +6545,10 @@ packages: resolution: {integrity: sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==} engines: {'0': node >=0.6.0} + /fast-copy@3.0.2: + resolution: {integrity: sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==} + dev: true + /fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -6492,6 +6586,15 @@ packages: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} dev: true + /fast-redact@3.5.0: + resolution: {integrity: sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==} + engines: {node: '>=6'} + dev: true + + /fast-safe-stringify@2.1.1: + resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} + dev: true + /fastq@1.17.1: resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} dependencies: @@ -6961,6 +7064,10 @@ packages: tslib: 2.6.2 dev: true + /help-me@5.0.0: + resolution: {integrity: sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==} + dev: true + /hoist-non-react-statics@3.3.2: resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} dependencies: @@ -6983,6 +7090,15 @@ packages: void-elements: 3.1.0 dev: false + /htmlparser2@8.0.2: + resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==} + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.1.0 + entities: 4.5.0 + dev: true + /http-proxy-agent@5.0.0: resolution: {integrity: sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==} engines: {node: '>= 6'} @@ -7881,6 +7997,11 @@ packages: - supports-color - ts-node + /joycon@3.1.1: + resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} + engines: {node: '>=10'} + dev: true + /jpeg-js@0.4.4: resolution: {integrity: sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==} dev: false @@ -8440,6 +8561,21 @@ packages: engines: {node: '>=0.10.0'} dev: true + /notistack@3.0.1(csstype@3.1.3)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-ntVZXXgSQH5WYfyU+3HfcXuKaapzAJ8fBLQ/G618rn3yvSzEbnOB8ZSOwhX+dAORy/lw+GC2N061JA0+gYWTVA==} + engines: {node: '>=12.0.0', npm: '>=6.0.0'} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + dependencies: + clsx: 1.2.1 + goober: 2.1.14(csstype@3.1.3) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + transitivePeerDependencies: + - csstype + dev: false + /npm-run-path@4.0.1: resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} engines: {node: '>=8'} @@ -8576,6 +8712,11 @@ packages: resolution: {integrity: sha512-LMJTtvgc/nugXj0Vcrrs68Mn2D1r0zf630VNtqtpI1FEO7e+O9FP4gqs9AcnBaSEeoHIPm28u6qgPR0oyEpGSw==} dev: false + /on-exit-leak-free@2.1.2: + resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} + engines: {node: '>=14.0.0'} + dev: true + /once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} dependencies: @@ -8721,6 +8862,13 @@ packages: json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 + /parse5-htmlparser2-tree-adapter@7.0.0: + resolution: {integrity: sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g==} + dependencies: + domhandler: 5.0.3 + parse5: 7.1.2 + dev: true + /parse5@7.1.2: resolution: {integrity: sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==} dependencies: @@ -8810,6 +8958,54 @@ packages: engines: {node: '>=6'} dev: true + /pino-abstract-transport@1.2.0: + resolution: {integrity: sha512-Guhh8EZfPCfH+PMXAb6rKOjGQEoy0xlAIn+irODG5kgfYV+BQ0rGYYWTIel3P5mmyXqkYkPmdIkywsn6QKUR1Q==} + dependencies: + readable-stream: 4.5.2 + split2: 4.2.0 + dev: true + + /pino-pretty@11.2.1: + resolution: {integrity: sha512-O05NuD9tkRasFRWVaF/uHLOvoRDFD7tb5VMertr78rbsYFjYp48Vg3477EshVAF5eZaEw+OpDl/tu+B0R5o+7g==} + hasBin: true + dependencies: + colorette: 2.0.20 + dateformat: 4.6.3 + fast-copy: 3.0.2 + fast-safe-stringify: 2.1.1 + help-me: 5.0.0 + joycon: 3.1.1 + minimist: 1.2.8 + on-exit-leak-free: 2.1.2 + pino-abstract-transport: 1.2.0 + pump: 3.0.0 + readable-stream: 4.5.2 + secure-json-parse: 2.7.0 + sonic-boom: 4.0.1 + strip-json-comments: 3.1.1 + dev: true + + /pino-std-serializers@7.0.0: + resolution: {integrity: sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==} + dev: true + + /pino@9.2.0: + resolution: {integrity: sha512-g3/hpwfujK5a4oVbaefoJxezLzsDgLcNJeITvC6yrfwYeT9la+edCK42j5QpEQSQCZgTKapXvnQIdgZwvRaZug==} + hasBin: true + dependencies: + atomic-sleep: 1.0.0 + fast-redact: 3.5.0 + on-exit-leak-free: 2.1.2 + pino-abstract-transport: 1.2.0 + pino-std-serializers: 7.0.0 + process-warning: 3.0.0 + quick-format-unescaped: 4.0.4 + real-require: 0.2.0 + safe-stable-stringify: 2.4.3 + sonic-boom: 4.0.1 + thread-stream: 3.1.0 + dev: true + /pirates@4.0.6: resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} engines: {node: '>= 6'} @@ -9027,6 +9223,10 @@ packages: fromentries: 1.3.2 dev: true + /process-warning@3.0.0: + resolution: {integrity: sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==} + dev: true + /process@0.11.10: resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} engines: {node: '>= 0.6.0'} @@ -9115,6 +9315,10 @@ packages: resolution: {integrity: sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==} dev: true + /quick-format-unescaped@4.0.4: + resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} + dev: true + /quick-lru@5.1.1: resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} engines: {node: '>=10'} @@ -9635,12 +9839,28 @@ packages: util-deprecate: 1.0.2 dev: true + /readable-stream@4.5.2: + resolution: {integrity: sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + abort-controller: 3.0.0 + buffer: 6.0.3 + events: 3.3.0 + process: 0.11.10 + string_decoder: 1.3.0 + dev: true + /readdirp@3.6.0: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} dependencies: picomatch: 2.3.1 + /real-require@0.2.0: + resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} + engines: {node: '>= 12.13.0'} + dev: true + /redux-thunk@3.1.0(redux@5.0.1): resolution: {integrity: sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==} peerDependencies: @@ -9924,6 +10144,11 @@ packages: is-regex: 1.1.4 dev: true + /safe-stable-stringify@2.4.3: + resolution: {integrity: sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==} + engines: {node: '>=10'} + dev: true + /safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} @@ -9973,6 +10198,10 @@ packages: compute-scroll-into-view: 3.1.0 dev: false + /secure-json-parse@2.7.0: + resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==} + dev: true + /semver@5.7.2: resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} hasBin: true @@ -10151,6 +10380,12 @@ packages: tslib: 2.6.2 dev: true + /sonic-boom@4.0.1: + resolution: {integrity: sha512-hTSD/6JMLyT4r9zeof6UtuBDpjJ9sO08/nmS5djaA9eozT9oOlNdpXSnzcgj4FTqpk3nkLrs61l4gip9r1HCrQ==} + dependencies: + atomic-sleep: 1.0.0 + dev: true + /source-map-js@1.2.0: resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==} engines: {node: '>=0.10.0'} @@ -10194,6 +10429,11 @@ packages: which: 2.0.2 dev: true + /split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + dev: true + /sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} @@ -10306,6 +10546,12 @@ packages: safe-buffer: 5.1.2 dev: true + /string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + dependencies: + safe-buffer: 5.2.1 + dev: true + /strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} @@ -10499,6 +10745,12 @@ packages: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} dev: true + /thread-stream@3.1.0: + resolution: {integrity: sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==} + dependencies: + real-require: 0.2.0 + dev: true + /throttleit@1.0.1: resolution: {integrity: sha512-vDZpf9Chs9mAdfY046mcPt8fg5QSZr37hEH4TXYBnDF+izxgrbRGUAAaBvIk/fJm9aOFCGFd1EsNg5AZCbnQCQ==} dev: true @@ -11414,7 +11666,3 @@ packages: resolution: {integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==} engines: {node: '>=12.20'} dev: true - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false diff --git a/frontend/appflowy_web_app/public/appflowy.svg b/frontend/appflowy_web_app/public/appflowy.svg index b1ac8d66fb..a3e0c6491d 100644 --- a/frontend/appflowy_web_app/public/appflowy.svg +++ b/frontend/appflowy_web_app/public/appflowy.svg @@ -1,38 +1,12 @@ - - - - - - - - - - - \ No newline at end of file + + + + + + + + + + + + diff --git a/frontend/appflowy_web_app/public/covers/m_cover_image_1.png b/frontend/appflowy_web_app/public/covers/m_cover_image_1.png new file mode 100644 index 0000000000..fb72022287 Binary files /dev/null and b/frontend/appflowy_web_app/public/covers/m_cover_image_1.png differ diff --git a/frontend/appflowy_web_app/public/covers/m_cover_image_2.png b/frontend/appflowy_web_app/public/covers/m_cover_image_2.png new file mode 100644 index 0000000000..9ecf02d253 Binary files /dev/null and b/frontend/appflowy_web_app/public/covers/m_cover_image_2.png differ diff --git a/frontend/appflowy_web_app/public/covers/m_cover_image_3.png b/frontend/appflowy_web_app/public/covers/m_cover_image_3.png new file mode 100644 index 0000000000..97072b04f4 Binary files /dev/null and b/frontend/appflowy_web_app/public/covers/m_cover_image_3.png differ diff --git a/frontend/appflowy_web_app/public/covers/m_cover_image_4.png b/frontend/appflowy_web_app/public/covers/m_cover_image_4.png new file mode 100644 index 0000000000..00d26a0500 Binary files /dev/null and b/frontend/appflowy_web_app/public/covers/m_cover_image_4.png differ diff --git a/frontend/appflowy_web_app/public/covers/m_cover_image_5.png b/frontend/appflowy_web_app/public/covers/m_cover_image_5.png new file mode 100644 index 0000000000..3ecc9546c1 Binary files /dev/null and b/frontend/appflowy_web_app/public/covers/m_cover_image_5.png differ diff --git a/frontend/appflowy_web_app/public/covers/m_cover_image_6.png b/frontend/appflowy_web_app/public/covers/m_cover_image_6.png new file mode 100644 index 0000000000..0abd2700e8 Binary files /dev/null and b/frontend/appflowy_web_app/public/covers/m_cover_image_6.png differ diff --git a/frontend/appflowy_web_app/public/launch_splash.jpg b/frontend/appflowy_web_app/public/launch_splash.jpg deleted file mode 100644 index 7e3bb9cee6..0000000000 Binary files a/frontend/appflowy_web_app/public/launch_splash.jpg and /dev/null differ diff --git a/frontend/appflowy_web_app/public/og-image.png b/frontend/appflowy_web_app/public/og-image.png new file mode 100644 index 0000000000..8b5996608a Binary files /dev/null and b/frontend/appflowy_web_app/public/og-image.png differ diff --git a/frontend/appflowy_web_app/scripts/merge-coverage.cjs b/frontend/appflowy_web_app/scripts/merge-coverage.cjs new file mode 100644 index 0000000000..1939ca4ef9 --- /dev/null +++ b/frontend/appflowy_web_app/scripts/merge-coverage.cjs @@ -0,0 +1,35 @@ +const { execSync } = require('child_process'); +const fs = require('fs'); +const path = require('path'); +const jestCoverageFile = path.join(__dirname, '../coverage/jest/coverage-final.json'); +const cypressCoverageFile = path.join(__dirname, '../coverage/cypress/coverage-final.json'); +const nycOutputDir = path.join(__dirname, '../coverage/.nyc_output'); + +// Ensure .nyc_output directory exists +if (fs.existsSync(nycOutputDir)) { + fs.rmSync(nycOutputDir, { recursive: true }); +} +fs.mkdirSync(nycOutputDir, { recursive: true }); + +if (fs.existsSync(path.join(__dirname, '../coverage/merged'))) { + fs.rmSync(path.join(__dirname, '../coverage/merged'), { recursive: true }); +} +// Copy Jest coverage file +fs.copyFileSync(jestCoverageFile, path.join(nycOutputDir, 'jest-coverage.json')); +// Copy Cypress E2E coverage file +fs.copyFileSync(cypressCoverageFile, path.join(nycOutputDir, 'cypress-coverage.json')); + +// Merge coverage files +execSync('nyc merge ./coverage/.nyc_output ./coverage/merged/coverage-final.json', { stdio: 'inherit' }); + +// Move the merged result to the .nyc_output directory +fs.rmSync(nycOutputDir, { recursive: true }); +fs.mkdirSync(nycOutputDir, { recursive: true }); +fs.copyFileSync(path.join(__dirname, '../coverage/merged/coverage-final.json'), path.join(nycOutputDir, 'out.json')); + +// Generate final merged report +execSync('nyc report --reporter=html --reporter=text-summary --report-dir=coverage/merged --temp-dir=coverage/.nyc_output', { stdio: 'inherit' }); +console.log(`Merged coverage report written to coverage/merged`); + + + diff --git a/frontend/appflowy_web_app/server.cjs b/frontend/appflowy_web_app/server.cjs deleted file mode 100644 index e13c337faa..0000000000 --- a/frontend/appflowy_web_app/server.cjs +++ /dev/null @@ -1,114 +0,0 @@ -const path = require('path'); -const fs = require('fs'); -const pino = require('pino'); -const cheerio = require('cheerio'); -const axios = require('axios'); - -const distDir = path.join(__dirname, 'dist'); -const indexPath = path.join(distDir, 'index.html'); - -const setOrUpdateMetaTag = ($, selector, attribute, content) => { - if ($(selector).length === 0) { - $('head').append(``); - } else { - $(selector).attr('content', content); - } -}; -// Create a new logger instance -const logger = pino({ - transport: { - target: 'pino-pretty', - level: 'info', - options: { - colorize: true, - translateTime: 'SYS:standard', - destination: `${__dirname}/pino-logger.log`, - }, - }, -}); - -const logRequestTimer = (req) => { - const start = Date.now(); - const pathname = new URL(req.url).pathname; - logger.info(`Incoming request: ${pathname}`); - return () => { - const duration = Date.now() - start; - logger.info(`Request for ${pathname} took ${duration}ms`); - }; -}; - -const fetchMetaData = async (url) => { - try { - const response = await axios.get(url); - return response.data; - } catch (error) { - logger.error('Error fetching meta data', error); - return null; - } -}; - -const createServer = async (req) => { - const timer = logRequestTimer(req); - - if (req.method === 'GET') { - const pageId = req.url.split('/').pop(); - let htmlData = fs.readFileSync(indexPath, 'utf8'); - const $ = cheerio.load(htmlData); - if (!pageId) { - timer(); - return new Response($.html(), { - headers: { 'Content-Type': 'text/html' }, - }); - } - - const description = 'Write, share, comment, react, and publish docs quickly and securely on AppFlowy.'; - let title = 'AppFlowy'; - const url = 'https://appflowy.com'; - let image = 'https://d3uafhn8yrvdfn.cloudfront.net/website/production/_next/static/media/og-image.e347bfb5.png'; - // Inject meta data into the HTML to support SEO and social sharing - // if (metaData) { - // title = metaData.title; - // image = metaData.image; - // } - - $('title').text(title); - setOrUpdateMetaTag($, 'meta[name="description"]', 'name', description); - setOrUpdateMetaTag($, 'meta[property="og:title"]', 'property', title); - setOrUpdateMetaTag($, 'meta[property="og:description"]', 'property', description); - setOrUpdateMetaTag($, 'meta[property="og:image"]', 'property', image); - setOrUpdateMetaTag($, 'meta[property="og:url"]', 'property', url); - setOrUpdateMetaTag($, 'meta[property="og:type"]', 'property', 'article'); - setOrUpdateMetaTag($, 'meta[name="twitter:card"]', 'name', 'summary_large_image'); - setOrUpdateMetaTag($, 'meta[name="twitter:title"]', 'name', title); - setOrUpdateMetaTag($, 'meta[name="twitter:description"]', 'name', description); - setOrUpdateMetaTag($, 'meta[name="twitter:image"]', 'name', image); - - timer(); - return new Response($.html(), { - headers: { 'Content-Type': 'text/html' }, - }); - } else { - timer(); - logger.error({ message: 'Method not allowed', method: req.method }); - return new Response('Method not allowed', { status: 405 }); - } -}; - -const start = () => { - try { - Bun.serve({ - port: 3000, - fetch: createServer, - error: (err) => { - logger.error(`Internal Server Error: ${err}`); - return new Response('Internal Server Error', { status: 500 }); - }, - }); - logger.info(`Server is running on port 3000`); - } catch (err) { - logger.error(err); - process.exit(1); - } -}; - -start(); diff --git a/frontend/appflowy_web_app/src/application/collab.type.ts b/frontend/appflowy_web_app/src/application/collab.type.ts index 567be3b4ed..27472acfcd 100644 --- a/frontend/appflowy_web_app/src/application/collab.type.ts +++ b/frontend/appflowy_web_app/src/application/collab.type.ts @@ -32,6 +32,7 @@ export enum BlockType { OutlineBlock = 'outline', TableBlock = 'table', TableCell = 'table/cell', + LinkPreview = 'link_preview', } export enum InlineBlockType { @@ -46,7 +47,7 @@ export enum AlignType { } export interface BlockData { - bg_color?: string; + bgColor?: string; font_color?: string; align?: AlignType; } @@ -79,6 +80,10 @@ export interface MathEquationBlockData extends BlockData { formula?: string; } +export interface LinkPreviewBlockData extends BlockData { + url?: string; +} + export enum ImageType { Local = 0, Internal = 1, @@ -111,6 +116,8 @@ export interface TableCellBlockData extends BlockData { height: number; rowPosition: number; width: number; + rowBackgroundColor: string; + colBackgroundColor: string; } export interface DatabaseNodeData extends BlockData { @@ -636,3 +643,27 @@ export enum LineHeightLayout { normal = 'normal', large = 'large', } + +export interface ViewMetaIcon { + ty: number; + value: string; +} + +export interface PublishViewInfo { + view_id: string; + name: string; + icon: ViewMetaIcon | null; + extra: string | null; + layout: number; + created_at: string; + created_by: string; + last_edited_time: string; + last_edited_by: string; + child_views: PublishViewInfo[] | null; +} + +export interface PublishViewMetaData { + view: PublishViewInfo; + child_views: PublishViewInfo[]; + ancestor_views: PublishViewInfo[]; +} diff --git a/frontend/appflowy_web_app/src/application/database-yjs/__tests__/selector.test.tsx b/frontend/appflowy_web_app/src/application/database-yjs/__tests__/selector.test.tsx index 23c8bc8221..46473fae01 100644 --- a/frontend/appflowy_web_app/src/application/database-yjs/__tests__/selector.test.tsx +++ b/frontend/appflowy_web_app/src/application/database-yjs/__tests__/selector.test.tsx @@ -18,7 +18,6 @@ import { useSortsSelector, } from '../selector'; import { useDatabaseViewId } from '../context'; -import { IdProvider } from '@/components/_shared/context-provider/IdProvider'; import { DatabaseContextProvider } from '@/components/database/DatabaseContext'; import { withTestingDatabase } from '@/application/database-yjs/__tests__/withTestingData'; import { expect } from '@jest/globals'; @@ -31,11 +30,9 @@ const wrapperCreator = (viewId: string, doc: YDoc, rowDocMap: Y.Map) => ({ children }: { children: React.ReactNode }) => { return ( - - - {children} - - + + {children} + ); }; diff --git a/frontend/appflowy_web_app/src/application/database-yjs/context.ts b/frontend/appflowy_web_app/src/application/database-yjs/context.ts index 5d51001976..cbac5bbd45 100644 --- a/frontend/appflowy_web_app/src/application/database-yjs/context.ts +++ b/frontend/appflowy_web_app/src/application/database-yjs/context.ts @@ -1,4 +1,5 @@ import { YDatabase, YDatabaseRow, YDoc, YjsDatabaseKey, YjsEditorKey } from '@/application/collab.type'; +import { ViewMeta } from '@/application/db/tables/view_metas'; import { createContext, useContext } from 'react'; import * as Y from 'yjs'; @@ -9,6 +10,10 @@ export interface DatabaseContextState { rowDocMap: Y.Map; isDatabaseRowPage?: boolean; navigateToRow?: (rowId: string) => void; + loadView?: (viewId: string) => Promise; + getViewRowsMap?: (viewId: string, rowIds: string[]) => Promise<{ rows: Y.Map; destroy: () => void }>; + loadViewMeta?: (viewId: string) => Promise; + navigateToView?: (viewId: string) => Promise; } export const DatabaseContext = createContext(null); diff --git a/frontend/appflowy_web_app/src/application/database-yjs/selector.ts b/frontend/appflowy_web_app/src/application/database-yjs/selector.ts index 884c58516d..2832a639f9 100644 --- a/frontend/appflowy_web_app/src/application/database-yjs/selector.ts +++ b/frontend/appflowy_web_app/src/application/database-yjs/selector.ts @@ -1,12 +1,4 @@ -import { - FieldId, - SortId, - YDatabaseField, - YDoc, - YjsDatabaseKey, - YjsEditorKey, - YjsFolderKey, -} from '@/application/collab.type'; +import { FieldId, SortId, YDatabaseField, YDoc, YjsDatabaseKey, YjsEditorKey } from '@/application/collab.type'; import { getCell, metaIdFromRowId, MIN_COLUMN_WIDTH } from '@/application/database-yjs/const'; import { useDatabase, @@ -19,7 +11,6 @@ import { import { filterBy, parseFilter } from '@/application/database-yjs/filter'; import { groupByField } from '@/application/database-yjs/group'; import { sortBy } from '@/application/database-yjs/sort'; -import { useViewsIdSelector } from '@/application/folder-yjs'; import { parseYDatabaseCellToCell } from '@/application/database-yjs/cell.parse'; import { DateTimeCell } from '@/application/database-yjs/cell.type'; import * as dayjs from 'dayjs'; @@ -42,9 +33,8 @@ export interface Row { const defaultVisible = [FieldVisibility.AlwaysShown, FieldVisibility.HideWhenEmpty]; -export function useDatabaseViewsSelector(iidIndex: string) { +export function useDatabaseViewsSelector(_iidIndex: string) { const database = useDatabase(); - const { viewsId: visibleViewsId, views: folderViews } = useViewsIdSelector(); const views = database?.get(YjsDatabaseKey.views); const [viewIds, setViewIds] = useState([]); @@ -65,22 +55,7 @@ export function useDatabaseViewsSelector(iidIndex: string) { return Number(viewB.created_at) - Number(viewA.created_at); }); - const viewsId = []; - - for (const viewItem of viewsSorted) { - const [key] = viewItem; - const view = folderViews?.get(key); - - if ( - visibleViewsId.includes(key) && - view && - (view.get(YjsFolderKey.bid) === iidIndex || view.get(YjsFolderKey.id) === iidIndex) - ) { - viewsId.push(key); - } - } - - setViewIds(viewsId); + setViewIds(viewsSorted.map(([key]) => key)); }; observerEvent(); @@ -89,7 +64,7 @@ export function useDatabaseViewsSelector(iidIndex: string) { return () => { views.unobserve(observerEvent); }; - }, [visibleViewsId, views, folderViews, iidIndex]); + }, [views]); return { childViews, diff --git a/frontend/appflowy_web_app/src/application/services/js-services/cache/db.ts b/frontend/appflowy_web_app/src/application/db/index.ts similarity index 55% rename from frontend/appflowy_web_app/src/application/services/js-services/cache/db.ts rename to frontend/appflowy_web_app/src/application/db/index.ts index a4d888498b..6c7ae21264 100644 --- a/frontend/appflowy_web_app/src/application/services/js-services/cache/db.ts +++ b/frontend/appflowy_web_app/src/application/db/index.ts @@ -2,6 +2,17 @@ import { YDoc } from '@/application/collab.type'; import { databasePrefix } from '@/application/constants'; import { IndexeddbPersistence } from 'y-indexeddb'; import * as Y from 'yjs'; +import BaseDexie from 'dexie'; +import { viewMetasSchema, ViewMetasTable } from '@/application/db/tables/view_metas'; + +type DexieTables = ViewMetasTable; + +export type Dexie = BaseDexie & T; + +export const db = new BaseDexie(`${databasePrefix}_cache`) as Dexie; +const schema = Object.assign({}, viewMetasSchema); + +db.version(1).stores(schema); const openedSet = new Set(); @@ -32,10 +43,16 @@ export async function openCollabDB(docName: string): Promise { return doc as YDoc; } -export function getCollabDBName(id: string, type: string, uuid?: string) { - if (!uuid) { - return `${type}_${id}`; +export async function closeCollabDB(docName: string) { + const name = `${databasePrefix}_${docName}`; + + if (openedSet.has(name)) { + openedSet.delete(name); } - return `${uuid}_${type}_${id}`; + const doc = new Y.Doc(); + + const provider = new IndexeddbPersistence(name, doc); + + await provider.destroy(); } diff --git a/frontend/appflowy_web_app/src/application/db/tables/view_metas.ts b/frontend/appflowy_web_app/src/application/db/tables/view_metas.ts new file mode 100644 index 0000000000..1f39ac19f8 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/db/tables/view_metas.ts @@ -0,0 +1,17 @@ +import { Table } from 'dexie'; +import { PublishViewInfo } from '@/application/collab.type'; + +export type ViewMeta = { + publish_name: string; + + child_views: PublishViewInfo[]; + ancestor_views: PublishViewInfo[]; +} & PublishViewInfo; + +export type ViewMetasTable = { + view_metas: Table; +}; + +export const viewMetasSchema = { + view_metas: 'publish_name', +}; diff --git a/frontend/appflowy_web_app/src/application/folder-yjs/context.ts b/frontend/appflowy_web_app/src/application/folder-yjs/context.ts deleted file mode 100644 index cb0e1f63ff..0000000000 --- a/frontend/appflowy_web_app/src/application/folder-yjs/context.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { ViewLayout, YFolder, YjsFolderKey } from '@/application/collab.type'; -import { createContext, useContext } from 'react'; -import { useParams } from 'react-router-dom'; - -export interface Crumb { - viewId: string; - rowId?: string; - name: string; - icon: string; -} - -export const FolderContext = createContext<{ - folder: YFolder | null; - onNavigateToView?: (viewId: string) => void; - crumbs?: Crumb[]; - setCrumbs?: React.Dispatch>; -} | null>(null); - -export const useFolderContext = () => { - return useContext(FolderContext)?.folder; -}; - -export const useViewLayout = () => { - const folder = useFolderContext(); - const { objectId } = useParams(); - const views = folder?.get(YjsFolderKey.views); - const view = objectId ? views?.get(objectId) : null; - - return Number(view?.get(YjsFolderKey.layout)) as ViewLayout; -}; - -export const useNavigateToView = () => { - return useContext(FolderContext)?.onNavigateToView; -}; - -export const useCrumbs = () => { - return useContext(FolderContext)?.crumbs; -}; diff --git a/frontend/appflowy_web_app/src/application/folder-yjs/folder.type.ts b/frontend/appflowy_web_app/src/application/folder-yjs/folder.type.ts deleted file mode 100644 index ce9bebcefb..0000000000 --- a/frontend/appflowy_web_app/src/application/folder-yjs/folder.type.ts +++ /dev/null @@ -1,9 +0,0 @@ -export enum CoverType { - NormalColor = 'color', - GradientColor = 'gradient', - BuildInImage = 'built_in', - CustomImage = 'custom', - LocalImage = 'local', - UpsplashImage = 'unsplash', - None = 'none', -} diff --git a/frontend/appflowy_web_app/src/application/folder-yjs/index.ts b/frontend/appflowy_web_app/src/application/folder-yjs/index.ts deleted file mode 100644 index f94cc509da..0000000000 --- a/frontend/appflowy_web_app/src/application/folder-yjs/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './selector'; -export * from './context'; diff --git a/frontend/appflowy_web_app/src/application/folder-yjs/selector.ts b/frontend/appflowy_web_app/src/application/folder-yjs/selector.ts deleted file mode 100644 index 8e43efbf6a..0000000000 --- a/frontend/appflowy_web_app/src/application/folder-yjs/selector.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { YjsFolderKey, YView } from '@/application/collab.type'; -import { useFolderContext } from '@/application/folder-yjs/context'; -import { useEffect, useState } from 'react'; - -export function useViewsIdSelector() { - const folder = useFolderContext(); - const [viewsId, setViewsId] = useState([]); - const views = folder?.get(YjsFolderKey.views); - const trash = folder?.get(YjsFolderKey.section)?.get(YjsFolderKey.trash); - const meta = folder?.get(YjsFolderKey.meta); - - useEffect(() => { - if (!views) { - return; - } - - const trashUid = trash ? Array.from(trash.keys())[0] : null; - const userTrash = trashUid ? trash?.get(trashUid) : null; - - const collectIds = () => { - const trashIds = userTrash?.toJSON()?.map((item) => item.id) || []; - - return Array.from(views.keys()).filter((id) => { - return !trashIds.includes(id) && id !== meta?.get(YjsFolderKey.current_workspace); - }); - }; - - setViewsId(collectIds()); - const observerEvent = () => setViewsId(collectIds()); - - views.observe(observerEvent); - userTrash?.observe(observerEvent); - - return () => { - views.unobserve(observerEvent); - userTrash?.unobserve(observerEvent); - }; - }, [views, trash, meta]); - - return { - viewsId, - views, - }; -} - -export function useViewSelector(viewId: string) { - const folder = useFolderContext(); - const [clock, setClock] = useState(0); - const [view, setView] = useState(null); - - useEffect(() => { - if (!folder) return; - - const view = folder.get(YjsFolderKey.views)?.get(viewId); - - setView(view || null); - const observerEvent = () => setClock((prev) => prev + 1); - - view?.observe(observerEvent); - - return () => { - view?.unobserve(observerEvent); - }; - }, [folder, viewId]); - - return { - clock, - view, - }; -} diff --git a/frontend/appflowy_web_app/src/application/publish/context.tsx b/frontend/appflowy_web_app/src/application/publish/context.tsx new file mode 100644 index 0000000000..8f99c28a42 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/publish/context.tsx @@ -0,0 +1,163 @@ +import { YDoc } from '@/application/collab.type'; +import { db } from '@/application/db'; +import { ViewMeta } from '@/application/db/tables/view_metas'; +import { AFConfigContext } from '@/components/app/AppConfig'; +import { useLiveQuery } from 'dexie-react-hooks'; +import { createContext, useCallback, useContext, useEffect, useRef } from 'react'; +import { useNavigate } from 'react-router-dom'; +import * as Y from 'yjs'; + +export interface PublishContextType { + namespace: string; + publishName: string; + viewMeta?: ViewMeta; + toView: (viewId: string) => Promise; + loadViewMeta: (viewId: string) => Promise; + getViewRowsMap?: (viewId: string, rowIds: string[]) => Promise<{ rows: Y.Map; destroy: () => void }>; + + loadView: (viewId: string) => Promise; +} + +export const PublishContext = createContext(null); + +export const PublishProvider = ({ + children, + namespace, + publishName, +}: { + children: React.ReactNode; + namespace: string; + publishName: string; +}) => { + const viewMeta = useLiveQuery(async () => { + const name = `${namespace}_${publishName}`; + + return db.view_metas.get(name); + }, [namespace, publishName]); + + const prevViewMeta = useRef(viewMeta); + + const service = useContext(AFConfigContext)?.service; + const navigate = useNavigate(); + const toView = useCallback( + async (viewId: string) => { + try { + const res = await service?.getPublishInfo(viewId); + + if (!res) { + throw new Error('Not found'); + } + + const { namespace, publishName } = res; + + navigate(`/${namespace}/${publishName}`); + } catch (e) { + return Promise.reject(e); + } + }, + [navigate, service] + ); + + const loadViewMeta = useCallback( + async (viewId: string) => { + try { + const info = await service?.getPublishInfo(viewId); + + if (!info) { + throw new Error('View has not been published yet'); + } + + const { namespace, publishName } = info; + + const res = await service?.getPublishViewMeta(namespace, publishName); + + if (!res) { + throw new Error('View meta has not been published yet'); + } + + return res; + } catch (e) { + return Promise.reject(e); + } + }, + [service] + ); + + const getViewRowsMap = useCallback( + async (viewId: string, rowIds: string[]) => { + try { + const info = await service?.getPublishInfo(viewId); + + if (!info) { + throw new Error('View has not been published yet'); + } + + const { namespace, publishName } = info; + const res = await service?.getPublishDatabaseViewRows(namespace, publishName, rowIds); + + if (!res) { + throw new Error('View has not been published yet'); + } + + return res; + } catch (e) { + return Promise.reject(e); + } + }, + [service] + ); + + const loadView = useCallback( + async (viewId: string) => { + try { + const res = await service?.getPublishInfo(viewId); + + if (!res) { + throw new Error('View has not been published yet'); + } + + const { namespace, publishName } = res; + + const data = service?.getPublishView(namespace, publishName); + + if (!data) { + throw new Error('View has not been published yet'); + } + + return data; + } catch (e) { + return Promise.reject(e); + } + }, + [service] + ); + + useEffect(() => { + if (!viewMeta && prevViewMeta.current) { + window.location.reload(); + return; + } + + prevViewMeta.current = viewMeta; + }, [viewMeta]); + + return ( + + {children} + + ); +}; + +export function usePublishContext() { + return useContext(PublishContext); +} diff --git a/frontend/appflowy_web_app/src/application/publish/index.ts b/frontend/appflowy_web_app/src/application/publish/index.ts new file mode 100644 index 0000000000..c38e8e8215 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/publish/index.ts @@ -0,0 +1 @@ +export * from './context'; diff --git a/frontend/appflowy_web_app/src/application/services/js-services/__tests__/cache.test.ts b/frontend/appflowy_web_app/src/application/services/js-services/__tests__/cache.test.ts deleted file mode 100644 index 9a7e3fcb9d..0000000000 --- a/frontend/appflowy_web_app/src/application/services/js-services/__tests__/cache.test.ts +++ /dev/null @@ -1,310 +0,0 @@ -import { CollabType } from '@/application/collab.type'; -import * as Y from 'yjs'; -import { withTestingYDoc } from '@/application/slate-yjs/__tests__/withTestingYjsEditor'; -import { expect } from '@jest/globals'; -import { getCollab, batchCollab, collabTypeToDBType } from '../cache'; -import { applyYDoc } from '@/application/ydoc/apply'; -import { getCollabDBName, openCollabDB } from '../cache/db'; -import { StrategyType } from '../cache/types'; - -jest.mock('@/application/ydoc/apply', () => ({ - applyYDoc: jest.fn(), -})); -jest.mock('../cache/db', () => ({ - openCollabDB: jest.fn(), - getCollabDBName: jest.fn(), -})); - -const emptyDoc = new Y.Doc(); -const normalDoc = withTestingYDoc('1'); -const mockFetcher = jest.fn(); -const mockBatchFetcher = jest.fn(); - -describe('Cache functions', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - describe('getCollab', () => { - describe('with CACHE_ONLY strategy', () => { - it('should throw error when no cache', async () => { - (openCollabDB as jest.Mock).mockResolvedValue(emptyDoc); - (getCollabDBName as jest.Mock).mockReturnValue('testDB'); - - await expect( - getCollab( - mockFetcher, - { - collabId: 'id1', - collabType: CollabType.Document, - }, - StrategyType.CACHE_ONLY - ) - ).rejects.toThrow('No cache found'); - }); - it('should fetch collab with CACHE_ONLY strategy and existing cache', async () => { - (openCollabDB as jest.Mock).mockResolvedValue(normalDoc); - (getCollabDBName as jest.Mock).mockReturnValue('testDB'); - - const result = await getCollab( - mockFetcher, - { - collabId: 'id1', - collabType: CollabType.Document, - }, - StrategyType.CACHE_ONLY - ); - - expect(result).toBe(normalDoc); - expect(mockFetcher).not.toHaveBeenCalled(); - expect(applyYDoc).not.toHaveBeenCalled(); - }); - }); - - describe('with CACHE_FIRST strategy', () => { - it('should fetch collab with CACHE_FIRST strategy and existing cache', async () => { - (openCollabDB as jest.Mock).mockResolvedValue(normalDoc); - (getCollabDBName as jest.Mock).mockReturnValue('testDB'); - - mockFetcher.mockResolvedValue({ state: new Uint8Array() }); - - const result = await getCollab( - mockFetcher, - { - collabId: 'id1', - collabType: CollabType.Document, - }, - StrategyType.CACHE_FIRST - ); - - expect(result).toBe(normalDoc); - expect(mockFetcher).not.toHaveBeenCalled(); - expect(applyYDoc).not.toHaveBeenCalled(); - }); - - it('should fetch collab with CACHE_FIRST strategy and no cache', async () => { - (openCollabDB as jest.Mock).mockResolvedValue(emptyDoc); - (getCollabDBName as jest.Mock).mockReturnValue('testDB'); - - mockFetcher.mockResolvedValue({ state: new Uint8Array() }); - - const result = await getCollab( - mockFetcher, - { - collabId: 'id1', - collabType: CollabType.Document, - }, - StrategyType.CACHE_FIRST - ); - - expect(result).toBe(emptyDoc); - expect(mockFetcher).toHaveBeenCalled(); - expect(applyYDoc).toHaveBeenCalled(); - }); - }); - - describe('with CACHE_AND_NETWORK strategy', () => { - it('should fetch collab with CACHE_AND_NETWORK strategy and existing cache', async () => { - (openCollabDB as jest.Mock).mockResolvedValue(normalDoc); - (getCollabDBName as jest.Mock).mockReturnValue('testDB'); - - mockFetcher.mockResolvedValue({ state: new Uint8Array() }); - - const result = await getCollab( - mockFetcher, - { - collabId: 'id1', - collabType: CollabType.Document, - }, - StrategyType.CACHE_AND_NETWORK - ); - - expect(result).toBe(normalDoc); - expect(mockFetcher).toHaveBeenCalled(); - expect(applyYDoc).toHaveBeenCalled(); - }); - - it('should fetch collab with CACHE_AND_NETWORK strategy and no cache', async () => { - (openCollabDB as jest.Mock).mockResolvedValue(emptyDoc); - (getCollabDBName as jest.Mock).mockReturnValue('testDB'); - - mockFetcher.mockResolvedValue({ state: new Uint8Array() }); - - const result = await getCollab( - mockFetcher, - { - collabId: 'id1', - collabType: CollabType.Document, - }, - StrategyType.CACHE_AND_NETWORK - ); - - expect(result).toBe(emptyDoc); - expect(mockFetcher).toHaveBeenCalled(); - expect(applyYDoc).toHaveBeenCalled(); - }); - }); - - describe('with default strategy', () => { - it('should fetch collab with default strategy', async () => { - (openCollabDB as jest.Mock).mockResolvedValue(normalDoc); - (getCollabDBName as jest.Mock).mockReturnValue('testDB'); - - mockFetcher.mockResolvedValue({ state: new Uint8Array() }); - - const result = await getCollab( - mockFetcher, - { - collabId: 'id1', - collabType: CollabType.Document, - }, - StrategyType.NETWORK_ONLY - ); - - expect(result).toBe(normalDoc); - expect(mockFetcher).toHaveBeenCalled(); - expect(applyYDoc).toHaveBeenCalled(); - }); - }); - }); - - describe('batchCollab', () => { - describe('with CACHE_ONLY strategy', () => { - it('should batch fetch collabs with CACHE_ONLY strategy and no cache', async () => { - (openCollabDB as jest.Mock).mockResolvedValue(emptyDoc); - - (getCollabDBName as jest.Mock).mockReturnValue('testDB'); - - await expect( - batchCollab( - mockBatchFetcher, - [ - { - collabId: 'id1', - collabType: CollabType.Document, - }, - ], - StrategyType.CACHE_ONLY - ) - ).rejects.toThrow('No cache found'); - }); - - it('should batch fetch collabs with CACHE_ONLY strategy and existing cache', async () => { - (openCollabDB as jest.Mock).mockResolvedValue(normalDoc); - - (getCollabDBName as jest.Mock).mockReturnValue('testDB'); - - await batchCollab( - mockBatchFetcher, - [ - { - collabId: 'id1', - collabType: CollabType.Document, - }, - ], - StrategyType.CACHE_ONLY - ); - - expect(mockBatchFetcher).not.toHaveBeenCalled(); - }); - }); - - describe('with CACHE_FIRST strategy', () => { - it('should batch fetch collabs with CACHE_FIRST strategy and existing cache', async () => { - (openCollabDB as jest.Mock).mockResolvedValue(normalDoc); - - (getCollabDBName as jest.Mock).mockReturnValue('testDB'); - - await batchCollab( - mockBatchFetcher, - [ - { - collabId: 'id1', - collabType: CollabType.Document, - }, - ], - StrategyType.CACHE_FIRST - ); - - expect(mockBatchFetcher).not.toHaveBeenCalled(); - }); - - it('should batch fetch collabs with CACHE_FIRST strategy and no cache', async () => { - (openCollabDB as jest.Mock).mockResolvedValue(emptyDoc); - - (getCollabDBName as jest.Mock).mockReturnValue('testDB'); - mockBatchFetcher.mockResolvedValue({ id1: [1, 2, 3] }); - - await batchCollab( - mockBatchFetcher, - [ - { - collabId: 'id1', - collabType: CollabType.Document, - }, - ], - StrategyType.CACHE_FIRST - ); - - expect(mockBatchFetcher).toHaveBeenCalled(); - expect(applyYDoc).toHaveBeenCalled(); - }); - }); - - describe('with CACHE_AND_NETWORK strategy', () => { - it('should batch fetch collabs with CACHE_AND_NETWORK strategy', async () => { - (openCollabDB as jest.Mock).mockResolvedValue(normalDoc); - - (getCollabDBName as jest.Mock).mockReturnValue('testDB'); - mockBatchFetcher.mockResolvedValue({ id1: [1, 2, 3] }); - - await batchCollab( - mockBatchFetcher, - [ - { - collabId: 'id1', - collabType: CollabType.Document, - }, - ], - StrategyType.CACHE_AND_NETWORK - ); - - expect(mockBatchFetcher).toHaveBeenCalled(); - expect(applyYDoc).toHaveBeenCalled(); - }); - - it('should batch fetch collabs with CACHE_AND_NETWORK strategy and no cache', async () => { - (openCollabDB as jest.Mock).mockResolvedValue(emptyDoc); - - (getCollabDBName as jest.Mock).mockReturnValue('testDB'); - mockBatchFetcher.mockResolvedValue({ id1: [1, 2, 3] }); - - await batchCollab( - mockBatchFetcher, - [ - { - collabId: 'id1', - collabType: CollabType.Document, - }, - ], - StrategyType.CACHE_AND_NETWORK - ); - - expect(mockBatchFetcher).toHaveBeenCalled(); - expect(applyYDoc).toHaveBeenCalled(); - }); - }); - }); -}); - -describe('collabTypeToDBType', () => { - it('should return correct DB type', () => { - expect(collabTypeToDBType(CollabType.Document)).toBe('document'); - expect(collabTypeToDBType(CollabType.Folder)).toBe('folder'); - expect(collabTypeToDBType(CollabType.Database)).toBe('database'); - expect(collabTypeToDBType(CollabType.WorkspaceDatabase)).toBe('databases'); - expect(collabTypeToDBType(CollabType.DatabaseRow)).toBe('database_row'); - expect(collabTypeToDBType(CollabType.UserAwareness)).toBe('user_awareness'); - expect(collabTypeToDBType(CollabType.Empty)).toBe(''); - }); -}); diff --git a/frontend/appflowy_web_app/src/application/services/js-services/__tests__/fetch.test.ts b/frontend/appflowy_web_app/src/application/services/js-services/__tests__/fetch.test.ts index afdc418c2a..575efef159 100644 --- a/frontend/appflowy_web_app/src/application/services/js-services/__tests__/fetch.test.ts +++ b/frontend/appflowy_web_app/src/application/services/js-services/__tests__/fetch.test.ts @@ -1,13 +1,13 @@ import { expect } from '@jest/globals'; -import { fetchCollab, batchFetchCollab } from '../fetch'; -import { CollabType } from '@/application/collab.type'; +import { fetchPublishView, fetchPublishViewMeta, fetchViewInfo } from '../fetch'; import { APIService } from '@/application/services/js-services/wasm'; jest.mock('@/application/services/js-services/wasm', () => { return { APIService: { - getCollab: jest.fn(), - batchGetCollab: jest.fn(), + getPublishView: jest.fn(), + getPublishViewMeta: jest.fn(), + getPublishInfoWithViewId: jest.fn(), }, }; }); @@ -17,41 +17,100 @@ describe('Collab fetch functions with deduplication', () => { jest.clearAllMocks(); }); - describe('fetchCollab', () => { - it('should fetch collab without duplicating requests', async () => { - const workspaceId = 'workspace1'; - const id = 'id1'; - const type = CollabType.Document; + describe('fetchPublishView', () => { + it('should fetch publish view without duplicating requests', async () => { + const namespace = 'namespace1'; + const publishName = 'publish1'; const mockResponse = { data: 'mockData' }; - (APIService.getCollab as jest.Mock).mockResolvedValue(mockResponse); + (APIService.getPublishView as jest.Mock).mockResolvedValue(mockResponse); - const result1 = fetchCollab(workspaceId, id, type); - const result2 = fetchCollab(workspaceId, id, type); + const result1 = fetchPublishView(namespace, publishName); + const result2 = fetchPublishView(namespace, publishName); expect(result1).toBe(result2); await expect(result1).resolves.toEqual(mockResponse); - expect(APIService.getCollab).toHaveBeenCalledTimes(1); + expect(APIService.getPublishView).toHaveBeenCalledTimes(1); + }); + + it('should fetch publish view with different params', async () => { + const namespace = 'namespace1'; + const publishName = 'publish1'; + const mockResponse = { data: 'mockData' }; + + (APIService.getPublishView as jest.Mock).mockResolvedValue(mockResponse); + + const result1 = fetchPublishView(namespace, publishName); + const result2 = fetchPublishView(namespace, 'publish2'); + + expect(result1).not.toBe(result2); + await expect(result1).resolves.toEqual(mockResponse); + await expect(result2).resolves.toEqual(mockResponse); + expect(APIService.getPublishView).toHaveBeenCalledTimes(2); }); }); - describe('batchFetchCollab', () => { - it('should batch fetch collabs without duplicating requests', async () => { - const workspaceId = 'workspace1'; - const params = [ - { collabId: 'id1', collabType: CollabType.Document }, - { collabId: 'id2', collabType: CollabType.Folder }, - ]; + describe('fetchViewInfo', () => { + it('should fetch view info without duplicating requests', async () => { + const viewId = 'view1'; const mockResponse = { data: 'mockData' }; - (APIService.batchGetCollab as jest.Mock).mockResolvedValue(mockResponse); + (APIService.getPublishInfoWithViewId as jest.Mock).mockResolvedValue(mockResponse); - const result1 = batchFetchCollab(workspaceId, params); - const result2 = batchFetchCollab(workspaceId, params); + const result1 = fetchViewInfo(viewId); + const result2 = fetchViewInfo(viewId); expect(result1).toBe(result2); await expect(result1).resolves.toEqual(mockResponse); - expect(APIService.batchGetCollab).toHaveBeenCalledTimes(1); + expect(APIService.getPublishInfoWithViewId).toHaveBeenCalledTimes(1); + }); + + it('should fetch view info with different params', async () => { + const viewId = 'view1'; + const mockResponse = { data: 'mockData' }; + + (APIService.getPublishInfoWithViewId as jest.Mock).mockResolvedValue(mockResponse); + + const result1 = fetchViewInfo(viewId); + const result2 = fetchViewInfo('view2'); + + expect(result1).not.toBe(result2); + await expect(result1).resolves.toEqual(mockResponse); + await expect(result2).resolves.toEqual(mockResponse); + expect(APIService.getPublishInfoWithViewId).toHaveBeenCalledTimes(2); + }); + }); + + describe('fetchPublishViewMeta', () => { + it('should fetch publish view meta without duplicating requests', async () => { + const namespace = 'namespace1'; + const publishName = 'publish1'; + const mockResponse = { data: 'mockData' }; + + (APIService.getPublishViewMeta as jest.Mock).mockResolvedValue(mockResponse); + + const result1 = fetchPublishViewMeta(namespace, publishName); + const result2 = fetchPublishViewMeta(namespace, publishName); + + expect(result1).toBe(result2); + await expect(result1).resolves.toEqual(mockResponse); + expect(APIService.getPublishViewMeta).toHaveBeenCalledTimes(1); + }); + + it('should fetch publish view meta with different params', async () => { + const namespace = 'namespace1'; + const publishName = 'publish1'; + const mockResponse = { data: 'mockData' }; + + (APIService.getPublishViewMeta as jest.Mock).mockResolvedValue(mockResponse); + + const result1 = fetchPublishViewMeta(namespace, publishName); + const result2 = fetchPublishViewMeta(namespace, 'publish2'); + + expect(result1).not.toBe(result2); + await expect(result1).resolves.toEqual(mockResponse); + await expect(result2).resolves.toEqual(mockResponse); + expect(APIService.getPublishViewMeta).toHaveBeenCalledTimes(2); }); }); }); diff --git a/frontend/appflowy_web_app/src/application/services/js-services/__tests__/index.test.ts b/frontend/appflowy_web_app/src/application/services/js-services/__tests__/index.test.ts new file mode 100644 index 0000000000..5dbf0b4f8c --- /dev/null +++ b/frontend/appflowy_web_app/src/application/services/js-services/__tests__/index.test.ts @@ -0,0 +1,128 @@ +import { withTestingYDoc } from '@/application/slate-yjs/__tests__/withTestingYjsEditor'; +import { AFClientService } from '../index'; +import { fetchViewInfo } from '@/application/services/js-services/fetch'; +import { expect, jest } from '@jest/globals'; +import { getBatchCollabs, getPublishView, getPublishViewMeta } from '@/application/services/js-services/cache'; + +jest.mock('@/application/services/js-services/wasm/client_api', () => { + return { + initAPIService: jest.fn(), + }; +}); +jest.mock('nanoid', () => { + return { + nanoid: jest.fn().mockReturnValue('12345678'), + }; +}); +jest.mock('@/application/services/js-services/fetch', () => { + return { + fetchPublishView: jest.fn(), + fetchPublishViewMeta: jest.fn(), + fetchViewInfo: jest.fn(), + }; +}); + +jest.mock('@/application/services/js-services/cache', () => { + return { + getPublishView: jest.fn(), + getPublishViewMeta: jest.fn(), + getBatchCollabs: jest.fn(), + }; +}); +describe('AFClientService', () => { + let service: AFClientService; + beforeEach(() => { + jest.clearAllMocks(); + service = new AFClientService({ + cloudConfig: { + baseURL: 'http://localhost:3000', + gotrueURL: 'http://localhost:3000', + wsURL: 'ws://localhost:3000', + }, + }); + }); + + it('should get view meta', async () => { + const namespace = 'namespace'; + const publishName = 'publishName'; + const mockResponse = { + view_id: 'view_id', + publish_name: publishName, + metadata: { + view: { + name: 'viewName', + view_id: 'view_id', + }, + child_views: [], + ancestor_views: [], + }, + }; + + // @ts-ignore + (getPublishViewMeta as jest.Mock).mockResolvedValue(mockResponse); + + const result = await service.getPublishViewMeta(namespace, publishName); + + expect(result).toEqual(mockResponse); + }); + + it('should get view', async () => { + const namespace = 'namespace'; + const publishName = 'publishName'; + const mockResponse = { + data: [1, 2, 3], + meta: { + metadata: { + view: { + name: 'viewName', + view_id: 'view_id', + }, + child_views: [], + ancestor_views: [], + }, + }, + }; + + // @ts-ignore + (getPublishView as jest.Mock).mockResolvedValue(mockResponse); + + const result = await service.getPublishView(namespace, publishName); + + expect(result).toEqual(mockResponse); + }); + + it('should get view info', async () => { + const viewId = 'viewId'; + const mockResponse = { + namespace: 'namespace', + publish_name: 'publishName', + }; + + // @ts-ignore + (fetchViewInfo as jest.Mock).mockResolvedValue(mockResponse); + + const result = await service.getPublishInfo(viewId); + + expect(result).toEqual({ + namespace: 'namespace', + publishName: 'publishName', + }); + }); + + it('getPublishDatabaseViewRows', async () => { + const namespace = 'namespace'; + const publishName = 'publishName'; + const rowIds = ['1', '2', '3']; + const mockResponse = [withTestingYDoc('1'), withTestingYDoc('2'), withTestingYDoc('3')]; + + // @ts-ignore + (getBatchCollabs as jest.Mock).mockResolvedValue(mockResponse); + + const result = await service.getPublishDatabaseViewRows(namespace, publishName, rowIds); + + expect(result).toEqual({ + rows: expect.any(Object), + destroy: expect.any(Function), + }); + }); +}); diff --git a/frontend/appflowy_web_app/src/application/services/js-services/auth.service.ts b/frontend/appflowy_web_app/src/application/services/js-services/auth.service.ts deleted file mode 100644 index 7f80c9f871..0000000000 --- a/frontend/appflowy_web_app/src/application/services/js-services/auth.service.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { AuthService } from '@/application/services/services.type'; -import { ProviderType, SignUpWithEmailPasswordParams } from '@/application/user.type'; -import { APIService } from 'src/application/services/js-services/wasm'; -import { signInSuccess } from '@/application/services/js-services/session/auth'; -import { invalidToken } from 'src/application/services/js-services/session'; -import { afterSignInDecorator } from '@/application/services/js-services/decorator'; - -export class JSAuthService implements AuthService { - constructor() { - // Do nothing - } - - getOAuthURL = async (_provider: ProviderType): Promise => { - return Promise.reject('Not implemented'); - }; - - @afterSignInDecorator(signInSuccess) - async signInWithOAuth(_: { uri: string }): Promise { - return Promise.reject('Not implemented'); - } - - signupWithEmailPassword = async (_params: SignUpWithEmailPasswordParams): Promise => { - return Promise.reject('Not implemented'); - }; - - @afterSignInDecorator(signInSuccess) - async signinWithEmailPassword(email: string, password: string): Promise { - try { - return APIService.signIn(email, password); - } catch (e) { - return Promise.reject(e); - } - } - - signOut = async (): Promise => { - invalidToken(); - return APIService.logout(); - }; -} diff --git a/frontend/appflowy_web_app/src/application/services/js-services/cache/__tests__/cache.test.ts b/frontend/appflowy_web_app/src/application/services/js-services/cache/__tests__/cache.test.ts new file mode 100644 index 0000000000..fae533182f --- /dev/null +++ b/frontend/appflowy_web_app/src/application/services/js-services/cache/__tests__/cache.test.ts @@ -0,0 +1,145 @@ +import { CollabType } from '@/application/collab.type'; +import { withTestingYDoc } from '@/application/slate-yjs/__tests__/withTestingYjsEditor'; +import { expect } from '@jest/globals'; +import { + collabTypeToDBType, + getPublishView, + getPublishViewMeta, + getBatchCollabs, +} from '@/application/services/js-services/cache'; +import { openCollabDB, db } from '@/application/db'; +import { StrategyType } from '@/application/services/js-services/cache/types'; + +jest.mock('@/application/ydoc/apply', () => ({ + applyYDoc: jest.fn(), +})); + +jest.mock('@/application/db', () => ({ + openCollabDB: jest.fn(), + db: { + view_metas: { + get: jest.fn(), + put: jest.fn(), + }, + }, +})); + +const normalDoc = withTestingYDoc('1'); +const mockFetcher = jest.fn(); + +async function runTestWithStrategy(strategy: StrategyType) { + return getPublishView( + mockFetcher, + { + namespace: 'appflowy', + publishName: 'test', + }, + strategy + ); +} + +async function runGetPublishViewMetaWithStrategy(strategy: StrategyType) { + return getPublishViewMeta( + mockFetcher, + { + namespace: 'appflowy', + publishName: 'test', + }, + strategy + ); +} + +describe('Cache functions', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockFetcher.mockClear(); + (openCollabDB as jest.Mock).mockClear(); + }); + + describe('getPublishView', () => { + it('should call fetcher when no cache found', async () => { + mockFetcher.mockResolvedValue({ data: [1, 2, 3], meta: { metadata: { view: { id: '1' } } } }); + (db.view_metas.get as jest.Mock).mockResolvedValue(undefined); + await runTestWithStrategy(StrategyType.CACHE_FIRST); + expect(mockFetcher).toBeCalledTimes(1); + + await runTestWithStrategy(StrategyType.CACHE_AND_NETWORK); + expect(mockFetcher).toBeCalledTimes(2); + await expect(runTestWithStrategy(StrategyType.CACHE_ONLY)).rejects.toThrow('No cache found'); + }); + it('should call fetcher when cache is invalid or strategy is CACHE_AND_NETWORK', async () => { + (openCollabDB as jest.Mock).mockResolvedValue(normalDoc); + (db.view_metas.get as jest.Mock).mockResolvedValue({ view_id: '1' }); + mockFetcher.mockResolvedValue({ data: [1, 2, 3], meta: { metadata: { view: { id: '1' } } } }); + await runTestWithStrategy(StrategyType.CACHE_ONLY); + expect(openCollabDB).toBeCalledTimes(1); + + await runTestWithStrategy(StrategyType.CACHE_FIRST); + expect(openCollabDB).toBeCalledTimes(2); + expect(mockFetcher).toBeCalledTimes(0); + + await runTestWithStrategy(StrategyType.CACHE_AND_NETWORK); + expect(openCollabDB).toBeCalledTimes(3); + expect(mockFetcher).toBeCalledTimes(1); + }); + }); + + describe('getPublishViewMeta', () => { + it('should call fetcher when no cache found', async () => { + mockFetcher.mockResolvedValue({ metadata: { view: { id: '1' }, child_views: [], ancestor_views: [] } }); + (db.view_metas.get as jest.Mock).mockResolvedValue(undefined); + await runGetPublishViewMetaWithStrategy(StrategyType.CACHE_FIRST); + expect(mockFetcher).toBeCalledTimes(1); + + await runGetPublishViewMetaWithStrategy(StrategyType.CACHE_AND_NETWORK); + expect(mockFetcher).toBeCalledTimes(2); + + await expect(runGetPublishViewMetaWithStrategy(StrategyType.CACHE_ONLY)).rejects.toThrow('No cache found'); + }); + + it('should call fetcher when cache is invalid or strategy is CACHE_AND_NETWORK', async () => { + (openCollabDB as jest.Mock).mockResolvedValue(normalDoc); + (db.view_metas.get as jest.Mock).mockResolvedValue({ view_id: '1' }); + + mockFetcher.mockResolvedValue({ metadata: { view: { id: '1' }, child_views: [], ancestor_views: [] } }); + const meta = await runGetPublishViewMetaWithStrategy(StrategyType.CACHE_ONLY); + expect(openCollabDB).toBeCalledTimes(0); + expect(meta).toBeDefined(); + + await runGetPublishViewMetaWithStrategy(StrategyType.CACHE_FIRST); + expect(openCollabDB).toBeCalledTimes(0); + expect(mockFetcher).toBeCalledTimes(0); + + await runGetPublishViewMetaWithStrategy(StrategyType.CACHE_AND_NETWORK); + expect(openCollabDB).toBeCalledTimes(0); + expect(mockFetcher).toBeCalledTimes(1); + }); + }); + + describe('getBatchCollabs', () => { + it('should return empty array when no cache found', async () => { + (openCollabDB as jest.Mock).mockResolvedValue(undefined); + const collabs = await getBatchCollabs(['1', '2', '3']); + expect(collabs).toEqual([]); + }); + + it('should return collabs when cache found', async () => { + (openCollabDB as jest.Mock).mockResolvedValue(normalDoc); + (db.view_metas.get as jest.Mock).mockResolvedValue({ view_id: '1' }); + const collabs = await getBatchCollabs(['1', '2', '3']); + expect(collabs).toEqual([normalDoc, normalDoc, normalDoc]); + }); + }); +}); + +describe('collabTypeToDBType', () => { + it('should return correct DB type', () => { + expect(collabTypeToDBType(CollabType.Document)).toBe('document'); + expect(collabTypeToDBType(CollabType.Folder)).toBe('folder'); + expect(collabTypeToDBType(CollabType.Database)).toBe('database'); + expect(collabTypeToDBType(CollabType.WorkspaceDatabase)).toBe('databases'); + expect(collabTypeToDBType(CollabType.DatabaseRow)).toBe('database_row'); + expect(collabTypeToDBType(CollabType.UserAwareness)).toBe('user_awareness'); + expect(collabTypeToDBType(CollabType.Empty)).toBe(''); + }); +}); diff --git a/frontend/appflowy_web_app/src/application/services/js-services/cache/index.ts b/frontend/appflowy_web_app/src/application/services/js-services/cache/index.ts index 1f7fe670f1..a4db114630 100644 --- a/frontend/appflowy_web_app/src/application/services/js-services/cache/index.ts +++ b/frontend/appflowy_web_app/src/application/services/js-services/cache/index.ts @@ -1,7 +1,14 @@ -import { CollabType, YDoc, YjsEditorKey, YSharedRoot } from '@/application/collab.type'; +import { + CollabType, + PublishViewInfo, + PublishViewMetaData, + YDoc, + YjsEditorKey, + YSharedRoot, +} from '@/application/collab.type'; import { applyYDoc } from '@/application/ydoc/apply'; -import { getCollabDBName, openCollabDB } from './db'; -import { Fetcher, StrategyType } from './types'; +import { closeCollabDB, db, openCollabDB } from '@/application/db'; +import { Fetcher, StrategyType } from '@/application/services/js-services/cache/types'; export function collabTypeToDBType(type: CollabType) { switch (type) { @@ -32,30 +39,40 @@ const collabSharedRootKeyMap = { [CollabType.Empty]: YjsEditorKey.empty, }; -export function hasCache(doc: YDoc, type: CollabType) { +export function hasCollabCache(doc: YDoc) { const data = doc.getMap(YjsEditorKey.data_section) as YSharedRoot; - return data.has(collabSharedRootKeyMap[type] as string); + return Object.values(collabSharedRootKeyMap).some((key) => { + return data.has(key); + }); } -export async function getCollab( - fetcher: Fetcher<{ - state: Uint8Array; - }>, +export async function hasViewMetaCache(name: string) { + const data = await db.view_metas.get(name); + + return !!data; +} + +export async function getPublishViewMeta< + T extends { + view: PublishViewInfo; + child_views: PublishViewInfo[]; + ancestor_views: PublishViewInfo[]; + } +>( + fetcher: Fetcher, { - collabId, - collabType, - uuid, + namespace, + publishName, }: { - uuid?: string; - collabId: string; - collabType: CollabType; + namespace: string; + publishName: string; }, strategy: StrategyType = StrategyType.CACHE_AND_NETWORK ) { - const name = getCollabDBName(collabId, collabTypeToDBType(collabType), uuid); - const collab = await openCollabDB(name); - const exist = hasCache(collab, collabType); + const name = `${namespace}_${publishName}`; + const exist = await hasViewMetaCache(name); + const meta = await db.view_metas.get(name); switch (strategy) { case StrategyType.CACHE_ONLY: { @@ -63,103 +80,159 @@ export async function getCollab( throw new Error('No cache found'); } - return collab; + return meta; } case StrategyType.CACHE_FIRST: { if (!exist) { - await revalidateCollab(fetcher, collab); + return revalidatePublishViewMeta(name, fetcher); } - return collab; + return meta; } case StrategyType.CACHE_AND_NETWORK: { if (!exist) { - await revalidateCollab(fetcher, collab); + return revalidatePublishViewMeta(name, fetcher); } else { - void revalidateCollab(fetcher, collab); + void revalidatePublishViewMeta(name, fetcher); } - return collab; + return meta; } default: { - await revalidateCollab(fetcher, collab); - - return collab; + return revalidatePublishViewMeta(name, fetcher); } } } -async function revalidateCollab( - fetcher: Fetcher<{ - state: Uint8Array; - }>, - collab: YDoc +export async function getPublishView< + T extends { + data: number[]; + meta: { + view: PublishViewInfo; + child_views: PublishViewInfo[]; + ancestor_views: PublishViewInfo[]; + }; + } +>( + fetcher: Fetcher, + { + namespace, + publishName, + }: { + namespace: string; + publishName: string; + }, + strategy: StrategyType = StrategyType.CACHE_AND_NETWORK ) { - const { state } = await fetcher(); + const name = `${namespace}_${publishName}`; + const doc = await openCollabDB(name); + const exist = (await hasViewMetaCache(name)) && hasCollabCache(doc); + + switch (strategy) { + case StrategyType.CACHE_ONLY: { + if (!exist) { + throw new Error('No cache found'); + } + + break; + } + + case StrategyType.CACHE_FIRST: { + if (!exist) { + await revalidatePublishView(name, fetcher, doc); + } + + break; + } + + case StrategyType.CACHE_AND_NETWORK: { + if (!exist) { + await revalidatePublishView(name, fetcher, doc); + } else { + void revalidatePublishView(name, fetcher, doc); + } + + break; + } + + default: { + await revalidatePublishView(name, fetcher, doc); + break; + } + } + + return doc; +} + +export async function revalidatePublishViewMeta< + T extends { + view: PublishViewInfo; + child_views: PublishViewInfo[]; + ancestor_views: PublishViewInfo[]; + } +>(name: string, fetcher: Fetcher) { + const { view, child_views, ancestor_views } = await fetcher(); + + await db.view_metas.put( + { + publish_name: name, + ...view, + child_views: child_views, + ancestor_views: ancestor_views, + }, + name + ); + + return db.view_metas.get(name); +} + +export async function revalidatePublishView< + T extends { + data: number[]; + rows?: Record; + meta: PublishViewMetaData; + } +>(name: string, fetcher: Fetcher, collab: YDoc) { + const { data, meta, rows } = await fetcher(); + + await db.view_metas.put( + { + publish_name: name, + ...meta.view, + child_views: meta.child_views, + ancestor_views: meta.ancestor_views, + }, + name + ); + + if (rows) { + for (const [key, value] of Object.entries(rows)) { + const row = await openCollabDB(`${name}_${key}`); + + applyYDoc(row, new Uint8Array(value)); + } + } + + const state = new Uint8Array(data); applyYDoc(collab, state); } -export async function batchCollab( - batchFetcher: Fetcher>, - collabs: { - collabId: string; - collabType: CollabType; - uuid?: string; - }[], - strategy: StrategyType = StrategyType.CACHE_AND_NETWORK, - itemCallback?: (id: string, doc: YDoc) => void -) { - const collabMap = new Map(); +export async function getBatchCollabs(names: string[]) { + const collabs = await Promise.all(names.map((name) => openCollabDB(name))); - for (const { collabId, collabType, uuid } of collabs) { - const name = getCollabDBName(collabId, collabTypeToDBType(collabType), uuid); - const collab = await openCollabDB(name); - const exist = hasCache(collab, collabType); - - collabMap.set(collabId, collab); - if (exist) { - itemCallback?.(collabId, collab); - } - } - - const notCacheIds = collabs.filter(({ collabId, collabType }) => { - const id = collabMap.get(collabId); - - if (!id) return false; - - return !hasCache(id, collabType); - }); - - if (strategy === StrategyType.CACHE_ONLY) { - if (notCacheIds.length > 0) { - throw new Error('No cache found'); - } - - return; - } - - if (strategy === StrategyType.CACHE_FIRST && notCacheIds.length === 0) { - return; - } - - const states = await batchFetcher(); - - for (const [collabId, data] of Object.entries(states)) { - const info = collabs.find((item) => item.collabId === collabId); - const collab = collabMap.get(collabId); - - if (!info || !collab) { - continue; - } - - const state = new Uint8Array(data); - - applyYDoc(collab, state); - - itemCallback?.(collabId, collab); - } + return collabs; +} + +export async function deleteViewMeta(name: string) { + await db.view_metas.delete(name); +} + +export async function deleteView(name: string) { + console.log('deleteView', name); + await deleteViewMeta(name); + await closeCollabDB(name); } diff --git a/frontend/appflowy_web_app/src/application/services/js-services/database.service.ts b/frontend/appflowy_web_app/src/application/services/js-services/database.service.ts deleted file mode 100644 index cf29b221dd..0000000000 --- a/frontend/appflowy_web_app/src/application/services/js-services/database.service.ts +++ /dev/null @@ -1,157 +0,0 @@ -import { CollabType, YDatabase, YDoc, YjsDatabaseKey, YjsEditorKey } from '@/application/collab.type'; -import { batchCollab, getCollab } from '@/application/services/js-services/cache'; -import { StrategyType } from '@/application/services/js-services/cache/types'; -import { batchFetchCollab, fetchCollab } from '@/application/services/js-services/fetch'; -import { getCurrentWorkspace } from 'src/application/services/js-services/session'; -import { DatabaseService } from '@/application/services/services.type'; -import * as Y from 'yjs'; - -export class JSDatabaseService implements DatabaseService { - private loadedDatabaseId: Set = new Set(); - - private loadedWorkspaceId: Set = new Set(); - - private cacheDatabaseRowDocMap: Map = new Map(); - - constructor() { - // - } - - currentWorkspace() { - return getCurrentWorkspace(); - } - - async getWorkspaceDatabases(): Promise<{ views: string[]; database_id: string }[]> { - const workspace = await this.currentWorkspace(); - - if (!workspace) { - throw new Error('Workspace database not found'); - } - - const isLoaded = this.loadedWorkspaceId.has(workspace.id); - - const workspaceDatabase = await getCollab( - () => { - return fetchCollab(workspace.id, workspace.workspaceDatabaseId, CollabType.WorkspaceDatabase); - }, - { - collabId: workspace.workspaceDatabaseId, - collabType: CollabType.WorkspaceDatabase, - }, - isLoaded ? StrategyType.CACHE_FIRST : StrategyType.CACHE_AND_NETWORK - ); - - if (!isLoaded) { - this.loadedWorkspaceId.add(workspace.id); - } - - return workspaceDatabase.getMap(YjsEditorKey.data_section).get(YjsEditorKey.workspace_database).toJSON() as { - views: string[]; - database_id: string; - }[]; - } - - async openDatabase(databaseId: string): Promise<{ - databaseDoc: YDoc; - rows: Y.Map; - }> { - const workspace = await this.currentWorkspace(); - - if (!workspace) { - throw new Error('Workspace database not found'); - } - - const workspaceId = workspace.id; - const isLoaded = this.loadedDatabaseId.has(databaseId); - - const rootRowsDoc = - this.cacheDatabaseRowDocMap.get(databaseId) ?? - new Y.Doc({ - guid: databaseId, - }); - - if (!this.cacheDatabaseRowDocMap.has(databaseId)) { - this.cacheDatabaseRowDocMap.set(databaseId, rootRowsDoc); - } - - const rowsFolder: Y.Map = rootRowsDoc.getMap(); - - const databaseDoc = await getCollab( - () => { - return fetchCollab(workspaceId, databaseId, CollabType.Database); - }, - { - collabId: databaseId, - collabType: CollabType.Database, - }, - isLoaded ? StrategyType.CACHE_FIRST : StrategyType.CACHE_AND_NETWORK - ); - - if (!isLoaded) this.loadedDatabaseId.add(databaseId); - - const database = databaseDoc.getMap(YjsEditorKey.data_section)?.get(YjsEditorKey.database) as YDatabase; - const viewId = database.get(YjsDatabaseKey.metas)?.get(YjsDatabaseKey.iid)?.toString(); - const rowOrders = database.get(YjsDatabaseKey.views)?.get(viewId)?.get(YjsDatabaseKey.row_orders); - const rowOrdersIds = rowOrders.toJSON() as { - id: string; - }[]; - - if (!rowOrdersIds) { - throw new Error('Database rows not found'); - } - - const rowsParams = rowOrdersIds.map((item) => ({ - collabId: item.id, - collabType: CollabType.DatabaseRow, - })); - - void batchCollab( - () => { - return batchFetchCollab(workspaceId, rowsParams); - }, - rowsParams, - isLoaded ? StrategyType.CACHE_FIRST : StrategyType.CACHE_AND_NETWORK, - (id: string, doc: YDoc) => { - if (!rowsFolder.has(id)) { - rowsFolder.set(id, doc); - } - } - ); - - // Update rows if there are new rows added after the database has been loaded - rowOrders?.observe((event) => { - if (event.changes.added.size > 0) { - const rowIds = rowOrders.toJSON() as { - id: string; - }[]; - - const params = rowIds.map((item) => ({ - collabId: item.id, - collabType: CollabType.DatabaseRow, - })); - - void batchCollab( - () => { - return batchFetchCollab(workspaceId, params); - }, - params, - StrategyType.CACHE_AND_NETWORK, - (id: string, doc: YDoc) => { - if (!rowsFolder.has(id)) { - rowsFolder.set(id, doc); - } - } - ); - } - }); - - return { - databaseDoc, - rows: rowsFolder, - }; - } - - async closeDatabase(databaseId: string) { - this.cacheDatabaseRowDocMap.delete(databaseId); - } -} diff --git a/frontend/appflowy_web_app/src/application/services/js-services/decorator.ts b/frontend/appflowy_web_app/src/application/services/js-services/decorator.ts deleted file mode 100644 index a6f9cf9ee4..0000000000 --- a/frontend/appflowy_web_app/src/application/services/js-services/decorator.ts +++ /dev/null @@ -1,60 +0,0 @@ -/** - * @description: - * * This is a decorator that can be used to read data from storage and fetch data from the server. - * * If the data is already in storage, it will return the data from storage and fetch the data from the server in the background. - * - * @param getStorage A function that returns the data from storage. eg. `() => Promise` - * - * @param setStorage A function that saves the data to storage. eg. `(data: T) => Promise` - * - * @param fetchFunction A function that fetches the data from the server. eg. `(params: P) => Promise` - * - * @returns: A function that returns the data from storage and fetches the data from the server in the background. - */ -export function asyncDataDecorator( - getStorage: () => Promise, - setStorage: (data: T) => Promise, - fetchFunction: (params: P) => Promise -) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) { - async function fetchData(params: P) { - const data = await fetchFunction(params); - - if (!data) return; - await setStorage(data); - return data; - } - - const originalMethod = descriptor.value; - - descriptor.value = async function (params: P) { - const data = await getStorage(); - - await originalMethod.apply(this, [params]); - if (data) { - void fetchData(params); - return data; - } else { - return fetchData(params); - } - }; - - return descriptor; - }; -} - -export function afterSignInDecorator(successCallback: () => Promise) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) { - const originalMethod = descriptor.value; - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - descriptor.value = async function (...args: any[]) { - await originalMethod.apply(this, args); - await successCallback(); - }; - - return descriptor; - }; -} diff --git a/frontend/appflowy_web_app/src/application/services/js-services/document.service.ts b/frontend/appflowy_web_app/src/application/services/js-services/document.service.ts deleted file mode 100644 index bcf73ae550..0000000000 --- a/frontend/appflowy_web_app/src/application/services/js-services/document.service.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { CollabOrigin, CollabType, YDoc } from '@/application/collab.type'; -import { getCollab } from '@/application/services/js-services/cache'; -import { StrategyType } from '@/application/services/js-services/cache/types'; -import { fetchCollab } from '@/application/services/js-services/fetch'; -import { getCurrentWorkspace } from 'src/application/services/js-services/session'; -import { DocumentService } from '@/application/services/services.type'; - -export class JSDocumentService implements DocumentService { - private loaded: Set = new Set(); - - constructor() { - // - } - - async openDocument(docId: string): Promise { - const workspace = await getCurrentWorkspace(); - - if (!workspace) { - throw new Error('Workspace database not found'); - } - - const isLoaded = this.loaded.has(docId); - - const doc = await getCollab( - () => { - return fetchCollab(workspace.id, docId, CollabType.Document); - }, - { - collabId: docId, - collabType: CollabType.Document, - }, - isLoaded ? StrategyType.CACHE_FIRST : StrategyType.CACHE_AND_NETWORK - ); - - if (!isLoaded) this.loaded.add(docId); - const handleUpdate = (update: Uint8Array, origin: CollabOrigin) => { - if (origin === CollabOrigin.LocalSync) { - // Send the update to the server - console.log('update', update); - } - }; - - doc.on('update', handleUpdate); - - return doc; - } -} diff --git a/frontend/appflowy_web_app/src/application/services/js-services/fetch.ts b/frontend/appflowy_web_app/src/application/services/js-services/fetch.ts index 7ae4dc8902..3f9ca9873b 100644 --- a/frontend/appflowy_web_app/src/application/services/js-services/fetch.ts +++ b/frontend/appflowy_web_app/src/application/services/js-services/fetch.ts @@ -1,4 +1,3 @@ -import { CollabType } from '@/application/collab.type'; import { APIService } from '@/application/services/js-services/wasm'; const pendingRequests = new Map(); @@ -31,36 +30,20 @@ function fetchWithDeduplication(url: string, params: Req, fetchFunctio return fetchPromise; } -/** - * Fetch collab - * @param workspaceId - * @param id - * @param type [CollabType] - */ -export function fetchCollab(workspaceId: string, id: string, type: CollabType) { - const fetchFunction = () => APIService.getCollab(workspaceId, id, type); +export function fetchPublishView(namespace: string, publishName: string) { + const fetchFunction = () => APIService.getPublishView(namespace, publishName); - return fetchWithDeduplication(`fetchCollab_${workspaceId}`, { id, type }, fetchFunction); + return fetchWithDeduplication(`fetchPublishView_${namespace}`, { publishName }, fetchFunction); } -/** - * Batch fetch collab - * Usage: - * // load database rows - * const rows = await batchFetchCollab(workspaceId, databaseRows.map((row) => ({ collabId: row.id, collabType: CollabType.DatabaseRow }))); - * - * @param workspaceId - * @param params [{ collabId: string; collabType: CollabType }] - */ -export function batchFetchCollab(workspaceId: string, params: { collabId: string; collabType: CollabType }[]) { - const fetchFunction = () => - APIService.batchGetCollab( - workspaceId, - params.map(({ collabId, collabType }) => ({ - object_id: collabId, - collab_type: collabType, - })) - ); +export function fetchViewInfo(viewId: string) { + const fetchFunction = () => APIService.getPublishInfoWithViewId(viewId); - return fetchWithDeduplication(`batchFetchCollab_${workspaceId}`, params, fetchFunction); + return fetchWithDeduplication(`fetchViewInfo`, { viewId }, fetchFunction); +} + +export function fetchPublishViewMeta(namespace: string, publishName: string) { + const fetchFunction = () => APIService.getPublishViewMeta(namespace, publishName); + + return fetchWithDeduplication(`fetchPublishViewMeta_${namespace}`, { publishName }, fetchFunction); } diff --git a/frontend/appflowy_web_app/src/application/services/js-services/folder.service.ts b/frontend/appflowy_web_app/src/application/services/js-services/folder.service.ts deleted file mode 100644 index f145480c18..0000000000 --- a/frontend/appflowy_web_app/src/application/services/js-services/folder.service.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { CollabOrigin, CollabType, YDoc } from '@/application/collab.type'; -import { getCollab } from '@/application/services/js-services/cache'; -import { StrategyType } from '@/application/services/js-services/cache/types'; -import { fetchCollab } from '@/application/services/js-services/fetch'; -import { FolderService } from '@/application/services/services.type'; - -export class JSFolderService implements FolderService { - private loaded: Set = new Set(); - - constructor() { - // - } - - async openWorkspace(workspaceId: string): Promise { - const isLoaded = this.loaded.has(workspaceId); - const doc = await getCollab( - () => { - return fetchCollab(workspaceId, workspaceId, CollabType.Folder); - }, - { - collabId: workspaceId, - collabType: CollabType.Folder, - }, - isLoaded ? StrategyType.CACHE_FIRST : StrategyType.CACHE_AND_NETWORK - ); - - if (!isLoaded) this.loaded.add(workspaceId); - const handleUpdate = (update: Uint8Array, origin: CollabOrigin) => { - if (origin === CollabOrigin.LocalSync) { - // Send the update to the server - console.log('update', update); - } - }; - - doc.on('update', handleUpdate); - - return doc; - } -} diff --git a/frontend/appflowy_web_app/src/application/services/js-services/index.ts b/frontend/appflowy_web_app/src/application/services/js-services/index.ts index d31b7f117a..9e88466b83 100644 --- a/frontend/appflowy_web_app/src/application/services/js-services/index.ts +++ b/frontend/appflowy_web_app/src/application/services/js-services/index.ts @@ -1,42 +1,34 @@ -import { JSDatabaseService } from '@/application/services/js-services/database.service'; +import { YDoc } from '@/application/collab.type'; import { - AFService, - AFServiceConfig, - AuthService, - DatabaseService, - DocumentService, - FolderService, - UserService, -} from '@/application/services/services.type'; -import { JSUserService } from '@/application/services/js-services/user.service'; -import { JSAuthService } from '@/application/services/js-services/auth.service'; -import { JSFolderService } from '@/application/services/js-services/folder.service'; -import { JSDocumentService } from '@/application/services/js-services/document.service'; + deleteView, + getBatchCollabs, + getPublishView, + getPublishViewMeta, + hasViewMetaCache, +} from '@/application/services/js-services/cache'; +import { StrategyType } from '@/application/services/js-services/cache/types'; +import { fetchPublishView, fetchPublishViewMeta, fetchViewInfo } from '@/application/services/js-services/fetch'; +import { AFService, AFServiceConfig } from '@/application/services/services.type'; import { nanoid } from 'nanoid'; import { initAPIService } from '@/application/services/js-services/wasm/client_api'; +import * as Y from 'yjs'; export class AFClientService implements AFService { - authService: AuthService; - - userService: UserService; - - documentService: DocumentService; - - folderService: FolderService; - - databaseService: DatabaseService; - private deviceId: string = nanoid(8); private clientId: string = 'web'; - getDeviceID = (): string => { - return this.deviceId; - }; + private publishViewLoaded: Set = new Set(); - getClientID = (): string => { - return this.clientId; - }; + private publishViewInfo: Map< + string, + { + namespace: string; + publishName: string; + } + > = new Map(); + + private cacheDatabaseRowDocMap: Map = new Map(); constructor(config: AFServiceConfig) { initAPIService({ @@ -44,11 +36,117 @@ export class AFClientService implements AFService { deviceId: this.deviceId, clientId: this.clientId, }); + } - this.authService = new JSAuthService(); - this.userService = new JSUserService(); - this.documentService = new JSDocumentService(); - this.folderService = new JSFolderService(); - this.databaseService = new JSDatabaseService(); + async getPublishViewMeta(namespace: string, publishName: string) { + const viewMeta = await getPublishViewMeta( + () => { + return fetchPublishViewMeta(namespace, publishName); + }, + { + namespace, + publishName, + }, + StrategyType.CACHE_AND_NETWORK + ); + + if (!viewMeta) { + return Promise.reject(new Error('View has not been published yet')); + } + + return viewMeta; + } + + async getPublishView(namespace: string, publishName: string) { + const name = `${namespace}_${publishName}`; + + const isLoaded = this.publishViewLoaded.has(name); + + const doc = await getPublishView( + async () => { + try { + return await fetchPublishView(namespace, publishName); + } catch (e) { + void (async () => { + if (await hasViewMetaCache(name)) { + this.publishViewLoaded.delete(name); + void deleteView(name); + } + })(); + + return Promise.reject(e); + } + }, + { + namespace, + publishName, + }, + isLoaded ? StrategyType.CACHE_FIRST : StrategyType.CACHE_AND_NETWORK + ); + + if (!isLoaded) { + this.publishViewLoaded.add(name); + } + + return doc; + } + + async getPublishDatabaseViewRows(namespace: string, publishName: string, rowIds: string[]) { + const name = `${namespace}_${publishName}`; + + if (!this.publishViewLoaded.has(name)) { + await this.getPublishView(namespace, publishName); + } + + const rootRowsDoc = + this.cacheDatabaseRowDocMap.get(name) ?? + new Y.Doc({ + guid: name, + }); + + if (!this.cacheDatabaseRowDocMap.has(name)) { + this.cacheDatabaseRowDocMap.set(name, rootRowsDoc); + } + + const rowsFolder: Y.Map = rootRowsDoc.getMap(); + const docs = await getBatchCollabs(rowIds); + + docs.forEach((doc, index) => { + rowsFolder.set(rowIds[index], doc); + }); + + return { + rows: rowsFolder, + destroy: () => { + this.cacheDatabaseRowDocMap.delete(name); + rootRowsDoc.destroy(); + }, + }; + } + + async getPublishInfo(viewId: string) { + if (this.publishViewInfo.has(viewId)) { + return this.publishViewInfo.get(viewId) as { + namespace: string; + publishName: string; + }; + } + + const info = await fetchViewInfo(viewId); + + const namespace = info.namespace; + + if (!namespace) { + return Promise.reject(new Error('View not found')); + } + + const data = { + namespace, + publishName: info.publish_name, + }; + + this.publishViewInfo.set(viewId, data); + + return data; } } diff --git a/frontend/appflowy_web_app/src/application/services/js-services/session/auth.ts b/frontend/appflowy_web_app/src/application/services/js-services/session/auth.ts deleted file mode 100644 index dd8d3d1d99..0000000000 --- a/frontend/appflowy_web_app/src/application/services/js-services/session/auth.ts +++ /dev/null @@ -1,3 +0,0 @@ -export async function signInSuccess() { - // Do nothing -} diff --git a/frontend/appflowy_web_app/src/application/services/js-services/session/index.ts b/frontend/appflowy_web_app/src/application/services/js-services/session/index.ts deleted file mode 100644 index c618a85cfd..0000000000 --- a/frontend/appflowy_web_app/src/application/services/js-services/session/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './token'; -export * from './user'; -export * from './auth'; diff --git a/frontend/appflowy_web_app/src/application/services/js-services/session/token.ts b/frontend/appflowy_web_app/src/application/services/js-services/session/token.ts deleted file mode 100644 index e22f980423..0000000000 --- a/frontend/appflowy_web_app/src/application/services/js-services/session/token.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { notify } from '@/components/_shared/notify'; - -const tokenKey = 'token'; - -export function readTokenStr() { - return sessionStorage.getItem(tokenKey); -} - -export function getAuthInfo() { - const token = readTokenStr() || ''; - - try { - const info = JSON.parse(token); - - return { - uuid: info.user.id, - access_token: info.access_token, - email: info.user.email, - }; - } catch (e) { - return; - } -} - -export function writeToken(token: string) { - if (!token) { - invalidToken(); - return; - } - - sessionStorage.setItem(tokenKey, token); -} - -export function invalidToken() { - sessionStorage.removeItem(tokenKey); - notify.error('Invalid token, please login again'); -} diff --git a/frontend/appflowy_web_app/src/application/services/js-services/session/user.ts b/frontend/appflowy_web_app/src/application/services/js-services/session/user.ts deleted file mode 100644 index 6fbab3f390..0000000000 --- a/frontend/appflowy_web_app/src/application/services/js-services/session/user.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { UserProfile, UserWorkspace, Workspace } from '@/application/user.type'; - -const userKey = 'user'; -const workspaceKey = 'workspace'; - -export async function getSignInUser(): Promise { - const userStr = localStorage.getItem(userKey); - - try { - return userStr ? JSON.parse(userStr) : undefined; - } catch (e) { - return undefined; - } -} - -export async function setSignInUser(profile: UserProfile) { - const userStr = JSON.stringify(profile); - - localStorage.setItem(userKey, userStr); -} - -export async function getUserWorkspace(): Promise { - const str = localStorage.getItem(workspaceKey); - - try { - return str ? JSON.parse(str) : undefined; - } catch (e) { - return undefined; - } -} - -export async function setUserWorkspace(workspace: UserWorkspace) { - const str = JSON.stringify(workspace); - - localStorage.setItem(workspaceKey, str); -} - -export async function getCurrentWorkspace(): Promise { - const userProfile = await getSignInUser(); - const userWorkspace = await getUserWorkspace(); - - return userWorkspace?.workspaces.find((workspace) => workspace.id === userProfile?.workspaceId); -} diff --git a/frontend/appflowy_web_app/src/application/services/js-services/user.service.ts b/frontend/appflowy_web_app/src/application/services/js-services/user.service.ts deleted file mode 100644 index ce912bd50f..0000000000 --- a/frontend/appflowy_web_app/src/application/services/js-services/user.service.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { UserService } from '@/application/services/services.type'; -import { UserProfile, UserWorkspace } from '@/application/user.type'; -import { APIService } from 'src/application/services/js-services/wasm'; -import { - getAuthInfo, - getSignInUser, - getUserWorkspace, - invalidToken, - setSignInUser, - setUserWorkspace, -} from 'src/application/services/js-services/session'; -import { asyncDataDecorator } from '@/application/services/js-services/decorator'; - -async function getUser() { - try { - const user = await APIService.getUser(); - - return user; - } catch (e) { - console.error(e); - invalidToken(); - } -} - -export class JSUserService implements UserService { - @asyncDataDecorator(getSignInUser, setSignInUser, getUser) - async getUserProfile(): Promise { - if (!getAuthInfo()) { - return Promise.reject('Not authenticated'); - } - - await this.getUserWorkspace(); - - return null!; - } - - async checkUser(): Promise { - return (await getSignInUser()) !== undefined; - } - - @asyncDataDecorator(getUserWorkspace, setUserWorkspace, APIService.getUserWorkspace) - async getUserWorkspace(): Promise { - return null!; - } -} diff --git a/frontend/appflowy_web_app/src/application/services/js-services/wasm/client_api.ts b/frontend/appflowy_web_app/src/application/services/js-services/wasm/client_api.ts index cd09cb74d1..9ee174b6c6 100644 --- a/frontend/appflowy_web_app/src/application/services/js-services/wasm/client_api.ts +++ b/frontend/appflowy_web_app/src/application/services/js-services/wasm/client_api.ts @@ -1,8 +1,6 @@ -import { CollabType } from '@/application/collab.type'; import { ClientAPI } from '@appflowyinc/client-api-wasm'; -import { UserProfile, UserWorkspace } from '@/application/user.type'; import { AFCloudConfig } from '@/application/services/services.type'; -import { invalidToken, readTokenStr, writeToken } from 'src/application/services/js-services/session'; +import { PublishViewMetaData } from '@/application/collab.type'; let client: ClientAPI; @@ -12,8 +10,18 @@ export function initAPIService( clientId: string; } ) { - window.refresh_token = writeToken; - window.invalid_token = invalidToken; + if (client) { + return; + } + + window.refresh_token = () => { + // + }; + + window.invalid_token = () => { + // invalidToken(); + }; + client = ClientAPI.new({ base_url: config.baseURL, ws_addr: config.wsURL, @@ -26,96 +34,25 @@ export function initAPIService( }, }); - const token = readTokenStr(); - - if (token) { - client.restore_token(token); - } - client.subscribe(); } -export function signIn(email: string, password: string) { - return client.login(email, password); -} - -export function logout() { - return client.logout(); -} - -export async function getUser(): Promise { - try { - const user = await client.get_user(); - - if (!user) { - throw new Error('No user found'); - } - - return { - uid: parseInt(user.uid), - uuid: user.uuid || undefined, - email: user.email || undefined, - name: user.name || undefined, - workspaceId: user.latest_workspace_id, - iconUrl: user.icon_url || undefined, - }; - } catch (e) { - return Promise.reject(e); - } -} - -export async function getCollab(workspaceId: string, object_id: string, collabType: CollabType) { - const res = await client.get_collab({ - workspace_id: workspaceId, - object_id: object_id, - collab_type: Number(collabType) as 0 | 1 | 2 | 3 | 4 | 5, - }); - - const state = new Uint8Array(res.doc_state); +export async function getPublishView(publishNamespace: string, publishName: string) { + const data = await client.get_publish_view(publishNamespace, publishName); return { - state, + data: data.data, + meta: JSON.parse(data.meta.data) as PublishViewMetaData, }; } -export async function batchGetCollab( - workspaceId: string, - params: { - object_id: string; - collab_type: CollabType; - }[] -) { - const res = (await client.batch_get_collab( - workspaceId, - params.map((param) => ({ - object_id: param.object_id, - collab_type: Number(param.collab_type) as 0 | 1 | 2 | 3 | 4 | 5, - })) - )) as unknown as Map; - - const result: Record = {}; - - res.forEach((value, key) => { - result[key] = value.doc_state; - }); - return result; +export async function getPublishInfoWithViewId(viewId: string) { + return client.get_publish_info(viewId); } -export async function getUserWorkspace(): Promise { - const res = await client.get_user_workspace(); +export async function getPublishViewMeta(publishNamespace: string, publishName: string) { + const data = await client.get_publish_view_meta(publishNamespace, publishName); + const metadata = JSON.parse(data.data) as PublishViewMetaData; - return { - visitingWorkspaceId: res.visiting_workspace_id, - workspaces: res.workspaces.map((workspace) => ({ - id: workspace.workspace_id, - name: workspace.workspace_name, - icon: workspace.icon, - owner: { - id: Number(workspace.owner_uid), - name: workspace.owner_name, - }, - type: workspace.workspace_type, - workspaceDatabaseId: workspace.database_storage_id, - })), - }; + return metadata; } diff --git a/frontend/appflowy_web_app/src/application/services/services.type.ts b/frontend/appflowy_web_app/src/application/services/services.type.ts index 1e837f1576..99076175a6 100644 --- a/frontend/appflowy_web_app/src/application/services/services.type.ts +++ b/frontend/appflowy_web_app/src/application/services/services.type.ts @@ -1,16 +1,8 @@ import { YDoc } from '@/application/collab.type'; -import { ProviderType, SignUpWithEmailPasswordParams, UserProfile } from '@/application/user.type'; +import { ViewMeta } from '@/application/db/tables/view_metas'; import * as Y from 'yjs'; -export interface AFService { - getDeviceID: () => string; - getClientID: () => string; - authService: AuthService; - userService: UserService; - documentService: DocumentService; - folderService: FolderService; - databaseService: DatabaseService; -} +export type AFService = PublishService; export interface AFServiceConfig { cloudConfig: AFCloudConfig; @@ -22,35 +14,16 @@ export interface AFCloudConfig { wsURL: string; } -export interface AuthService { - getOAuthURL: (provider: ProviderType) => Promise; - signInWithOAuth: (params: { uri: string }) => Promise; - signupWithEmailPassword: (params: SignUpWithEmailPasswordParams) => Promise; - signinWithEmailPassword: (email: string, password: string) => Promise; - signOut: () => Promise; -} - -export interface DocumentService { - openDocument: (docId: string) => Promise; -} - -export interface DatabaseService { - getWorkspaceDatabases: () => Promise<{ views: string[]; database_id: string }[]>; - openDatabase: ( - databaseId: string, - rowIds?: string[] +export interface PublishService { + getPublishViewMeta: (namespace: string, publishName: string) => Promise; + getPublishView: (namespace: string, publishName: string) => Promise; + getPublishInfo: (viewId: string) => Promise<{ namespace: string; publishName: string }>; + getPublishDatabaseViewRows: ( + namespace: string, + publishName: string, + rowIds: string[] ) => Promise<{ - databaseDoc: YDoc; rows: Y.Map; + destroy: () => void; }>; - closeDatabase: (databaseId: string) => Promise; -} - -export interface UserService { - getUserProfile: () => Promise; - checkUser: () => Promise; -} - -export interface FolderService { - openWorkspace: (workspaceId: string) => Promise; } diff --git a/frontend/appflowy_web_app/src/application/services/tauri-services/auth.service.ts b/frontend/appflowy_web_app/src/application/services/tauri-services/auth.service.ts deleted file mode 100644 index f039782058..0000000000 --- a/frontend/appflowy_web_app/src/application/services/tauri-services/auth.service.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { AFCloudConfig, AuthService } from '@/application/services/services.type'; -import { - AuthenticatorPB, - OauthProviderPB, - OauthSignInPB, - SignInPayloadPB, - SignUpPayloadPB, - UserEventGetOauthURLWithProvider, - UserEventOauthSignIn, - UserEventSignInWithEmailPassword, - UserEventSignOut, - UserEventSignUp, - UserProfilePB, -} from './backend/events/flowy-user'; -import { ProviderType, SignUpWithEmailPasswordParams, UserProfile } from '@/application/user.type'; - -export class TauriAuthService implements AuthService { - - constructor (private cloudConfig: AFCloudConfig, private clientConfig: { - deviceId: string; - clientId: string; - - }) {} - - getDeviceID = (): string => { - return this.clientConfig.deviceId; - }; - getOAuthURL = async (provider: ProviderType): Promise => { - const providerDataRes = await UserEventGetOauthURLWithProvider( - OauthProviderPB.fromObject({ - provider: provider as number, - }), - ); - - if (!providerDataRes.ok) { - throw new Error(providerDataRes.val.msg); - } - - const providerData = providerDataRes.val; - - return providerData.oauth_url; - }; - - signInWithOAuth = async ({ uri }: { uri: string }): Promise => { - const payload = OauthSignInPB.fromObject({ - authenticator: AuthenticatorPB.AppFlowyCloud, - map: { - sign_in_url: uri, - device_id: this.getDeviceID(), - }, - }); - - const res = await UserEventOauthSignIn(payload); - - if (!res.ok) { - throw new Error(res.val.msg); - } - - return; - }; - signinWithEmailPassword = async (email: string, password: string): Promise => { - const payload = SignInPayloadPB.fromObject({ - email, - password, - }); - - const res = await UserEventSignInWithEmailPassword(payload); - - if (!res.ok) { - return Promise.reject(res.val.msg); - } - - return; - }; - - signupWithEmailPassword = async (params: SignUpWithEmailPasswordParams): Promise => { - const payload = SignUpPayloadPB.fromObject({ - name: params.name, - email: params.email, - password: params.password, - device_id: this.getDeviceID(), - }); - - const res = await UserEventSignUp(payload); - - if (!res.ok) { - return Promise.reject(res.val.msg); - } - - return; - }; - - signOut = async () => { - const res = await UserEventSignOut(); - - if (!res.ok) { - return Promise.reject(res.val.msg); - } - - return; - }; -} - -export function parseUserProfileFrom (userPB: UserProfilePB): UserProfile { - const user = userPB.toObject(); - - return { - uid: user.id as number, - email: user.email, - name: user.name, - iconUrl: user.icon_url, - workspaceId: user.workspace_id, - }; -} diff --git a/frontend/appflowy_web_app/src/application/services/tauri-services/database.service.ts b/frontend/appflowy_web_app/src/application/services/tauri-services/database.service.ts deleted file mode 100644 index d7909679fb..0000000000 --- a/frontend/appflowy_web_app/src/application/services/tauri-services/database.service.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { YDoc } from '@/application/collab.type'; -import { DatabaseService } from '@/application/services/services.type'; -import * as Y from 'yjs'; - -export class TauriDatabaseService implements DatabaseService { - constructor() { - // - } - - async getWorkspaceDatabases(): Promise<{ views: string[]; database_id: string }[]> { - return Promise.reject('Not implemented'); - } - - async closeDatabase(_databaseId: string) { - return Promise.reject('Not implemented'); - } - - async openDatabase(_viewId: string): Promise<{ - databaseDoc: YDoc; - rows: Y.Map; - }> { - return Promise.reject('Not implemented'); - } -} diff --git a/frontend/appflowy_web_app/src/application/services/tauri-services/document.service.ts b/frontend/appflowy_web_app/src/application/services/tauri-services/document.service.ts deleted file mode 100644 index 9ae2987350..0000000000 --- a/frontend/appflowy_web_app/src/application/services/tauri-services/document.service.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { DocumentService } from '@/application/services/services.type'; -import * as Y from 'yjs'; - -export class TauriDocumentService implements DocumentService { - async openDocument(_id: string): Promise { - return Promise.reject('Not implemented'); - } -} diff --git a/frontend/appflowy_web_app/src/application/services/tauri-services/folder.service.ts b/frontend/appflowy_web_app/src/application/services/tauri-services/folder.service.ts deleted file mode 100644 index 868e6f1391..0000000000 --- a/frontend/appflowy_web_app/src/application/services/tauri-services/folder.service.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { YDoc } from '@/application/collab.type'; -import { FolderService } from '@/application/services/services.type'; - -export class TauriFolderService implements FolderService { - constructor() { - // - } - - async openWorkspace(_workspaceId: string): Promise { - return Promise.reject('Not implemented'); - } -} diff --git a/frontend/appflowy_web_app/src/application/services/tauri-services/index.ts b/frontend/appflowy_web_app/src/application/services/tauri-services/index.ts index 8908c002ee..8e81f4ed5f 100644 --- a/frontend/appflowy_web_app/src/application/services/tauri-services/index.ts +++ b/frontend/appflowy_web_app/src/application/services/tauri-services/index.ts @@ -1,50 +1,24 @@ -import { - AFService, - AFServiceConfig, - AuthService, - DatabaseService, - DocumentService, - FolderService, - UserService, -} from '@/application/services/services.type'; -import { TauriAuthService } from '@/application/services/tauri-services/auth.service'; -import { TauriDatabaseService } from '@/application/services/tauri-services/database.service'; -import { TauriFolderService } from '@/application/services/tauri-services/folder.service'; -import { TauriUserService } from '@/application/services/tauri-services/user.service'; -import { TauriDocumentService } from '@/application/services/tauri-services/document.service'; +import { AFService } from '@/application/services/services.type'; import { nanoid } from 'nanoid'; export class AFClientService implements AFService { - authService: AuthService; - - userService: UserService; - - documentService: DocumentService; - - folderService: FolderService; - - databaseService: DatabaseService; - private deviceId: string = nanoid(8); - private clientId: string = 'web'; + private clientId: string = 'tauri'; - getDeviceID = (): string => { - return this.deviceId; - }; + async getPublishView(_namespace: string, _publishName: string) { + return Promise.reject('Method not implemented'); + } - getClientID = (): string => { - return this.clientId; - }; + async getPublishInfo(_viewId: string) { + return Promise.reject('Method not implemented'); + } - constructor(config: AFServiceConfig) { - this.authService = new TauriAuthService(config.cloudConfig, { - deviceId: this.deviceId, - clientId: this.clientId, - }); - this.userService = new TauriUserService(); - this.documentService = new TauriDocumentService(); - this.folderService = new TauriFolderService(); - this.databaseService = new TauriDatabaseService(); + async getPublishViewMeta(_namespace: string, _publishName: string) { + return Promise.reject('Method not implemented'); + } + + async getPublishDatabaseViewRows(_namespace: string, _publishName: string, _rowIds: string[]) { + return Promise.reject('Method not implemented'); } } diff --git a/frontend/appflowy_web_app/src/application/services/tauri-services/user.service.ts b/frontend/appflowy_web_app/src/application/services/tauri-services/user.service.ts deleted file mode 100644 index 383e648052..0000000000 --- a/frontend/appflowy_web_app/src/application/services/tauri-services/user.service.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { UserService } from '@/application/services/services.type'; -import { UserProfile } from '@/application/user.type'; -import { UserEventGetUserProfile } from './backend/events/flowy-user'; -import { parseUserProfileFrom } from '@/application/services/tauri-services/auth.service'; - -export class TauriUserService implements UserService { - async getUserProfile(): Promise { - const res = await UserEventGetUserProfile(); - - if (res.ok) { - return parseUserProfileFrom(res.val); - } - - return null; - } - - async checkUser(): Promise { - return Promise.resolve(false); - } -} diff --git a/frontend/appflowy_web_app/src/application/slate-yjs/__tests__/convert.test.ts b/frontend/appflowy_web_app/src/application/slate-yjs/__tests__/convert.test.ts index 0e473517d8..161669a4ac 100644 --- a/frontend/appflowy_web_app/src/application/slate-yjs/__tests__/convert.test.ts +++ b/frontend/appflowy_web_app/src/application/slate-yjs/__tests__/convert.test.ts @@ -7,7 +7,7 @@ describe('convert yjs data to slate content', () => { it('should return undefined if root block is not exist', () => { const doc = new Y.Doc(); - expect(() => yDocToSlateContent(doc)).toThrowError(); + expect(yDocToSlateContent(doc)).toBeUndefined(); const doc2 = withTestingYDoc('1'); const { blocks, childrenMap, textMap, pageId } = getTestingDocData(doc2); diff --git a/frontend/appflowy_web_app/src/application/slate-yjs/plugins/withYjs.ts b/frontend/appflowy_web_app/src/application/slate-yjs/plugins/withYjs.ts index 837063fd1d..3b9f527087 100644 --- a/frontend/appflowy_web_app/src/application/slate-yjs/plugins/withYjs.ts +++ b/frontend/appflowy_web_app/src/application/slate-yjs/plugins/withYjs.ts @@ -75,7 +75,6 @@ export function withYjs( e.children = content.children; - console.log('initializeDocumentContent', doc.getMap(YjsEditorKey.data_section).toJSON(), e.children); Editor.normalize(editor, { force: true }); }; diff --git a/frontend/appflowy_web_app/src/application/slate-yjs/utils/convert.ts b/frontend/appflowy_web_app/src/application/slate-yjs/utils/convert.ts index 67defd6acc..954ad3518e 100644 --- a/frontend/appflowy_web_app/src/application/slate-yjs/utils/convert.ts +++ b/frontend/appflowy_web_app/src/application/slate-yjs/utils/convert.ts @@ -97,9 +97,11 @@ export function yDataToSlateContent({ export function yDocToSlateContent(doc: YDoc): Element | undefined { const sharedRoot = doc.getMap(YjsEditorKey.data_section) as YSharedRoot; + if (!sharedRoot || sharedRoot.size === 0) return; const document = sharedRoot.get(YjsEditorKey.document); const pageId = document.get(YjsEditorKey.page_id) as string; const blocks = document.get(YjsEditorKey.blocks) as YBlocks; + const meta = document.get(YjsEditorKey.meta) as YMeta; const childrenMap = meta.get(YjsEditorKey.children_map) as YChildrenMap; const textMap = meta.get(YjsEditorKey.text_map) as YTextMap; diff --git a/frontend/appflowy_web_app/src/application/user.type.ts b/frontend/appflowy_web_app/src/application/user.type.ts deleted file mode 100644 index e2c3bcdb43..0000000000 --- a/frontend/appflowy_web_app/src/application/user.type.ts +++ /dev/null @@ -1,75 +0,0 @@ -export enum Authenticator { - Local = 0, - Supabase = 1, - AppFlowyCloud = 2, -} - -export enum EncryptionType { - NoEncryption = 0, - Symmetric = 1, -} - -export interface UserProfile { - uid: number; - uuid?: string; - email?: string; - name?: string; - iconUrl?: string; - workspaceId?: string; -} - -export interface UserWorkspace { - visitingWorkspaceId: string; - workspaces: Workspace[]; -} - -export interface Workspace { - id: string; - name: string; - icon: string; - owner: { - id: number; - name: string; - }; - type: number; - workspaceDatabaseId: string; -} - -export interface SignUpWithEmailPasswordParams { - name: string; - email: string; - password: string; -} - -export enum ProviderType { - Apple = 0, - Azure = 1, - Bitbucket = 2, - Discord = 3, - Facebook = 4, - Figma = 5, - Github = 6, - Gitlab = 7, - Google = 8, - Keycloak = 9, - Kakao = 10, - Linkedin = 11, - Notion = 12, - Spotify = 13, - Slack = 14, - Workos = 15, - Twitch = 16, - Twitter = 17, - Email = 18, - Phone = 19, - Zoom = 20, -} - -export interface UserSetting { - workspaceId: string; - latestView?: { - id: string; - name: string; - }; - hasLatestView: boolean; -} diff --git a/frontend/appflowy_web_app/src/assets/appflowy.svg b/frontend/appflowy_web_app/src/assets/appflowy.svg new file mode 100644 index 0000000000..c282c112e1 --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/appflowy.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + diff --git a/frontend/appflowy_web_app/src/assets/arrow_right.svg b/frontend/appflowy_web_app/src/assets/arrow_right.svg new file mode 100644 index 0000000000..990748cab3 --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/arrow_right.svg @@ -0,0 +1,6 @@ + + + + + diff --git a/frontend/appflowy_web_app/src/assets/board.svg b/frontend/appflowy_web_app/src/assets/board.svg new file mode 100644 index 0000000000..0ae7bbfd20 --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/board.svg @@ -0,0 +1,6 @@ + + + + diff --git a/frontend/appflowy_web_app/src/assets/bulleted_list_icon_1.svg b/frontend/appflowy_web_app/src/assets/bulleted_list_icon_1.svg new file mode 100644 index 0000000000..de5a08230b --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/bulleted_list_icon_1.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/appflowy_web_app/src/assets/bulleted_list_icon_2.svg b/frontend/appflowy_web_app/src/assets/bulleted_list_icon_2.svg new file mode 100644 index 0000000000..abaa0a776d --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/bulleted_list_icon_2.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/appflowy_web_app/src/assets/bulleted_list_icon_3.svg b/frontend/appflowy_web_app/src/assets/bulleted_list_icon_3.svg new file mode 100644 index 0000000000..5fb47c442f --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/bulleted_list_icon_3.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/appflowy_web_app/src/assets/calendar.svg b/frontend/appflowy_web_app/src/assets/calendar.svg new file mode 100644 index 0000000000..dc9ad4b579 --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/calendar.svg @@ -0,0 +1,7 @@ + + + + + diff --git a/frontend/appflowy_web_app/src/assets/chat_ai.svg b/frontend/appflowy_web_app/src/assets/chat_ai.svg new file mode 100644 index 0000000000..a01aa41887 --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/chat_ai.svg @@ -0,0 +1,10 @@ + + + + + + diff --git a/frontend/appflowy_web_app/src/assets/chevron_down.svg b/frontend/appflowy_web_app/src/assets/chevron_down.svg new file mode 100644 index 0000000000..fbf3c9aabd --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/chevron_down.svg @@ -0,0 +1,7 @@ + + + + + diff --git a/frontend/appflowy_web_app/src/assets/copy.svg b/frontend/appflowy_web_app/src/assets/copy.svg new file mode 100644 index 0000000000..e31580d7f2 --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/copy.svg @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/frontend/appflowy_web_app/src/assets/document.svg b/frontend/appflowy_web_app/src/assets/document.svg new file mode 100644 index 0000000000..d418c09dd2 --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/document.svg @@ -0,0 +1,9 @@ + + + + + + + diff --git a/frontend/appflowy_web_app/src/assets/grid.svg b/frontend/appflowy_web_app/src/assets/grid.svg new file mode 100644 index 0000000000..19dfe4b6e1 --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/grid.svg @@ -0,0 +1,7 @@ + + + + + diff --git a/frontend/appflowy_web_app/src/assets/math.svg b/frontend/appflowy_web_app/src/assets/math.svg new file mode 100644 index 0000000000..1775b66e83 --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/math.svg @@ -0,0 +1,7 @@ + + + \ No newline at end of file diff --git a/frontend/appflowy_web_app/src/assets/moon.svg b/frontend/appflowy_web_app/src/assets/moon.svg new file mode 100644 index 0000000000..304a7ec978 --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/moon.svg @@ -0,0 +1,9 @@ + + + + + + + diff --git a/frontend/appflowy_web_app/src/assets/more.svg b/frontend/appflowy_web_app/src/assets/more.svg new file mode 100644 index 0000000000..b41b2fd04f --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/more.svg @@ -0,0 +1,8 @@ + + + + + diff --git a/frontend/appflowy_web_app/src/assets/report.svg b/frontend/appflowy_web_app/src/assets/report.svg new file mode 100644 index 0000000000..6a5bd10fa3 --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/report.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + diff --git a/frontend/appflowy_web_app/src/assets/search.svg b/frontend/appflowy_web_app/src/assets/search.svg new file mode 100644 index 0000000000..fdad759166 --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/search.svg @@ -0,0 +1,11 @@ + + + + + + + + diff --git a/frontend/appflowy_web_app/src/assets/settings/discord.png b/frontend/appflowy_web_app/src/assets/settings/discord.png deleted file mode 100644 index f71e68c6ed..0000000000 Binary files a/frontend/appflowy_web_app/src/assets/settings/discord.png and /dev/null differ diff --git a/frontend/appflowy_web_app/src/assets/settings/github.png b/frontend/appflowy_web_app/src/assets/settings/github.png deleted file mode 100644 index 597883b7a3..0000000000 Binary files a/frontend/appflowy_web_app/src/assets/settings/github.png and /dev/null differ diff --git a/frontend/appflowy_web_app/src/assets/settings/google.png b/frontend/appflowy_web_app/src/assets/settings/google.png deleted file mode 100644 index 60032628a8..0000000000 Binary files a/frontend/appflowy_web_app/src/assets/settings/google.png and /dev/null differ diff --git a/frontend/appflowy_web_app/src/assets/side_outlined.svg b/frontend/appflowy_web_app/src/assets/side_outlined.svg new file mode 100644 index 0000000000..6f1be3f72d --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/side_outlined.svg @@ -0,0 +1,14 @@ + + + + + + + + + + diff --git a/frontend/appflowy_web_app/src/assets/space_icon/space_icon_1.svg b/frontend/appflowy_web_app/src/assets/space_icon/space_icon_1.svg new file mode 100644 index 0000000000..bfb045fedc --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/space_icon/space_icon_1.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/frontend/appflowy_web_app/src/assets/space_icon/space_icon_10.svg b/frontend/appflowy_web_app/src/assets/space_icon/space_icon_10.svg new file mode 100644 index 0000000000..4b9d51a6b0 --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/space_icon/space_icon_10.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/appflowy_web_app/src/assets/space_icon/space_icon_11.svg b/frontend/appflowy_web_app/src/assets/space_icon/space_icon_11.svg new file mode 100644 index 0000000000..bb6ec9dea9 --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/space_icon/space_icon_11.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/appflowy_web_app/src/assets/space_icon/space_icon_12.svg b/frontend/appflowy_web_app/src/assets/space_icon/space_icon_12.svg new file mode 100644 index 0000000000..a10232d2f0 --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/space_icon/space_icon_12.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/frontend/appflowy_web_app/src/assets/space_icon/space_icon_13.svg b/frontend/appflowy_web_app/src/assets/space_icon/space_icon_13.svg new file mode 100644 index 0000000000..da0007d043 --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/space_icon/space_icon_13.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/appflowy_web_app/src/assets/space_icon/space_icon_14.svg b/frontend/appflowy_web_app/src/assets/space_icon/space_icon_14.svg new file mode 100644 index 0000000000..80e00912bd --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/space_icon/space_icon_14.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/appflowy_web_app/src/assets/space_icon/space_icon_15.svg b/frontend/appflowy_web_app/src/assets/space_icon/space_icon_15.svg new file mode 100644 index 0000000000..dcd06dc4b4 --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/space_icon/space_icon_15.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/appflowy_web_app/src/assets/space_icon/space_icon_2.svg b/frontend/appflowy_web_app/src/assets/space_icon/space_icon_2.svg new file mode 100644 index 0000000000..ecfd797076 --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/space_icon/space_icon_2.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/appflowy_web_app/src/assets/space_icon/space_icon_3.svg b/frontend/appflowy_web_app/src/assets/space_icon/space_icon_3.svg new file mode 100644 index 0000000000..cef3794152 --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/space_icon/space_icon_3.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/appflowy_web_app/src/assets/space_icon/space_icon_4.svg b/frontend/appflowy_web_app/src/assets/space_icon/space_icon_4.svg new file mode 100644 index 0000000000..244db0745e --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/space_icon/space_icon_4.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/appflowy_web_app/src/assets/space_icon/space_icon_5.svg b/frontend/appflowy_web_app/src/assets/space_icon/space_icon_5.svg new file mode 100644 index 0000000000..0ee1709993 --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/space_icon/space_icon_5.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/frontend/appflowy_web_app/src/assets/space_icon/space_icon_6.svg b/frontend/appflowy_web_app/src/assets/space_icon/space_icon_6.svg new file mode 100644 index 0000000000..66dafd1e7f --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/space_icon/space_icon_6.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/appflowy_web_app/src/assets/space_icon/space_icon_7.svg b/frontend/appflowy_web_app/src/assets/space_icon/space_icon_7.svg new file mode 100644 index 0000000000..4d7910296b --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/space_icon/space_icon_7.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/appflowy_web_app/src/assets/space_icon/space_icon_8.svg b/frontend/appflowy_web_app/src/assets/space_icon/space_icon_8.svg new file mode 100644 index 0000000000..275bb3ae07 --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/space_icon/space_icon_8.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/frontend/appflowy_web_app/src/assets/space_icon/space_icon_9.svg b/frontend/appflowy_web_app/src/assets/space_icon/space_icon_9.svg new file mode 100644 index 0000000000..c2cc63c35a --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/space_icon/space_icon_9.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/frontend/appflowy_web_app/src/assets/sun.svg b/frontend/appflowy_web_app/src/assets/sun.svg new file mode 100644 index 0000000000..fe4e2e8982 --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/sun.svg @@ -0,0 +1,14 @@ + + + + + + + + + + diff --git a/frontend/appflowy_web_app/src/components/_shared/context-provider/FolderProvider.tsx b/frontend/appflowy_web_app/src/components/_shared/context-provider/FolderProvider.tsx deleted file mode 100644 index 37bb03533b..0000000000 --- a/frontend/appflowy_web_app/src/components/_shared/context-provider/FolderProvider.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { YFolder } from '@/application/collab.type'; -import { Crumb, FolderContext } from '@/application/folder-yjs'; - -export const FolderProvider: React.FC<{ - folder: YFolder | null; - children?: React.ReactNode; - onNavigateToView?: (viewId: string) => void; - crumbs?: Crumb[]; - setCrumbs?: React.Dispatch>; -}> = ({ folder, children, onNavigateToView, crumbs, setCrumbs }) => { - return ( - - {children} - - ); -}; diff --git a/frontend/appflowy_web_app/src/components/_shared/context-provider/IdProvider.tsx b/frontend/appflowy_web_app/src/components/_shared/context-provider/IdProvider.tsx deleted file mode 100644 index 666554ff73..0000000000 --- a/frontend/appflowy_web_app/src/components/_shared/context-provider/IdProvider.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { useContext, createContext } from 'react'; - -export const IdContext = createContext(null); - -interface IdProviderProps { - objectId: string; -} - -export const IdProvider = ({ children, ...props }: IdProviderProps & { children: React.ReactNode }) => { - return {children}; -}; - -const defaultIdValue = {} as IdProviderProps; - -export function useId() { - return useContext(IdContext) || defaultIdValue; -} diff --git a/frontend/appflowy_web_app/src/components/_shared/not-found/RecordNotFound.tsx b/frontend/appflowy_web_app/src/components/_shared/not-found/RecordNotFound.tsx deleted file mode 100644 index a7ef1d2684..0000000000 --- a/frontend/appflowy_web_app/src/components/_shared/not-found/RecordNotFound.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { getCurrentWorkspace } from 'src/application/services/js-services/session'; -import { Button, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle } from '@mui/material'; -import React from 'react'; -import { useNavigate } from 'react-router-dom'; - -export function RecordNotFound({ open, title }: { open: boolean; title?: string }) { - const navigate = useNavigate(); - - return ( - - Oops.. something went wrong - - - {title ? title : 'The record you are looking for does not exist.'} - - - - - - - ); -} - -export default RecordNotFound; diff --git a/frontend/appflowy_web_app/src/components/_shared/not-found/index.ts b/frontend/appflowy_web_app/src/components/_shared/not-found/index.ts deleted file mode 100644 index e4f431167c..0000000000 --- a/frontend/appflowy_web_app/src/components/_shared/not-found/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './RecordNotFound'; diff --git a/frontend/appflowy_web_app/src/components/_shared/notify/index.ts b/frontend/appflowy_web_app/src/components/_shared/notify/index.ts index 1086cabdfd..9b6e888f2e 100644 --- a/frontend/appflowy_web_app/src/components/_shared/notify/index.ts +++ b/frontend/appflowy_web_app/src/components/_shared/notify/index.ts @@ -1,27 +1,20 @@ -import toast from 'react-hot-toast'; - -const commonOptions = { - style: { - background: 'var(--bg-base)', - color: 'var(--text-title)', - shadows: 'var(--shadow)', - }, -}; - export const notify = { success: (message: string) => { - toast.success(message, commonOptions); + window.toast.success(message); }, error: (message: string) => { - toast.error(message, commonOptions); + window.toast.error(message); }, - loading: (message: string) => { - toast.loading(message, commonOptions); + default: (message: string) => { + window.toast.default(message); + }, + warning: (message: string) => { + window.toast.warning(message); }, info: (message: string) => { - toast(message, commonOptions); + window.toast.info(message); }, clear: () => { - toast.dismiss(); + window.toast.clear(); }, }; diff --git a/frontend/appflowy_web_app/src/components/_shared/page/Page.tsx b/frontend/appflowy_web_app/src/components/_shared/page/Page.tsx deleted file mode 100644 index 090c15d3b2..0000000000 --- a/frontend/appflowy_web_app/src/components/_shared/page/Page.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { YView } from '@/application/collab.type'; -import { usePageInfo } from '@/components/_shared/page/usePageInfo'; -import React from 'react'; - -export function Page({ - id, - onClick, - ...props -}: { - id: string; - onClick?: (view: YView) => void; - style?: React.CSSProperties; -}) { - const { view, icon, name } = usePageInfo(id); - - return ( -
{ - onClick && view && onClick(view); - }} - className={'flex items-center justify-center gap-2 overflow-hidden'} - {...props} - > -
{icon}
-
{name}
-
- ); -} - -export default Page; diff --git a/frontend/appflowy_web_app/src/components/_shared/page/index.ts b/frontend/appflowy_web_app/src/components/_shared/page/index.ts deleted file mode 100644 index d9925d7520..0000000000 --- a/frontend/appflowy_web_app/src/components/_shared/page/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './Page'; diff --git a/frontend/appflowy_web_app/src/components/_shared/page/usePageInfo.tsx b/frontend/appflowy_web_app/src/components/_shared/page/usePageInfo.tsx deleted file mode 100644 index 2418d669b0..0000000000 --- a/frontend/appflowy_web_app/src/components/_shared/page/usePageInfo.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import { FontLayout, LineHeightLayout, ViewLayout, YjsFolderKey, YView } from '@/application/collab.type'; -import { useViewSelector } from '@/application/folder-yjs'; -import { CoverType } from '@/application/folder-yjs/folder.type'; -import React, { useEffect, useMemo, useState } from 'react'; -import { ReactComponent as DocumentSvg } from '$icons/16x/document.svg'; -import { ReactComponent as GridSvg } from '$icons/16x/grid.svg'; -import { ReactComponent as BoardSvg } from '$icons/16x/board.svg'; -import { ReactComponent as CalendarSvg } from '$icons/16x/date.svg'; -import { useTranslation } from 'react-i18next'; - -export interface PageCover { - type: CoverType; - value: string; -} - -export interface PageExtra { - cover: PageCover | null; - fontLayout: FontLayout; - lineHeightLayout: LineHeightLayout; - font?: string; -} - -function parseExtra(extra: string): PageExtra { - let extraObj; - - try { - extraObj = JSON.parse(extra); - } catch (e) { - extraObj = {}; - } - - return { - cover: extraObj.cover - ? { - type: extraObj.cover.type, - value: extraObj.cover.value, - } - : null, - fontLayout: extraObj.font_layout || FontLayout.normal, - lineHeightLayout: extraObj.line_height_layout || LineHeightLayout.normal, - font: extraObj.font, - }; -} - -export function usePageInfo(id: string) { - const { view } = useViewSelector(id); - - const [loading, setLoading] = useState(true); - const layout = view?.get(YjsFolderKey.layout); - const icon = view?.get(YjsFolderKey.icon); - const extra = view?.get(YjsFolderKey.extra); - const name = view?.get(YjsFolderKey.name) || ''; - const iconObj = useMemo(() => { - try { - return JSON.parse(icon || ''); - } catch (e) { - return null; - } - }, [icon]); - - const extraObj = useMemo(() => { - return parseExtra(extra || ''); - }, [extra]); - - const defaultIcon = useMemo(() => { - switch (parseInt(layout ?? '0')) { - case ViewLayout.Document: - return ; - case ViewLayout.Grid: - return ; - case ViewLayout.Board: - return ; - case ViewLayout.Calendar: - return ; - default: - return ; - } - }, [layout]); - - const { t } = useTranslation(); - - useEffect(() => { - setLoading(!view); - }, [view]); - return { - icon: iconObj?.value || defaultIcon, - name: name || t('menuAppHeader.defaultNewPageName'), - view: view as YView, - loading, - extra: extraObj, - }; -} diff --git a/frontend/appflowy_web_app/src/components/_shared/view-icon/ViewIcon.tsx b/frontend/appflowy_web_app/src/components/_shared/view-icon/ViewIcon.tsx new file mode 100644 index 0000000000..4a802a174d --- /dev/null +++ b/frontend/appflowy_web_app/src/components/_shared/view-icon/ViewIcon.tsx @@ -0,0 +1,44 @@ +import { ViewLayout } from '@/application/collab.type'; +import React, { useMemo } from 'react'; +import { ReactComponent as BoardSvg } from '@/assets/board.svg'; +import { ReactComponent as CalendarSvg } from '@/assets/calendar.svg'; +import { ReactComponent as DocumentSvg } from '@/assets/document.svg'; +import { ReactComponent as GridSvg } from '@/assets/grid.svg'; + +export function ViewIcon({ layout, size }: { layout: ViewLayout; size: number | 'small' | 'medium' | 'large' }) { + const iconSize = useMemo(() => { + if (size === 'small') { + return 'h-4 w-4'; + } + + if (size === 'medium') { + return 'h-6 w-6'; + } + + if (size === 'large') { + return 'h-8 w-8'; + } + + return `h-${size} w-${size}`; + }, [size]); + + if (layout === ViewLayout.Grid) { + return ; + } + + if (layout === ViewLayout.Board) { + return ; + } + + if (layout === ViewLayout.Calendar) { + return ; + } + + if (layout === ViewLayout.Document) { + return ; + } + + return null; +} + +export default ViewIcon; diff --git a/frontend/appflowy_web_app/src/components/_shared/view-icon/index.ts b/frontend/appflowy_web_app/src/components/_shared/view-icon/index.ts new file mode 100644 index 0000000000..1db8ea5807 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/_shared/view-icon/index.ts @@ -0,0 +1 @@ +export * from './ViewIcon'; diff --git a/frontend/appflowy_web_app/src/components/app/App.tsx b/frontend/appflowy_web_app/src/components/app/App.tsx index d7e9037ad5..c66556d8f7 100644 --- a/frontend/appflowy_web_app/src/components/app/App.tsx +++ b/frontend/appflowy_web_app/src/components/app/App.tsx @@ -1,18 +1,15 @@ -import FolderPage from '@/pages/FolderPage'; +import NotFound from '@/components/error/NotFound'; +import PublishPage from '@/pages/PublishPage'; import { BrowserRouter, Route, Routes } from 'react-router-dom'; -import ProtectedRoutes from '@/components/auth/ProtectedRoutes'; -import LoginPage from '@/pages/LoginPage'; -import ProductPage from '@/pages/ProductPage'; import withAppWrapper from '@/components/app/withAppWrapper'; +import '@/styles/app.scss'; const AppMain = withAppWrapper(() => { return ( - }> - } /> - } /> - - } /> + } /> + } /> + } /> ); }); diff --git a/frontend/appflowy_web_app/src/components/app/AppConfig.tsx b/frontend/appflowy_web_app/src/components/app/AppConfig.tsx index fe817e7004..7c215bd530 100644 --- a/frontend/appflowy_web_app/src/components/app/AppConfig.tsx +++ b/frontend/appflowy_web_app/src/components/app/AppConfig.tsx @@ -1,8 +1,20 @@ import { useAppLanguage } from '@/components/app/useAppLanguage'; -import React, { createContext, useEffect, useMemo, useState } from 'react'; -import { AFService } from '@/application/services/services.type'; +import { useSnackbar } from 'notistack'; +import React, { createContext, useEffect, useState } from 'react'; +import { AFService, AFServiceConfig } from '@/application/services/services.type'; import { getService } from '@/application/services'; -import { useAppSelector } from '@/stores/store'; + +const baseURL = import.meta.env.AF_BASE_URL || 'https://test.appflowy.cloud'; +const gotrueURL = import.meta.env.AF_GOTRUE_URL || 'https://test.appflowy.cloud/gotrue'; +const wsURL = import.meta.env.AF_WS_URL || 'wss://test.appflowy.cloud/ws/v1'; + +const defaultConfig: AFServiceConfig = { + cloudConfig: { + baseURL, + gotrueURL, + wsURL, + }, +}; export const AFConfigContext = createContext< | { @@ -12,7 +24,7 @@ export const AFConfigContext = createContext< >(undefined); function AppConfig({ children }: { children: React.ReactNode }) { - const appConfig = useAppSelector((state) => state.app.appConfig); + const [appConfig] = useState(defaultConfig); const [service, setService] = useState(); useAppLanguage(); @@ -24,14 +36,42 @@ function AppConfig({ children }: { children: React.ReactNode }) { })(); }, [appConfig]); - const config = useMemo( - () => ({ - service, - }), - [service] - ); + const { enqueueSnackbar, closeSnackbar } = useSnackbar(); - return {children}; + useEffect(() => { + window.toast = { + success: (message: string) => { + enqueueSnackbar(message, { variant: 'success' }); + }, + error: (message: string) => { + console.log('error', message); + enqueueSnackbar(message, { variant: 'error' }); + }, + warning: (message: string) => { + enqueueSnackbar(message, { variant: 'warning' }); + }, + default: (message: string) => { + enqueueSnackbar(message, { variant: 'default' }); + }, + info: (message: string) => { + enqueueSnackbar(message, { variant: 'info' }); + }, + + clear: () => { + closeSnackbar(); + }, + }; + }, [closeSnackbar, enqueueSnackbar]); + + return ( + + {children} + + ); } export default AppConfig; diff --git a/frontend/appflowy_web_app/src/components/app/AppTheme.tsx b/frontend/appflowy_web_app/src/components/app/AppTheme.tsx index 8ae3d12616..09a3bfe13b 100644 --- a/frontend/appflowy_web_app/src/components/app/AppTheme.tsx +++ b/frontend/appflowy_web_app/src/components/app/AppTheme.tsx @@ -1,4 +1,4 @@ -import { useAppThemeMode } from '@/components/app/useAppThemeMode'; +import { ThemeModeContext, useAppThemeMode } from '@/components/app/useAppThemeMode'; import React, { useMemo } from 'react'; import createTheme from '@mui/material/styles/createTheme'; import ThemeProvider from '@mui/material/styles/ThemeProvider'; @@ -8,7 +8,8 @@ import 'src/styles/tailwind.css'; import 'src/styles/template.css'; function AppTheme({ children }: { children: React.ReactNode }) { - const { isDark } = useAppThemeMode(); + const { isDark, setIsDark } = useAppThemeMode(); + const theme = useMemo( () => createTheme({ @@ -161,7 +162,16 @@ function AppTheme({ children }: { children: React.ReactNode }) { [isDark] ); - return {children}; + return ( + + {children} + + ); } export default AppTheme; diff --git a/frontend/appflowy_web_app/src/components/app/useAppThemeMode.ts b/frontend/appflowy_web_app/src/components/app/useAppThemeMode.ts index 8250d999ec..cdd5eed890 100644 --- a/frontend/appflowy_web_app/src/components/app/useAppThemeMode.ts +++ b/frontend/appflowy_web_app/src/components/app/useAppThemeMode.ts @@ -1,17 +1,30 @@ -import { useEffect, useState } from 'react'; +import { useEffect, useState, createContext } from 'react'; + +export const ThemeModeContext = createContext< + | { + isDark: boolean; + setDark: (isDark: boolean) => void; + } + | undefined +>(undefined); export function useAppThemeMode() { - const [isDark, setIsDark] = useState(false); + const [isDark, setIsDark] = useState(() => { + const darkMode = localStorage.getItem('dark-mode'); + + return darkMode === 'true'; + }); useEffect(() => { function detectColorScheme() { const darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); setIsDark(darkModeMediaQuery.matches); - document.documentElement.setAttribute('data-dark-mode', darkModeMediaQuery.matches ? 'true' : 'false'); } - detectColorScheme(); + if (localStorage.getItem('dark-mode') === null) { + detectColorScheme(); + } window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', detectColorScheme); return () => { @@ -19,7 +32,13 @@ export function useAppThemeMode() { }; }, []); + useEffect(() => { + document.documentElement.setAttribute('data-dark-mode', isDark ? 'true' : 'false'); + localStorage.setItem('dark-mode', isDark ? 'true' : 'false'); + }, [isDark]); + return { isDark, + setIsDark, }; } diff --git a/frontend/appflowy_web_app/src/components/app/withAppWrapper.tsx b/frontend/appflowy_web_app/src/components/app/withAppWrapper.tsx index ca5bdcd100..0fddafd2a5 100644 --- a/frontend/appflowy_web_app/src/components/app/withAppWrapper.tsx +++ b/frontend/appflowy_web_app/src/components/app/withAppWrapper.tsx @@ -1,27 +1,53 @@ -import { Provider } from 'react-redux'; -import { store } from 'src/stores/store'; import { ErrorBoundary } from 'react-error-boundary'; import { ErrorHandlerPage } from 'src/components/error/ErrorHandlerPage'; import AppTheme from '@/components/app/AppTheme'; -import { Toaster } from 'react-hot-toast'; import AppConfig from '@/components/app/AppConfig'; import { Suspense } from 'react'; +import { SnackbarProvider } from 'notistack'; +import { styled } from '@mui/material'; -export default function withAppWrapper (Component: React.FC): React.FC { - return function AppWrapper (): JSX.Element { +const StyledSnackbarProvider = styled(SnackbarProvider)` + &.notistack-MuiContent-default { + background-color: var(--fill-toolbar); + } + + &.notistack-MuiContent-info { + background-color: var(--function-info); + } + + &.notistack-MuiContent-success { + background-color: var(--function-success); + } + + &.notistack-MuiContent-error { + background-color: var(--function-error); + } + + &.notistack-MuiContent-warning { + background-color: var(--function-warning); + } +`; + +export default function withAppWrapper(Component: React.FC): React.FC { + return function AppWrapper(): JSX.Element { return ( - - - + + + - - - - + + + ); }; -} \ No newline at end of file +} diff --git a/frontend/appflowy_web_app/src/components/auth/LoginButtonGroup.tsx b/frontend/appflowy_web_app/src/components/auth/LoginButtonGroup.tsx deleted file mode 100644 index 5e437bd0f7..0000000000 --- a/frontend/appflowy_web_app/src/components/auth/LoginButtonGroup.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import Button from '@mui/material/Button'; -import GoogleIcon from '@/assets/settings/google.png'; -import GithubIcon from '@/assets/settings/github.png'; -import DiscordIcon from '@/assets/settings/discord.png'; -import { useTranslation } from 'react-i18next'; -import { useAuth } from './auth.hooks'; -import { ProviderType } from '@/application/user.type'; -import { useState } from 'react'; -import EmailOutlined from '@mui/icons-material/EmailOutlined'; -import SignInWithEmail from './SignInWithEmail'; - -export const LoginButtonGroup = () => { - const { t } = useTranslation(); - const [openSignInWithEmail, setOpenSignInWithEmail] = useState(false); - const { signInWithProvider } = useAuth(); - - return ( -
- - - - - setOpenSignInWithEmail(false)} /> -
- ); -}; diff --git a/frontend/appflowy_web_app/src/components/auth/ProtectedRoutes.tsx b/frontend/appflowy_web_app/src/components/auth/ProtectedRoutes.tsx deleted file mode 100644 index 4d6825c1cb..0000000000 --- a/frontend/appflowy_web_app/src/components/auth/ProtectedRoutes.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import React, { lazy, useCallback, useEffect, useMemo, useState } from 'react'; -import { useAuth } from '@/components/auth/auth.hooks'; -import { currentUserActions, LoginState } from '@/stores/currentUser/slice'; -import { useAppDispatch } from '@/stores/store'; -import { getPlatform } from '@/utils/platform'; -import SplashScreen from '@/components/auth/SplashScreen'; -import CircularProgress from '@mui/material/CircularProgress'; -import Portal from '@mui/material/Portal'; -import { ReactComponent as Logo } from '@/assets/logo.svg'; -import { useNavigate } from 'react-router-dom'; - -const TauriAuth = lazy(() => import('@/components/tauri/TauriAuth')); - -function ProtectedRoutes() { - const { currentUser, checkUser, isReady } = useAuth(); - - const isLoading = currentUser?.loginState === LoginState.LOADING; - const [checked, setChecked] = useState(false); - - const checkUserStatus = useCallback(async () => { - if (!isReady) return; - setChecked(false); - try { - if (!currentUser.isAuthenticated) { - await checkUser(); - } - } finally { - setChecked(true); - } - }, [checkUser, isReady, currentUser.isAuthenticated]); - - useEffect(() => { - void checkUserStatus(); - }, [checkUserStatus]); - - const platform = useMemo(() => getPlatform(), []); - - const navigate = useNavigate(); - - if (checked && !currentUser.isAuthenticated && window.location.pathname !== '/login') { - navigate(`/login?redirect=${encodeURIComponent(window.location.pathname)}`); - return null; - } - - if (currentUser.user?.workspaceId && (window.location.pathname === '/' || window.location.pathname === '')) { - navigate(`/view/${currentUser.user.workspaceId}`); - return null; - } - - return ( -
- {checked ? ( - - ) : ( -
- -
- )} - - {isLoading && } - {platform.isTauri && } -
- ); -} - -export default ProtectedRoutes; - -const StartLoading = () => { - const dispatch = useAppDispatch(); - - useEffect(() => { - const preventDefault = (e: KeyboardEvent) => { - if (e.key === 'Escape') { - e.preventDefault(); - dispatch(currentUserActions.resetLoginState()); - } - }; - - document.addEventListener('keydown', preventDefault, true); - - return () => { - document.removeEventListener('keydown', preventDefault, true); - }; - }, [dispatch]); - return ( - -
- -
-
- ); -}; diff --git a/frontend/appflowy_web_app/src/components/auth/SignInWithEmail.tsx b/frontend/appflowy_web_app/src/components/auth/SignInWithEmail.tsx deleted file mode 100644 index 06d36c2594..0000000000 --- a/frontend/appflowy_web_app/src/components/auth/SignInWithEmail.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import { Button, CircularProgress, Dialog, DialogActions, DialogContent, TextField } from '@mui/material'; -import React, { useState } from 'react'; -import { useAuth } from '@/components/auth/auth.hooks'; -import { useTranslation } from 'react-i18next'; - -function SignInWithEmail({ open, onClose }: { open: boolean; onClose: () => void }) { - const { t } = useTranslation(); - const [loading, setLoading] = useState(false); - const [email, setEmail] = useState(''); - const [password, setPassword] = useState(''); - const { signInWithEmailPassword } = useAuth(); - - const handleSignIn = async () => { - setLoading(true); - try { - await signInWithEmailPassword(email, password); - onClose(); - } catch (e) { - // Handle error - } - - setLoading(false); - }; - - return ( - { - if (e.key === 'Enter') { - e.preventDefault(); - void handleSignIn(); - } - }} - > - - setEmail(e.target.value)} - /> - setPassword(e.target.value)} - /> - - - - - - - ); -} - -export default SignInWithEmail; diff --git a/frontend/appflowy_web_app/src/components/auth/SplashScreen.tsx b/frontend/appflowy_web_app/src/components/auth/SplashScreen.tsx deleted file mode 100644 index bf5a5a854d..0000000000 --- a/frontend/appflowy_web_app/src/components/auth/SplashScreen.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import React from 'react'; -import { Outlet } from 'react-router-dom'; -import Layout from '@/components/layout/Layout'; - -function SplashScreen () { - - return ( - - - - ); -} - -export default SplashScreen; \ No newline at end of file diff --git a/frontend/appflowy_web_app/src/components/auth/Welcome.cy.tsx b/frontend/appflowy_web_app/src/components/auth/Welcome.cy.tsx deleted file mode 100644 index 768cf3587b..0000000000 --- a/frontend/appflowy_web_app/src/components/auth/Welcome.cy.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import React from 'react'; -import Welcome from './Welcome'; -import withAppWrapper from '@/components/app/withAppWrapper'; - -describe('', () => { - beforeEach(() => { - cy.mockAPI(); - }); - - it('renders', () => { - const AppWrapper = withAppWrapper(Welcome); - - cy.mount(); - }); - - it('should handle login success', () => { - const AppWrapper = withAppWrapper(Welcome); - - cy.mount(); - - cy.get('[data-cy=signInWithEmail]').click(); - - cy.wait(100); - - cy.get('[data-cy=signInWithEmailDialog]').as('dialog').should('be.visible'); - cy.get('[data-cy=email]').type('fakeEmail123'); - cy.get('[data-cy=password]').type('fakePassword123'); - cy.get('[data-cy=submit]').click(); - cy.wait('@loginSuccess'); - cy.wait('@verifyToken'); - cy.wait('@getUserProfile'); - cy.wait('@getUserWorkspace'); - cy.get('@dialog').should('not.exist'); - }); -}); diff --git a/frontend/appflowy_web_app/src/components/auth/Welcome.tsx b/frontend/appflowy_web_app/src/components/auth/Welcome.tsx deleted file mode 100644 index 1281c3336f..0000000000 --- a/frontend/appflowy_web_app/src/components/auth/Welcome.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { ReactComponent as AppflowyLogo } from '@/assets/logo.svg'; -import { Stack } from '@mui/material'; -import { useTranslation } from 'react-i18next'; -import { LoginButtonGroup } from './LoginButtonGroup'; -import { getPlatform } from '@/utils/platform'; -import { lazy } from 'react'; - -const SignInAsAnonymous = lazy(() => import('@/components/tauri/SignInAsAnonymous')); - -export const Welcome = () => { - const { t } = useTranslation(); - - return ( - <> -
e.preventDefault()} method='POST'> - -
- -
- -
- - {t('welcomeTo')} {t('appName')} - -
- -
- {getPlatform().isTauri && } -
- -
-
-
-
- - ); -}; - -export default Welcome; diff --git a/frontend/appflowy_web_app/src/components/auth/auth.hooks.ts b/frontend/appflowy_web_app/src/components/auth/auth.hooks.ts deleted file mode 100644 index affe339c81..0000000000 --- a/frontend/appflowy_web_app/src/components/auth/auth.hooks.ts +++ /dev/null @@ -1,192 +0,0 @@ -import { useAppDispatch, useAppSelector } from '@/stores/store'; -import { useCallback, useContext } from 'react'; -import { nanoid } from 'nanoid'; -import { open } from '@tauri-apps/api/shell'; -import { ProviderType, UserProfile } from '@/application/user.type'; -import { currentUserActions } from '@/stores/currentUser/slice'; -import { AFConfigContext } from '@/components/app/AppConfig'; -import { notify } from '@/components/_shared/notify'; - -export const useAuth = () => { - const dispatch = useAppDispatch(); - const AFConfig = useContext(AFConfigContext); - const currentUser = useAppSelector((state) => state.currentUser); - const isReady = !!AFConfig?.service; - - const handleSuccess = useCallback(() => { - notify.clear(); - dispatch(currentUserActions.loginSuccess()); - }, [dispatch]); - - const setUser = useCallback( - async (userProfile: UserProfile) => { - handleSuccess(); - dispatch(currentUserActions.updateUser(userProfile)); - }, - [dispatch, handleSuccess] - ); - - const handleStart = useCallback(() => { - notify.clear(); - notify.loading('Loading...'); - dispatch(currentUserActions.loginStart()); - }, [dispatch]); - - const handleError = useCallback( - ({ message }: { message: string }) => { - notify.clear(); - notify.error(message); - dispatch(currentUserActions.loginError()); - }, - [dispatch] - ); - - // Check if the user is authenticated - const checkUser = useCallback(async () => { - try { - const userHasSignIn = await AFConfig?.service?.userService.checkUser(); - - if (!userHasSignIn) { - throw new Error('Failed to check user'); - } - - const userProfile = await AFConfig?.service?.userService.getUserProfile(); - - if (!userProfile) { - throw new Error('Failed to check user'); - } - - console.log('userProfile', userProfile); - await setUser(userProfile); - - return userProfile; - } catch (e) { - return Promise.reject('Failed to check user'); - } - }, [AFConfig?.service?.userService, setUser]); - - const register = useCallback( - async (email: string, password: string, name: string): Promise => { - handleStart(); - try { - const userProfile = await AFConfig?.service?.authService.signupWithEmailPassword({ - email, - password, - name, - }); - - if (!userProfile) { - throw new Error('Failed to register'); - } - - await setUser(userProfile); - - return userProfile; - } catch (e) { - handleError({ - message: 'Failed to register', - }); - return null; - } - }, - [handleStart, AFConfig?.service?.authService, setUser, handleError] - ); - - const logout = useCallback(async () => { - try { - await AFConfig?.service?.authService.signOut(); - dispatch(currentUserActions.logout()); - } catch (e) { - handleError({ - message: 'Failed to logout', - }); - } - }, [AFConfig?.service?.authService, dispatch, handleError]); - - const signInAsAnonymous = useCallback(async () => { - const fakeEmail = nanoid(8) + '@appflowy.io'; - const fakePassword = 'AppFlowy123@'; - const fakeName = 'Me'; - - await register(fakeEmail, fakePassword, fakeName); - }, [register]); - - const signInWithProvider = useCallback( - async (provider: ProviderType) => { - handleStart(); - try { - const url = await AFConfig?.service?.authService.getOAuthURL(provider); - - if (!url) { - throw new Error(); - } - - await open(url); - } catch { - handleError({ - message: 'Failed to sign in', - }); - } - }, - [AFConfig?.service?.authService, handleError, handleStart] - ); - - const signInWithOAuth = useCallback( - async (uri: string) => { - handleStart(); - try { - await AFConfig?.service?.authService.signInWithOAuth({ uri }); - const userProfile = await AFConfig?.service?.userService.getUserProfile(); - - if (!userProfile) { - throw new Error(); - } - - await setUser(userProfile); - - return userProfile; - } catch (e) { - handleError({ - message: 'Failed to sign in', - }); - } - }, - [AFConfig?.service?.authService, AFConfig?.service?.userService, handleError, handleStart, setUser] - ); - - const signInWithEmailPassword = useCallback( - async (email: string, password: string) => { - handleStart(); - try { - await AFConfig?.service?.authService.signinWithEmailPassword(email, password); - - const userProfile = await AFConfig?.service?.userService.getUserProfile(); - - if (!userProfile) { - throw new Error(); - } - - await setUser(userProfile); - - return userProfile; - } catch (e) { - handleError({ - message: 'Failed to sign in', - }); - } - }, - [AFConfig?.service?.authService, AFConfig?.service?.userService, handleError, handleStart, setUser] - ); - - return { - isReady, - currentUser, - checkUser, - register, - logout, - signInWithProvider, - signInAsAnonymous, - signInWithOAuth, - signInWithEmailPassword, - }; -}; diff --git a/frontend/appflowy_web_app/src/components/database/Database.hooks.ts b/frontend/appflowy_web_app/src/components/database/Database.hooks.ts deleted file mode 100644 index a8945cc6ba..0000000000 --- a/frontend/appflowy_web_app/src/components/database/Database.hooks.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { YDoc, YjsEditorKey } from '@/application/collab.type'; -import { DatabaseContextState } from '@/application/database-yjs'; -import { AFConfigContext } from '@/components/app/AppConfig'; -import { Log } from '@/utils/log'; -import { useCallback, useContext, useEffect, useState } from 'react'; - -export function useGetDatabaseId(iidIndex: string) { - const [databaseId, setDatabaseId] = useState(); - const databaseService = useContext(AFConfigContext)?.service?.databaseService; - - const loadDatabaseId = useCallback(async () => { - if (!databaseService) return; - const databases = await databaseService.getWorkspaceDatabases(); - - console.log('databses', databases); - const id = databases.find((item) => item.views.includes(iidIndex))?.database_id; - - if (!id) return; - setDatabaseId(id); - }, [iidIndex, databaseService]); - - useEffect(() => { - void loadDatabaseId(); - }, [loadDatabaseId]); - return databaseId; -} - -export function useGetDatabaseDispatch() { - const databaseService = useContext(AFConfigContext)?.service?.databaseService; - const onOpenDatabase = useCallback( - async ({ databaseId, rowIds }: { databaseId: string; rowIds?: string[] }) => { - if (!databaseService) return Promise.reject(); - return databaseService.openDatabase(databaseId, rowIds); - }, - [databaseService] - ); - - const onCloseDatabase = useCallback( - (databaseId: string) => { - if (!databaseService) return; - void databaseService.closeDatabase(databaseId); - }, - [databaseService] - ); - - return { - onOpenDatabase, - onCloseDatabase, - }; -} - -export function useLoadDatabase({ databaseId, rowIds }: { databaseId?: string; rowIds?: string[] }) { - const [doc, setDoc] = useState(null); - const [rows, setRows] = useState(null); // Map(false); - const { onOpenDatabase, onCloseDatabase } = useGetDatabaseDispatch(); - - const handleOpenDatabase = useCallback( - async (databaseId: string, rowIds?: string[]) => { - try { - setDoc(null); - const { databaseDoc, rows } = await onOpenDatabase({ - databaseId, - rowIds, - }); - - console.log('databaseDoc', databaseDoc.getMap(YjsEditorKey.data_section).toJSON()); - console.log('rows', rows); - - setDoc(databaseDoc); - setRows(rows); - } catch (e) { - Log.error(e); - setNotFound(true); - } - }, - [onOpenDatabase] - ); - - useEffect(() => { - if (!databaseId) return; - void handleOpenDatabase(databaseId, rowIds); - return () => { - onCloseDatabase(databaseId); - }; - }, [handleOpenDatabase, databaseId, rowIds, onCloseDatabase]); - - return { doc, rows, notFound }; -} diff --git a/frontend/appflowy_web_app/src/components/database/Database.tsx b/frontend/appflowy_web_app/src/components/database/Database.tsx index 0c590462f9..4b3964e240 100644 --- a/frontend/appflowy_web_app/src/components/database/Database.tsx +++ b/frontend/appflowy_web_app/src/components/database/Database.tsx @@ -1,23 +1,103 @@ +import { YDatabase, YDoc, YjsDatabaseKey, YjsEditorKey } from '@/application/collab.type'; +import { ViewMeta } from '@/application/db/tables/view_metas'; +import ComponentLoading from '@/components/_shared/progress/ComponentLoading'; +import DatabaseRow from '@/components/database/DatabaseRow'; import DatabaseViews from '@/components/database/DatabaseViews'; +import { ViewMetaPreview, ViewMetaProps } from '@/components/view-meta/ViewMetaPreview'; +import React, { Suspense, useCallback, useEffect, useMemo, useState } from 'react'; +import { useSearchParams } from 'react-router-dom'; +import * as Y from 'yjs'; +import { DatabaseContextProvider } from './DatabaseContext'; -import React, { memo } from 'react'; +export interface Database2Props extends ViewMetaProps { + doc: YDoc; + getViewRowsMap?: (viewId: string, rowIds: string[]) => Promise<{ rows: Y.Map; destroy: () => void }>; + loadView?: (viewId: string) => Promise; + navigateToView?: (viewId: string) => Promise; + loadViewMeta?: (viewId: string) => Promise; +} -export const Database = memo( - ({ - viewId, - onNavigateToView, - iidIndex, - }: { - iidIndex: string; - viewId: string; - onNavigateToView: (viewId: string) => void; - }) => { - return ( -
- -
- ); +function Database({ doc, getViewRowsMap, navigateToView, loadViewMeta, loadView, ...viewMeta }: Database2Props) { + const [search, setSearch] = useSearchParams(); + + const viewId = search.get('v') || viewMeta.viewId; + + const rowIds = useMemo(() => { + if (!viewId) return []; + const database = doc.getMap(YjsEditorKey.data_section).get(YjsEditorKey.database) as YDatabase; + const rows = database.get(YjsDatabaseKey.views).get(viewId).get(YjsDatabaseKey.row_orders); + + return rows.toArray().map((row) => row.get(YjsDatabaseKey.id)); + }, [doc, viewId]); + + const iidIndex = useMemo(() => { + const database = doc.getMap(YjsEditorKey.data_section).get(YjsEditorKey.database) as YDatabase; + + return database.get(YjsDatabaseKey.metas).get(YjsDatabaseKey.iid); + }, [doc]); + + const [rowDocMap, setRowDocMap] = useState | null>(null); + + useEffect(() => { + if (!getViewRowsMap || !rowIds.length || !iidIndex) return; + + void (async () => { + const { rows, destroy } = await getViewRowsMap(iidIndex, rowIds); + + setRowDocMap(rows); + return destroy; + })(); + }, [getViewRowsMap, rowIds, iidIndex]); + + const rowId = search.get('r'); + + const handleChangeView = useCallback( + (viewId: string) => { + setSearch({ v: viewId }); + }, + [setSearch] + ); + + const handleNavigateToRow = useCallback( + (rowId: string) => { + setSearch({ r: rowId }); + }, + [setSearch] + ); + + if (!rowDocMap || !viewId) { + return null; } -); + + return ( +
+ }> + + {rowId ? ( + + ) : ( +
+ {viewMeta && } + +
+ +
+
+ )} +
+
+
+ ); +} export default Database; diff --git a/frontend/appflowy_web_app/src/components/database/DatabaseTitle.tsx b/frontend/appflowy_web_app/src/components/database/DatabaseTitle.tsx deleted file mode 100644 index fb996978ff..0000000000 --- a/frontend/appflowy_web_app/src/components/database/DatabaseTitle.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { usePageInfo } from '@/components/_shared/page/usePageInfo'; -import React from 'react'; - -function DatabaseTitle({ viewId }: { viewId: string }) { - const { name, icon } = usePageInfo(viewId); - - return ( -
-
-
-
{icon}
-
{name}
-
-
-
- ); -} - -export default DatabaseTitle; diff --git a/frontend/appflowy_web_app/src/components/database/__tests__/Database.cy.tsx b/frontend/appflowy_web_app/src/components/database/__tests__/Database.cy.tsx index 23793e9227..3bcc1dd215 100644 --- a/frontend/appflowy_web_app/src/components/database/__tests__/Database.cy.tsx +++ b/frontend/appflowy_web_app/src/components/database/__tests__/Database.cy.tsx @@ -1,11 +1,10 @@ import { renderDatabase } from '@/components/database/__tests__/withTestingDatabase'; -import '@/components/layout/layout.scss'; +import '@/styles/app.scss'; describe('', () => { beforeEach(() => { cy.viewport(1280, 720); Object.defineProperty(window.navigator, 'language', { value: 'en-US' }); - cy.mockDatabase(); }); it('renders with a database', () => { @@ -18,7 +17,7 @@ describe('', () => { onNavigateToView, }, () => { - cy.get('[data-testid^=view-tab-]').should('have.length', 4); + cy.get('[data-testid^=view-tab-]').should('have.length', 10); cy.get('.database-grid').should('exist'); cy.get('[data-testid=view-tab-e410747b-5f2f-45a0-b2f7-890ad3001355]').click(); @@ -27,11 +26,13 @@ describe('', () => { cy.wait(800); cy.get('[data-testid=view-tab-7d2148fc-cace-4452-9c5c-96e52e6bf8b5]').click(); + cy.wait(800); cy.get('.database-grid').should('exist'); cy.wrap(onNavigateToView).should('have.been.calledWith', '7d2148fc-cace-4452-9c5c-96e52e6bf8b5'); cy.wait(800); cy.get('[data-testid=view-tab-2143e95d-5dcb-4e0f-bb2c-50944e6e019f]').click(); + cy.wait(800); cy.get('.database-calendar').should('exist'); cy.wrap(onNavigateToView).should('have.been.calledWith', '2143e95d-5dcb-4e0f-bb2c-50944e6e019f'); } diff --git a/frontend/appflowy_web_app/src/components/database/__tests__/DatabaseRow.cy.tsx b/frontend/appflowy_web_app/src/components/database/__tests__/DatabaseRow.cy.tsx index 00255c4ed8..bd6e6c5125 100644 --- a/frontend/appflowy_web_app/src/components/database/__tests__/DatabaseRow.cy.tsx +++ b/frontend/appflowy_web_app/src/components/database/__tests__/DatabaseRow.cy.tsx @@ -1,46 +1,39 @@ -import { YDoc, YFolder, YjsEditorKey } from '@/application/collab.type'; +import { YDoc } from '@/application/collab.type'; import { applyYDoc } from '@/application/ydoc/apply'; -import { FolderProvider } from '@/components/_shared/context-provider/FolderProvider'; -import { IdProvider } from '@/components/_shared/context-provider/IdProvider'; import withAppWrapper from '@/components/app/withAppWrapper'; import { DatabaseRow } from 'src/components/database/DatabaseRow'; import { DatabaseContextProvider } from 'src/components/database/DatabaseContext'; import * as Y from 'yjs'; -import '@/components/layout/layout.scss'; +import '@/styles/app.scss'; describe('', () => { beforeEach(() => { cy.viewport(1280, 720); - Object.defineProperty(window.navigator, 'language', { value: 'en-US' }); - Object.defineProperty(window.navigator, 'languages', { value: ['en-US'] }); - cy.mockDatabase(); - cy.mockDocument('f56bdf0f-90c8-53fb-97d9-ad5860d2b7a0'); }); it('renders with a row', () => { cy.wait(1000); - cy.fixture('folder').then((folderJson) => { + Object.defineProperty(window.navigator, 'language', { value: 'en-US' }); + cy.fixture('database/4c658817-20db-4f56-b7f9-0637a22dfeb6').then((database) => { const doc = new Y.Doc(); - const state = new Uint8Array(folderJson.data.doc_state); + const databaseState = new Uint8Array(database.data.doc_state); - applyYDoc(doc, state); - const folder = doc.getMap(YjsEditorKey.data_section).get(YjsEditorKey.folder) as YFolder; + applyYDoc(doc, databaseState); - cy.fixture('database/4c658817-20db-4f56-b7f9-0637a22dfeb6').then((database) => { - const doc = new Y.Doc(); - const databaseState = new Uint8Array(database.data.doc_state); + cy.fixture('database/rows/4c658817-20db-4f56-b7f9-0637a22dfeb6').then((rows) => { + const rootRowsDoc = new Y.Doc(); + const rowsFolder: Y.Map = rootRowsDoc.getMap(); + const data = rows['2f944220-9f45-40d9-96b5-e8c0888daf7c']; + const rowDoc = new Y.Doc(); - applyYDoc(doc, databaseState); + applyYDoc(rowDoc, new Uint8Array(data)); + rowsFolder.set('2f944220-9f45-40d9-96b5-e8c0888daf7c', rowDoc); - cy.fixture('database/rows/4c658817-20db-4f56-b7f9-0637a22dfeb6').then((rows) => { - const rootRowsDoc = new Y.Doc(); - const rowsFolder: Y.Map = rootRowsDoc.getMap(); - const data = rows['2f944220-9f45-40d9-96b5-e8c0888daf7c']; - const rowDoc = new Y.Doc(); - - applyYDoc(rowDoc, new Uint8Array(data)); - rowsFolder.set('2f944220-9f45-40d9-96b5-e8c0888daf7c', rowDoc); + cy.fixture('simple_doc').then((docJson) => { + const subDoc = new Y.Doc(); + const state = new Uint8Array(docJson.data.doc_state); + applyYDoc(subDoc, state); const AppWrapper = withAppWrapper(() => { return (
@@ -48,8 +41,8 @@ describe('', () => { rowId={'2f944220-9f45-40d9-96b5-e8c0888daf7c'} databaseDoc={doc} rows={rowsFolder} - folder={folder} viewId={'7d2148fc-cace-4452-9c5c-96e52e6bf8b5'} + loadView={() => Promise.resolve(subDoc)} />
); @@ -70,28 +63,25 @@ function TestDatabaseRow({ rowId, databaseDoc, rows, - folder, viewId, + loadView, }: { rowId: string; databaseDoc: YDoc; rows: Y.Map; - folder: YFolder; viewId: string; + loadView?: (viewId: string) => Promise; }) { return ( - - - - - - - + + + ); } diff --git a/frontend/appflowy_web_app/src/components/database/__tests__/DatabaseWithFilter.cy.tsx b/frontend/appflowy_web_app/src/components/database/__tests__/DatabaseWithFilter.cy.tsx index 4c63443ad9..a76678db78 100644 --- a/frontend/appflowy_web_app/src/components/database/__tests__/DatabaseWithFilter.cy.tsx +++ b/frontend/appflowy_web_app/src/components/database/__tests__/DatabaseWithFilter.cy.tsx @@ -1,11 +1,10 @@ import { renderDatabase } from '@/components/database/__tests__/withTestingDatabase'; -import '@/components/layout/layout.scss'; +import '@/styles/app.scss'; describe(' with filters and sorts', () => { beforeEach(() => { cy.viewport(1280, 720); Object.defineProperty(window.navigator, 'language', { value: 'en-US' }); - cy.mockDatabase(); }); it('render a database with filters and sorts', () => { diff --git a/frontend/appflowy_web_app/src/components/database/__tests__/withTestingDatabase.tsx b/frontend/appflowy_web_app/src/components/database/__tests__/withTestingDatabase.tsx index d9e79811d1..9b79f9c40a 100644 --- a/frontend/appflowy_web_app/src/components/database/__tests__/withTestingDatabase.tsx +++ b/frontend/appflowy_web_app/src/components/database/__tests__/withTestingDatabase.tsx @@ -1,12 +1,10 @@ -import { YDoc, YFolder, YjsEditorKey } from '@/application/collab.type'; +import { YDoc } from '@/application/collab.type'; import { applyYDoc } from '@/application/ydoc/apply'; -import { FolderProvider } from '@/components/_shared/context-provider/FolderProvider'; -import { IdProvider } from '@/components/_shared/context-provider/IdProvider'; import withAppWrapper from '@/components/app/withAppWrapper'; import { DatabaseContextProvider } from '@/components/database/DatabaseContext'; +import DatabaseViews from '@/components/database/DatabaseViews'; import { useState } from 'react'; import * as Y from 'yjs'; -import { Database } from 'src/components/database/Database'; export function renderDatabase( { @@ -20,49 +18,39 @@ export function renderDatabase( }, onAfterRender?: () => void ) { - cy.fixture('folder').then((folderJson) => { - const doc = new Y.Doc(); - const state = new Uint8Array(folderJson.data.doc_state); + cy.fixture(`database/${databaseId}`).then((database) => { + cy.fixture(`database/rows/${databaseId}`).then((rows) => { + const doc = new Y.Doc(); + const rootRowsDoc = new Y.Doc(); + const rowsFolder: Y.Map = rootRowsDoc.getMap(); + const databaseState = new Uint8Array(database.data.doc_state); - applyYDoc(doc, state); + applyYDoc(doc, databaseState); - const folder = doc.getMap(YjsEditorKey.data_section).get(YjsEditorKey.folder) as YFolder; + Object.keys(rows).forEach((key) => { + const data = rows[key]; + const rowDoc = new Y.Doc(); - cy.fixture(`database/${databaseId}`).then((database) => { - cy.fixture(`database/rows/${databaseId}`).then((rows) => { - const doc = new Y.Doc(); - const rootRowsDoc = new Y.Doc(); - const rowsFolder: Y.Map = rootRowsDoc.getMap(); - const databaseState = new Uint8Array(database.data.doc_state); - - applyYDoc(doc, databaseState); - - Object.keys(rows).forEach((key) => { - const data = rows[key]; - const rowDoc = new Y.Doc(); - - applyYDoc(rowDoc, new Uint8Array(data)); - rowsFolder.set(key, rowDoc); - }); - - const AppWrapper = withAppWrapper(() => { - return ( -
- -
- ); - }); - - cy.mount(); - onAfterRender?.(); + applyYDoc(rowDoc, new Uint8Array(data)); + rowsFolder.set(key, rowDoc); }); + + const AppWrapper = withAppWrapper(() => { + return ( +
+ +
+ ); + }); + + cy.mount(); + onAfterRender?.(); }); }); } @@ -70,14 +58,12 @@ export function renderDatabase( export function TestDatabase({ databaseDoc, rows, - folder, iidIndex, initialViewId, onNavigateToView, }: { databaseDoc: YDoc; rows: Y.Map; - folder: YFolder; iidIndex: string; initialViewId: string; onNavigateToView: (viewId: string) => void; @@ -90,17 +76,13 @@ export function TestDatabase({ }; return ( - - - - - - - + + + ); } diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/relation/RelationItems.tsx b/frontend/appflowy_web_app/src/components/database/components/cell/relation/RelationItems.tsx index 259447e0ae..7086d70a1b 100644 --- a/frontend/appflowy_web_app/src/components/database/components/cell/relation/RelationItems.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/cell/relation/RelationItems.tsx @@ -1,54 +1,28 @@ -import { YDatabaseField, YDatabaseFields, YjsDatabaseKey, YjsEditorKey } from '@/application/collab.type'; -import { - DatabaseContextState, - parseRelationTypeOption, - useDatabase, - useFieldSelector, - useNavigateToRow, -} from '@/application/database-yjs'; +import { YjsDatabaseKey } from '@/application/collab.type'; +import { DatabaseContext, DatabaseContextState, useDatabase, useNavigateToRow } from '@/application/database-yjs'; import { RelationCell, RelationCellData } from '@/application/database-yjs/cell.type'; import { RelationPrimaryValue } from '@/components/database/components/cell/relation/RelationPrimaryValue'; -import { useGetDatabaseDispatch } from '@/components/database/Database.hooks'; -import React, { useEffect, useMemo, useState } from 'react'; +import React, { useContext, useEffect, useMemo, useState } from 'react'; -function RelationItems({ style, cell, fieldId }: { cell: RelationCell; fieldId: string; style?: React.CSSProperties }) { - const { field } = useFieldSelector(fieldId); - const currentDatabaseId = useDatabase()?.get(YjsDatabaseKey.id); - const { onOpenDatabase, onCloseDatabase } = useGetDatabaseDispatch(); +function RelationItems({ style, cell }: { cell: RelationCell; fieldId: string; style?: React.CSSProperties }) { + const database = useDatabase(); + const viewId = database.get(YjsDatabaseKey.metas)?.get(YjsDatabaseKey.iid)?.toString(); const rowIds = useMemo(() => { return (cell.data?.toJSON() as RelationCellData) ?? []; }, [cell.data]); - const databaseId = rowIds.length > 0 && field ? parseRelationTypeOption(field).database_id : undefined; - const [databasePrimaryFieldId, setDatabasePrimaryFieldId] = useState(undefined); + const getViewRowsMap = useContext(DatabaseContext)?.getViewRowsMap; + const [rows, setRows] = useState(); const navigateToRow = useNavigateToRow(); useEffect(() => { - if (!databaseId || !rowIds.length) return; - void onOpenDatabase({ databaseId, rowIds }).then(({ databaseDoc: doc, rows }) => { - const fields = doc - .getMap(YjsEditorKey.data_section) - .get(YjsEditorKey.database) - .get(YjsDatabaseKey.fields) as YDatabaseFields; - - fields.forEach((field, fieldId) => { - if ((field as YDatabaseField).get(YjsDatabaseKey.is_primary)) { - setDatabasePrimaryFieldId(fieldId); - } - }); + if (!viewId || !rowIds.length) return; + void getViewRowsMap?.(viewId, rowIds).then(({ rows }) => { setRows(rows); }); - }, [onOpenDatabase, databaseId, rowIds, onCloseDatabase]); - - useEffect(() => { - return () => { - if (currentDatabaseId !== databaseId && databaseId) { - onCloseDatabase(databaseId); - } - }; - }, [databaseId, currentDatabaseId, onCloseDatabase]); + }, [getViewRowsMap, rowIds, viewId]); return (
@@ -64,9 +38,7 @@ function RelationItems({ style, cell, fieldId }: { cell: RelationCell; fieldId: }} className={'w-full cursor-pointer underline'} > - {rowDoc && databasePrimaryFieldId && ( - - )} + {rowDoc && }
); })} diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/relation/RelationPrimaryValue.tsx b/frontend/appflowy_web_app/src/components/database/components/cell/relation/RelationPrimaryValue.tsx index a6ae613dd5..cb10266e23 100644 --- a/frontend/appflowy_web_app/src/components/database/components/cell/relation/RelationPrimaryValue.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/cell/relation/RelationPrimaryValue.tsx @@ -1,8 +1,9 @@ -import { FieldId, YDatabaseRow, YDoc, YjsDatabaseKey, YjsEditorKey } from '@/application/collab.type'; +import { FieldId, YDatabaseCell, YDatabaseRow, YDoc, YjsDatabaseKey, YjsEditorKey } from '@/application/collab.type'; +import { FieldType } from '@/application/database-yjs'; import { parseYDatabaseCellToCell } from '@/application/database-yjs/cell.parse'; import React, { useEffect, useState } from 'react'; -export function RelationPrimaryValue({ rowDoc, fieldId }: { rowDoc: YDoc; fieldId: FieldId }) { +export function RelationPrimaryValue({ rowDoc, fieldId }: { rowDoc: YDoc; fieldId?: FieldId }) { const [text, setText] = useState(null); const [row, setRow] = useState(null); @@ -23,18 +24,34 @@ export function RelationPrimaryValue({ rowDoc, fieldId }: { rowDoc: YDoc; fieldI useEffect(() => { if (!row) return; const cells = row.get(YjsDatabaseKey.cells); - const primaryCell = cells.get(fieldId); - if (!primaryCell) return; + let primaryCell: YDatabaseCell | undefined; + + if (fieldId) { + primaryCell = cells?.get(fieldId); + } else { + const fieldId = Array.from(cells.keys()).find((key) => { + const fieldType = cells.get(key)?.get(YjsDatabaseKey.field_type); + + if (!fieldType) return false; + return Number(fieldType) === FieldType.RichText; + }); + + if (fieldId) { + primaryCell = cells?.get(fieldId); + } + } + const observeHandler = () => { + if (!primaryCell) return; setText(parseYDatabaseCellToCell(primaryCell).data as string); }; observeHandler(); - primaryCell.observe(observeHandler); + primaryCell?.observe(observeHandler); return () => { - primaryCell.unobserve(observeHandler); + primaryCell?.unobserve(observeHandler); }; }, [row, fieldId]); diff --git a/frontend/appflowy_web_app/src/components/database/components/database-row/DatabaseRowSubDocument.tsx b/frontend/appflowy_web_app/src/components/database/components/database-row/DatabaseRowSubDocument.tsx index 09339b530e..bf761b5612 100644 --- a/frontend/appflowy_web_app/src/components/database/components/database-row/DatabaseRowSubDocument.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/database-row/DatabaseRowSubDocument.tsx @@ -1,6 +1,5 @@ import { YDoc } from '@/application/collab.type'; -import { useRowMetaSelector } from '@/application/database-yjs'; -import { AFConfigContext } from '@/components/app/AppConfig'; +import { DatabaseContext, useRowMetaSelector } from '@/application/database-yjs'; import { Editor } from '@/components/editor'; import CircularProgress from '@mui/material/CircularProgress'; import React, { useCallback, useContext, useEffect, useState } from 'react'; @@ -8,17 +7,19 @@ import React, { useCallback, useContext, useEffect, useState } from 'react'; export function DatabaseRowSubDocument({ rowId }: { rowId: string }) { const meta = useRowMetaSelector(rowId); const documentId = meta?.documentId; + const loadView = useContext(DatabaseContext)?.loadView; + const getViewRowsMap = useContext(DatabaseContext)?.getViewRowsMap; + const navigateToView = useContext(DatabaseContext)?.navigateToView; + const loadViewMeta = useContext(DatabaseContext)?.loadViewMeta; const [loading, setLoading] = useState(true); const [doc, setDoc] = useState(null); - const documentService = useContext(AFConfigContext)?.service?.documentService; - const handleOpenDocument = useCallback(async () => { - if (!documentService || !documentId) return; + if (!loadView || !documentId) return; try { setDoc(null); - const doc = await documentService.openDocument(documentId); + const doc = await loadView(documentId); console.log('doc', doc); setDoc(doc); @@ -26,7 +27,7 @@ export function DatabaseRowSubDocument({ rowId }: { rowId: string }) { console.error(e); // haven't created by client, ignore error and show empty } - }, [documentService, documentId]); + }, [loadView, documentId]); useEffect(() => { setLoading(true); @@ -43,7 +44,16 @@ export function DatabaseRowSubDocument({ rowId }: { rowId: string }) { if (!doc) return null; - return ; + return ( + + ); } export default DatabaseRowSubDocument; diff --git a/frontend/appflowy_web_app/src/components/database/components/header/DatabaseHeader.tsx b/frontend/appflowy_web_app/src/components/database/components/header/DatabaseHeader.tsx deleted file mode 100644 index f84da67aa2..0000000000 --- a/frontend/appflowy_web_app/src/components/database/components/header/DatabaseHeader.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { usePageInfo } from '@/components/_shared/page/usePageInfo'; -import Title from './Title'; -import React from 'react'; - -export function DatabaseHeader({ viewId }: { viewId: string }) { - const { name, icon } = usePageInfo(viewId); - - return ; -} - -export default DatabaseHeader; diff --git a/frontend/appflowy_web_app/src/components/database/components/header/DatabaseRowHeader.tsx b/frontend/appflowy_web_app/src/components/database/components/header/DatabaseRowHeader.tsx index 330e5ba1cf..1cffe320fd 100644 --- a/frontend/appflowy_web_app/src/components/database/components/header/DatabaseRowHeader.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/header/DatabaseRowHeader.tsx @@ -1,12 +1,9 @@ -import { useCellSelector, useDatabaseViewId, usePrimaryFieldId, useRowMetaSelector } from '@/application/database-yjs'; -import { FolderContext } from '@/application/folder-yjs'; +import { useCellSelector, usePrimaryFieldId, useRowMetaSelector } from '@/application/database-yjs'; import Title from '@/components/database/components/header/Title'; -import React, { useContext, useEffect } from 'react'; +import React from 'react'; function DatabaseRowHeader({ rowId }: { rowId: string }) { const fieldId = usePrimaryFieldId() || ''; - const setCrumbs = useContext(FolderContext)?.setCrumbs; - const viewId = useDatabaseViewId(); const meta = useRowMetaSelector(rowId); const cell = useCellSelector({ @@ -14,22 +11,6 @@ function DatabaseRowHeader({ rowId }: { rowId: string }) { fieldId, }); - useEffect(() => { - if (!viewId) return; - setCrumbs?.((prev) => { - const lastCrumb = prev[prev.length - 1]; - const crumb = { - viewId, - rowId, - name: cell?.data as string, - icon: meta?.icon || '', - }; - - if (lastCrumb?.rowId === rowId) return [...prev.slice(0, -1), crumb]; - return [...prev, crumb]; - }); - }, [cell, meta, rowId, setCrumbs, viewId]); - return <Title icon={meta?.icon} name={cell?.data as string} />; } diff --git a/frontend/appflowy_web_app/src/components/database/components/header/index.ts b/frontend/appflowy_web_app/src/components/database/components/header/index.ts index 452eceafe1..53e50ae7af 100644 --- a/frontend/appflowy_web_app/src/components/database/components/header/index.ts +++ b/frontend/appflowy_web_app/src/components/database/components/header/index.ts @@ -1,2 +1 @@ -export * from './DatabaseHeader'; export * from './DatabaseRowHeader'; diff --git a/frontend/appflowy_web_app/src/components/database/components/tabs/DatabaseTabs.tsx b/frontend/appflowy_web_app/src/components/database/components/tabs/DatabaseTabs.tsx index 881f3d91df..7b52d96a84 100644 --- a/frontend/appflowy_web_app/src/components/database/components/tabs/DatabaseTabs.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/tabs/DatabaseTabs.tsx @@ -1,16 +1,14 @@ -import { DatabaseViewLayout, ViewLayout, YjsDatabaseKey, YjsFolderKey, YView } from '@/application/collab.type'; -import { useDatabaseView } from '@/application/database-yjs'; -import { useFolderContext } from '@/application/folder-yjs'; +import { DatabaseViewLayout, YDatabaseView, YjsDatabaseKey } from '@/application/collab.type'; +import { useDatabase, useDatabaseView } from '@/application/database-yjs'; import { DatabaseActions } from '@/components/database/components/conditions'; import { Tooltip } from '@mui/material'; -import { forwardRef, FunctionComponent, SVGProps, useCallback, useMemo } from 'react'; +import { forwardRef, FunctionComponent, SVGProps, useMemo } from 'react'; import { ViewTabs, ViewTab } from './ViewTabs'; import { useTranslation } from 'react-i18next'; import { ReactComponent as GridSvg } from '$icons/16x/grid.svg'; import { ReactComponent as BoardSvg } from '$icons/16x/board.svg'; import { ReactComponent as CalendarSvg } from '$icons/16x/date.svg'; -import { ReactComponent as DocumentSvg } from '$icons/16x/document.svg'; export interface DatabaseTabBarProps { viewIds: string[]; @@ -19,33 +17,24 @@ export interface DatabaseTabBarProps { } const DatabaseIcons: { - [key in ViewLayout]: FunctionComponent<SVGProps<SVGSVGElement> & { title?: string | undefined }>; + [key in DatabaseViewLayout]: FunctionComponent<SVGProps<SVGSVGElement> & { title?: string | undefined }>; } = { - [ViewLayout.Document]: DocumentSvg, - [ViewLayout.Grid]: GridSvg, - [ViewLayout.Board]: BoardSvg, - [ViewLayout.Calendar]: CalendarSvg, + [DatabaseViewLayout.Grid]: GridSvg, + [DatabaseViewLayout.Board]: BoardSvg, + [DatabaseViewLayout.Calendar]: CalendarSvg, }; export const DatabaseTabs = forwardRef<HTMLDivElement, DatabaseTabBarProps>( ({ viewIds, selectedViewId, setSelectedViewId }, ref) => { const { t } = useTranslation(); - const folder = useFolderContext(); const view = useDatabaseView(); + const views = useDatabase().get(YjsDatabaseKey.views); const layout = Number(view?.get(YjsDatabaseKey.layout)) as DatabaseViewLayout; const handleChange = (_: React.SyntheticEvent, newValue: string) => { setSelectedViewId?.(newValue); }; - const getFolderView = useCallback( - (viewId: string) => { - if (!folder) return null; - return folder.get(YjsFolderKey.views)?.get(viewId) as YView | null; - }, - [folder] - ); - const className = useMemo(() => { const classList = [ 'mx-16 -mb-[0.5px] flex items-center overflow-hidden border-line-divider text-text-title max-md:mx-4', @@ -75,12 +64,12 @@ export const DatabaseTabs = forwardRef<HTMLDivElement, DatabaseTabBarProps>( onChange={handleChange} > {viewIds.map((viewId) => { - const view = getFolderView(viewId); + const view = views?.get(viewId) as YDatabaseView | null; if (!view) return null; - const layout = Number(view.get(YjsFolderKey.layout)) as ViewLayout; + const layout = Number(view.get(YjsDatabaseKey.layout)) as DatabaseViewLayout; const Icon = DatabaseIcons[layout]; - const name = view.get(YjsFolderKey.name); + const name = view.get(YjsDatabaseKey.name); return ( <ViewTab diff --git a/frontend/appflowy_web_app/src/components/document/Document.tsx b/frontend/appflowy_web_app/src/components/document/Document.tsx index 8809cffee3..15188e2ba2 100644 --- a/frontend/appflowy_web_app/src/components/document/Document.tsx +++ b/frontend/appflowy_web_app/src/components/document/Document.tsx @@ -1,112 +1,43 @@ import { YDoc } from '@/application/collab.type'; -import { useId } from '@/components/_shared/context-provider/IdProvider'; -import { usePageInfo } from '@/components/_shared/page/usePageInfo'; +import { ViewMeta } from '@/application/db/tables/view_metas'; import ComponentLoading from '@/components/_shared/progress/ComponentLoading'; -import { AFConfigContext } from '@/components/app/AppConfig'; -import { DocumentHeader } from '@/components/document/document_header'; import { Editor } from '@/components/editor'; -import { EditorLayoutStyle } from '@/components/editor/EditorContext'; -import { Log } from '@/utils/log'; -import CircularProgress from '@mui/material/CircularProgress'; -import React, { Suspense, useCallback, useContext, useEffect, useMemo, useState } from 'react'; -import RecordNotFound from 'src/components/_shared/not-found/RecordNotFound'; +import React, { Suspense } from 'react'; +import ViewMetaPreview, { ViewMetaProps } from '@/components/view-meta/ViewMetaPreview'; +import Y from 'yjs'; -export const Document = () => { - const { objectId: documentId } = useId() || {}; - const [doc, setDoc] = useState<YDoc | null>(null); - const [notFound, setNotFound] = useState<boolean>(false); - const extra = usePageInfo(documentId).extra; - - const layoutStyle: EditorLayoutStyle = useMemo(() => { - return { - font: extra?.font || '', - fontLayout: extra?.fontLayout, - lineHeightLayout: extra?.lineHeightLayout, - }; - }, [extra]); - const documentService = useContext(AFConfigContext)?.service?.documentService; - - const handleOpenDocument = useCallback(async () => { - if (!documentService || !documentId) return; - try { - setDoc(null); - const doc = await documentService.openDocument(documentId); - - setDoc(doc); - } catch (e) { - Log.error(e); - setNotFound(true); - } - }, [documentService, documentId]); - - useEffect(() => { - setNotFound(false); - void handleOpenDocument(); - }, [handleOpenDocument]); - - const style = useMemo(() => { - const fontSizeMap = { - small: '14px', - normal: '16px', - large: '20px', - }; - - return { - fontFamily: layoutStyle.font, - fontSize: fontSizeMap[layoutStyle.fontLayout], - }; - }, [layoutStyle]); - - const layoutClassName = useMemo(() => { - const classList = []; - - if (layoutStyle.fontLayout === 'large') { - classList.push('font-large'); - } else if (layoutStyle.fontLayout === 'small') { - classList.push('font-small'); - } - - if (layoutStyle.lineHeightLayout === 'large') { - classList.push('line-height-large'); - } else if (layoutStyle.lineHeightLayout === 'small') { - classList.push('line-height-small'); - } - - return classList.join(' '); - }, [layoutStyle]); - - useEffect(() => { - if (!layoutStyle.font) return; - void window.WebFont?.load({ - google: { - families: [layoutStyle.font], - }, - }); - }, [layoutStyle.font]); - - if (!documentId) return null; +export interface DocumentProps extends ViewMetaProps { + doc: YDoc; + navigateToView?: (viewId: string) => Promise<void>; + loadViewMeta?: (viewId: string) => Promise<ViewMeta>; + loadView?: (viewId: string) => Promise<YDoc>; + getViewRowsMap?: (viewId: string, rowIds: string[]) => Promise<{ rows: Y.Map<YDoc>; destroy: () => void }>; +} +export const Document = ({ + doc, + loadView, + navigateToView, + loadViewMeta, + getViewRowsMap, + ...viewMeta +}: DocumentProps) => { return ( - <> - {doc ? ( - <div style={style} className={`relative w-full ${layoutClassName}`}> - <DocumentHeader doc={doc} viewId={documentId} /> - <div className={'flex w-full justify-center'}> - <Suspense fallback={<ComponentLoading />}> - <div className={'max-w-screen w-[964px] min-w-0'}> - <Editor doc={doc} readOnly={true} layoutStyle={layoutStyle} /> - </div> - </Suspense> - </div> + <div className={'mb-16 flex h-full w-full flex-col items-center justify-center'}> + <ViewMetaPreview {...viewMeta} /> + <Suspense fallback={<ComponentLoading />}> + <div className={'mx-16 w-[964px] min-w-0 max-w-full'}> + <Editor + loadView={loadView} + loadViewMeta={loadViewMeta} + navigateToView={navigateToView} + getViewRowsMap={getViewRowsMap} + doc={doc} + readOnly={true} + /> </div> - ) : ( - <div className={'flex h-full w-full items-center justify-center'}> - <CircularProgress /> - </div> - )} - - <RecordNotFound open={notFound} /> - </> + </Suspense> + </div> ); }; diff --git a/frontend/appflowy_web_app/src/components/document/document_header/DocumentCover.tsx b/frontend/appflowy_web_app/src/components/document/document_header/DocumentCover.tsx deleted file mode 100644 index 1d01474622..0000000000 --- a/frontend/appflowy_web_app/src/components/document/document_header/DocumentCover.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { showColorsForImage } from '@/components/document/document_header/utils'; -import { renderColor } from '@/utils/color'; -import React, { useCallback } from 'react'; - -function DocumentCover({ - coverValue, - coverType, - onTextColor, -}: { - coverValue?: string; - coverType?: string; - onTextColor: (color: string) => void; -}) { - const renderCoverColor = useCallback((color: string) => { - return ( - <div - style={{ - background: renderColor(color), - }} - className={`h-full w-full`} - /> - ); - }, []); - - const renderCoverImage = useCallback( - (url: string) => { - return ( - <img - onLoad={(e) => { - void showColorsForImage(e.currentTarget).then((res) => { - onTextColor(res); - }); - }} - draggable={false} - src={url} - alt={''} - className={'h-full w-full object-cover'} - /> - ); - }, - [onTextColor] - ); - - if (!coverType || !coverValue) { - return null; - } - - return ( - <div className={'relative flex h-[255px] w-full max-sm:h-[180px]'}> - {coverType === 'color' && renderCoverColor(coverValue)} - {(coverType === 'custom' || coverType === 'built_in') && renderCoverImage(coverValue)} - </div> - ); -} - -export default DocumentCover; diff --git a/frontend/appflowy_web_app/src/components/document/document_header/DocumentHeader.tsx b/frontend/appflowy_web_app/src/components/document/document_header/DocumentHeader.tsx deleted file mode 100644 index 04201f5ce5..0000000000 --- a/frontend/appflowy_web_app/src/components/document/document_header/DocumentHeader.tsx +++ /dev/null @@ -1,100 +0,0 @@ -import { DocCoverType, YDoc, YjsFolderKey } from '@/application/collab.type'; -import { useViewSelector } from '@/application/folder-yjs'; -import { CoverType } from '@/application/folder-yjs/folder.type'; -import { usePageInfo } from '@/components/_shared/page/usePageInfo'; -import DocumentCover from '@/components/document/document_header/DocumentCover'; -import { useBlockCover } from '@/components/document/document_header/useBlockCover'; -import React, { memo, useMemo, useRef, useState } from 'react'; -import BuiltInImage1 from '@/assets/cover/m_cover_image_1.png'; -import BuiltInImage2 from '@/assets/cover/m_cover_image_2.png'; -import BuiltInImage3 from '@/assets/cover/m_cover_image_3.png'; -import BuiltInImage4 from '@/assets/cover/m_cover_image_4.png'; -import BuiltInImage5 from '@/assets/cover/m_cover_image_5.png'; -import BuiltInImage6 from '@/assets/cover/m_cover_image_6.png'; - -export function DocumentHeader({ viewId, doc }: { viewId: string; doc: YDoc }) { - const ref = useRef<HTMLDivElement>(null); - const { view } = useViewSelector(viewId); - const [textColor, setTextColor] = useState<string>('var(--text-title)'); - const icon = view?.get(YjsFolderKey.icon); - const iconObject = useMemo(() => { - try { - return JSON.parse(icon || ''); - } catch (e) { - return null; - } - }, [icon]); - - const { extra } = usePageInfo(viewId); - - const pageCover = extra.cover; - const { cover } = useBlockCover(doc); - - const coverType = useMemo(() => { - if ( - (pageCover && [CoverType.NormalColor, CoverType.GradientColor].includes(pageCover.type)) || - cover?.cover_selection_type === DocCoverType.Color - ) { - return 'color'; - } - - if (CoverType.BuildInImage === pageCover?.type || cover?.cover_selection_type === DocCoverType.Asset) { - return 'built_in'; - } - - if ( - (pageCover && [CoverType.CustomImage, CoverType.UpsplashImage].includes(pageCover.type)) || - cover?.cover_selection_type === DocCoverType.Image - ) { - return 'custom'; - } - }, [cover?.cover_selection_type, pageCover]); - - const coverValue = useMemo(() => { - if (coverType === 'built_in') { - return { - 1: BuiltInImage1, - 2: BuiltInImage2, - 3: BuiltInImage3, - 4: BuiltInImage4, - 5: BuiltInImage5, - 6: BuiltInImage6, - }[pageCover?.value as string]; - } - - return pageCover?.value || cover?.cover_selection; - }, [coverType, cover?.cover_selection, pageCover]); - - return ( - <div ref={ref} className={'document-header mb-[10px] select-none'}> - <div className={'view-banner relative flex w-full flex-col overflow-hidden'}> - <DocumentCover onTextColor={setTextColor} coverType={coverType} coverValue={coverValue} /> - - <div className={`relative mx-16 w-[964px] min-w-0 max-w-full overflow-visible max-md:mx-4`}> - <div - style={{ - position: coverValue ? 'absolute' : 'relative', - bottom: '100%', - width: '100%', - }} - className={'flex items-center gap-2 px-14 py-8 text-4xl max-md:px-2 max-sm:text-[7vw]'} - > - <div className={`view-icon`}>{iconObject?.value}</div> - <div className={'flex flex-1 items-center gap-2 overflow-hidden'}> - <div - style={{ - color: textColor, - }} - className={'font-bold leading-[1.5em]'} - > - {view?.get(YjsFolderKey.name)} - </div> - </div> - </div> - </div> - </div> - </div> - ); -} - -export default memo(DocumentHeader); diff --git a/frontend/appflowy_web_app/src/components/document/document_header/index.ts b/frontend/appflowy_web_app/src/components/document/document_header/index.ts deleted file mode 100644 index 00f48716bf..0000000000 --- a/frontend/appflowy_web_app/src/components/document/document_header/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './DocumentHeader'; diff --git a/frontend/appflowy_web_app/src/components/document/document_header/useBlockCover.ts b/frontend/appflowy_web_app/src/components/document/document_header/useBlockCover.ts deleted file mode 100644 index ba6226a6e8..0000000000 --- a/frontend/appflowy_web_app/src/components/document/document_header/useBlockCover.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { DocCover, YBlocks, YDoc, YDocument, YjsEditorKey } from '@/application/collab.type'; -import { useEffect, useMemo, useState } from 'react'; - -export function useBlockCover(doc: YDoc) { - const [cover, setCover] = useState<string | null>(null); - - useEffect(() => { - if (!doc) return; - - const document = doc.getMap(YjsEditorKey.data_section).get(YjsEditorKey.document) as YDocument; - const pageId = document.get(YjsEditorKey.page_id) as string; - const blocks = document.get(YjsEditorKey.blocks) as YBlocks; - const root = blocks.get(pageId); - - setCover(root.toJSON().data || null); - const observerEvent = () => setCover(root.toJSON().data || null); - - root.observe(observerEvent); - - return () => { - root.unobserve(observerEvent); - }; - }, [doc]); - - const coverObj: DocCover = useMemo(() => { - try { - return JSON.parse(cover || ''); - } catch (e) { - return null; - } - }, [cover]); - - return { - cover: coverObj, - }; -} diff --git a/frontend/appflowy_web_app/src/components/document/document_header/utils.ts b/frontend/appflowy_web_app/src/components/document/document_header/utils.ts deleted file mode 100644 index fe2c0acbe0..0000000000 --- a/frontend/appflowy_web_app/src/components/document/document_header/utils.ts +++ /dev/null @@ -1,28 +0,0 @@ -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-expect-error -import ColorThief from 'colorthief'; - -const colorThief = new ColorThief(); - -export function calculateTextColor(rgb: [number, number, number]): string { - const [r, g, b] = rgb; - const brightness = (r * 299 + g * 587 + b * 114) / 1000; - - return brightness > 125 ? 'black' : 'white'; -} - -export async function showColorsForImage(image: HTMLImageElement) { - const img = new Image(); - - img.crossOrigin = 'Anonymous'; // Handle CORS - img.src = image.src; - - await new Promise((resolve, reject) => { - img.onload = resolve; - img.onerror = reject; - }); - - const dominantColor = colorThief.getColor(img); - - return calculateTextColor(dominantColor); -} diff --git a/frontend/appflowy_web_app/src/components/editor/Editable.tsx b/frontend/appflowy_web_app/src/components/editor/Editable.tsx index 2ba4e83e3a..81929bd364 100644 --- a/frontend/appflowy_web_app/src/components/editor/Editable.tsx +++ b/frontend/appflowy_web_app/src/components/editor/Editable.tsx @@ -1,36 +1,42 @@ import { useDecorate } from '@/components/editor/components/blocks/code/useDecorate'; import { Leaf } from '@/components/editor/components/leaf'; import { useEditorContext } from '@/components/editor/EditorContext'; -import React, { useCallback } from 'react'; +import React, { Suspense, useCallback } from 'react'; import { NodeEntry } from 'slate'; import { Editable, ReactEditor, RenderElementProps } from 'slate-react'; import { Element } from './components/element'; +import { Skeleton } from '@mui/material'; const EditorEditable = ({ editor }: { editor: ReactEditor }) => { const { readOnly } = useEditorContext(); const codeDecorate = useDecorate(editor); - const decorate = useCallback( - (entry: NodeEntry) => { - return [...codeDecorate(entry)]; - }, - [codeDecorate] - ); - - const renderElement = useCallback((props: RenderElementProps) => <Element {...props} />, []); + const renderElement = useCallback((props: RenderElementProps) => { + return ( + <Suspense fallback={<Skeleton width={'100%'} height={24} />}> + <Element {...props} /> + </Suspense> + ); + }, []); return ( - <Editable - role={'textbox'} - decorate={decorate} - className={'px-16 outline-none focus:outline-none max-md:px-4'} - renderLeaf={Leaf} - renderElement={renderElement} - readOnly={readOnly} - spellCheck={false} - autoCorrect={'off'} - autoComplete={'off'} - /> + <> + <Editable + role={'textbox'} + decorate={(entry: NodeEntry) => { + const decoration = codeDecorate?.(entry); + + return decoration || []; + }} + className={'px-16 outline-none focus:outline-none max-md:px-4'} + renderLeaf={Leaf} + renderElement={renderElement} + readOnly={readOnly} + spellCheck={false} + autoCorrect={'off'} + autoComplete={'off'} + /> + </> ); }; diff --git a/frontend/appflowy_web_app/src/components/editor/Editor.cy.tsx b/frontend/appflowy_web_app/src/components/editor/Editor.cy.tsx index 7c6ec9a55e..f3eb62c9d3 100644 --- a/frontend/appflowy_web_app/src/components/editor/Editor.cy.tsx +++ b/frontend/appflowy_web_app/src/components/editor/Editor.cy.tsx @@ -1,7 +1,6 @@ -import { YDoc, YFolder, YjsEditorKey } from '@/application/collab.type'; +import { YDoc } from '@/application/collab.type'; import { DocumentTest } from '@/../cypress/support/document'; import { applyYDoc } from '@/application/ydoc/apply'; -import { FolderProvider } from '@/components/_shared/context-provider/FolderProvider'; import React from 'react'; import * as Y from 'yjs'; import { Editor } from './Editor'; @@ -20,39 +19,23 @@ describe('<Editor />', () => { }); it('renders with a full document', () => { - cy.mockDatabase(); Object.defineProperty(window.navigator, 'language', { value: 'en-US' }); Object.defineProperty(window.navigator, 'languages', { value: ['en-US'] }); - cy.fixture('folder').then((folderJson) => { + cy.fixture('full_doc').then((docJson) => { const doc = new Y.Doc(); - const state = new Uint8Array(folderJson.data.doc_state); + const state = new Uint8Array(docJson.data.doc_state); applyYDoc(doc, state); - - const folder = doc.getMap(YjsEditorKey.data_section).get(YjsEditorKey.folder) as YFolder; - - cy.fixture('full_doc').then((docJson) => { - const doc = new Y.Doc(); - const state = new Uint8Array(docJson.data.doc_state); - - applyYDoc(doc, state); - renderEditor(doc, folder); - }); + renderEditor(doc); }); }); }); -function renderEditor(doc: YDoc, folder?: YFolder) { +function renderEditor(doc: YDoc) { const AppWrapper = withAppWrapper(() => { return ( <div className={'h-screen w-screen overflow-y-auto'}> - {folder ? ( - <FolderProvider folder={folder}> - <Editor doc={doc} readOnly /> - </FolderProvider> - ) : ( - <Editor doc={doc} readOnly /> - )} + <Editor doc={doc} readOnly /> </div> ); }); diff --git a/frontend/appflowy_web_app/src/components/editor/Editor.tsx b/frontend/appflowy_web_app/src/components/editor/Editor.tsx index 183aed8918..421dc4b689 100644 --- a/frontend/appflowy_web_app/src/components/editor/Editor.tsx +++ b/frontend/appflowy_web_app/src/components/editor/Editor.tsx @@ -1,18 +1,27 @@ import { YDoc } from '@/application/collab.type'; import CollaborativeEditor from '@/components/editor/CollaborativeEditor'; -import { defaultLayoutStyle, EditorContextProvider, EditorLayoutStyle } from '@/components/editor/EditorContext'; +import { defaultLayoutStyle, EditorContextProvider, EditorContextState } from '@/components/editor/EditorContext'; import React, { memo } from 'react'; import './editor.scss'; -export interface EditorProps { - readOnly: boolean; +export interface EditorProps extends EditorContextState { doc: YDoc; - layoutStyle?: EditorLayoutStyle; } -export const Editor = memo(({ readOnly, doc, layoutStyle = defaultLayoutStyle }: EditorProps) => { +export const Editor = memo(({ doc, layoutStyle = defaultLayoutStyle, ...props }: EditorProps) => { + const [codeGrammars, setCodeGrammars] = React.useState<Record<string, string>>({}); + + const handleAddCodeGrammars = React.useCallback((blockId: string, grammar: string) => { + setCodeGrammars((prev) => ({ ...prev, [blockId]: grammar })); + }, []); + return ( - <EditorContextProvider layoutStyle={layoutStyle} readOnly={readOnly}> + <EditorContextProvider + {...props} + codeGrammars={codeGrammars} + addCodeGrammars={handleAddCodeGrammars} + layoutStyle={layoutStyle} + > <CollaborativeEditor doc={doc} /> </EditorContextProvider> ); diff --git a/frontend/appflowy_web_app/src/components/editor/EditorContext.tsx b/frontend/appflowy_web_app/src/components/editor/EditorContext.tsx index c360c969dd..5b8eb3431d 100644 --- a/frontend/appflowy_web_app/src/components/editor/EditorContext.tsx +++ b/frontend/appflowy_web_app/src/components/editor/EditorContext.tsx @@ -1,5 +1,7 @@ -import { FontLayout, LineHeightLayout } from '@/application/collab.type'; +import { FontLayout, LineHeightLayout, YDoc } from '@/application/collab.type'; +import { ViewMeta } from '@/application/db/tables/view_metas'; import { createContext, useContext } from 'react'; +import Y from 'yjs'; export interface EditorLayoutStyle { fontLayout: FontLayout; @@ -13,14 +15,21 @@ export const defaultLayoutStyle: EditorLayoutStyle = { lineHeightLayout: LineHeightLayout.normal, }; -interface EditorContextState { +export interface EditorContextState { readOnly: boolean; - layoutStyle: EditorLayoutStyle; + layoutStyle?: EditorLayoutStyle; + codeGrammars?: Record<string, string>; + addCodeGrammars?: (blockId: string, grammar: string) => void; + navigateToView?: (viewId: string) => Promise<void>; + loadViewMeta?: (viewId: string) => Promise<ViewMeta>; + loadView?: (viewId: string) => Promise<YDoc>; + getViewRowsMap?: (viewId: string, rowIds: string[]) => Promise<{ rows: Y.Map<YDoc>; destroy: () => void }>; } export const EditorContext = createContext<EditorContextState>({ readOnly: true, layoutStyle: defaultLayoutStyle, + codeGrammars: {}, }); export const EditorContextProvider = ({ children, ...props }: EditorContextState & { children: React.ReactNode }) => { diff --git a/frontend/appflowy_web_app/src/components/editor/components/block-actions/RightTopActions.tsx b/frontend/appflowy_web_app/src/components/editor/components/block-actions/RightTopActions.tsx new file mode 100644 index 0000000000..b9681f2f55 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/editor/components/block-actions/RightTopActions.tsx @@ -0,0 +1,20 @@ +import { IconButton, Tooltip } from '@mui/material'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { ReactComponent as CopyIcon } from '@/assets/copy.svg'; + +function RightTopActions({ onCopy }: { onCopy: () => void }) { + const { t } = useTranslation(); + + return ( + <div className={'flex w-fit flex-grow transform items-center justify-end gap-2 rounded bg-bg-body shadow-lg'}> + <Tooltip title={t('editor.copy')}> + <IconButton onClick={onCopy}> + <CopyIcon className={'h-6 w-6'} /> + </IconButton> + </Tooltip> + </div> + ); +} + +export default RightTopActions; diff --git a/frontend/appflowy_web_app/src/components/editor/components/block-actions/RightTopActionsToolbar.tsx b/frontend/appflowy_web_app/src/components/editor/components/block-actions/RightTopActionsToolbar.tsx new file mode 100644 index 0000000000..e953d8c87e --- /dev/null +++ b/frontend/appflowy_web_app/src/components/editor/components/block-actions/RightTopActionsToolbar.tsx @@ -0,0 +1,14 @@ +import RightTopActions from '@/components/editor/components/block-actions/RightTopActions'; +import React, { useRef } from 'react'; + +function RightTopActionsToolbar({ onCopy, style }: { onCopy: () => void; style?: React.CSSProperties }) { + const ref = useRef<HTMLDivElement | null>(null); + + return ( + <div ref={ref} style={style} contentEditable={false} className={`block-actions absolute right-2 top-2 z-10`}> + <RightTopActions onCopy={onCopy} /> + </div> + ); +} + +export default RightTopActionsToolbar; diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/bulleted-list/BulletedListIcon.tsx b/frontend/appflowy_web_app/src/components/editor/components/blocks/bulleted-list/BulletedListIcon.tsx index 62e06b6ba9..3fee74bfad 100644 --- a/frontend/appflowy_web_app/src/components/editor/components/blocks/bulleted-list/BulletedListIcon.tsx +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/bulleted-list/BulletedListIcon.tsx @@ -2,6 +2,9 @@ import { BulletedListNode } from '@/components/editor/editor.type'; import { getListLevel } from '@/components/editor/utils/list'; import React, { useMemo } from 'react'; import { ReactEditor, useSlateStatic } from 'slate-react'; +import { ReactComponent as DiscIcon } from '@/assets/bulleted_list_icon_1.svg'; +import { ReactComponent as CircleIcon } from '@/assets/bulleted_list_icon_2.svg'; +import { ReactComponent as SquareIcon } from '@/assets/bulleted_list_icon_3.svg'; enum Letter { Disc, @@ -25,25 +28,27 @@ export function BulletedListIcon({ block, className }: { block: BulletedListNode } }, [block.type, staticEditor, path]); - const dataLetter = useMemo(() => { + const Icon = useMemo(() => { switch (letter) { case Letter.Disc: - return '•'; + return DiscIcon; case Letter.Circle: - return '◦'; + return CircleIcon; case Letter.Square: - return '▪'; + return SquareIcon; } }, [letter]); return ( <span + data-playwright-selected={false} + contentEditable={false} onMouseDown={(e) => { e.preventDefault(); }} - data-letter={dataLetter} - contentEditable={false} - className={`${className} bulleted-icon flex min-w-[24px] justify-center pr-1 font-medium`} - /> + className={`${className} pr-1 text-xl`} + > + <Icon /> + </span> ); } diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/callout/CalloutIcon.tsx b/frontend/appflowy_web_app/src/components/editor/components/blocks/callout/CalloutIcon.tsx index 0c72c971d8..11fef2d45a 100644 --- a/frontend/appflowy_web_app/src/components/editor/components/blocks/callout/CalloutIcon.tsx +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/callout/CalloutIcon.tsx @@ -6,7 +6,7 @@ function CalloutIcon({ node }: { node: CalloutNode }) { return ( <> - <span contentEditable={false} ref={ref} className={`flex h-8 w-8 items-center p-1`}> + <span contentEditable={false} ref={ref} className={`icon flex h-8 w-8 items-center p-1`}> {node.data.icon} </span> </> diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/code/Code.hooks.ts b/frontend/appflowy_web_app/src/components/editor/components/blocks/code/Code.hooks.ts index b7bb3500af..8f3b3f99de 100644 --- a/frontend/appflowy_web_app/src/components/editor/components/blocks/code/Code.hooks.ts +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/code/Code.hooks.ts @@ -1,11 +1,43 @@ import { CodeNode } from '@/components/editor/editor.type'; -import { useCallback } from 'react'; +import { useEditorContext } from '@/components/editor/EditorContext'; +import { useCallback, useEffect } from 'react'; import { ReactEditor, useSlateStatic } from 'slate-react'; import { Element as SlateElement, Transforms } from 'slate'; +const Prism = window.Prism; +const hljs = window.hljs; + export function useCodeBlock(node: CodeNode) { const language = node.data.language; const editor = useSlateStatic() as ReactEditor; + + const addCodeGrammars = useEditorContext().addCodeGrammars; + + useEffect(() => { + const path = ReactEditor.findPath(editor, node); + let detectedLanguage = language; + + if (!language) { + const codeSnippet = editor.string(path); + + detectedLanguage = hljs.highlightAuto(codeSnippet).language; + } + + const prismLanguage = Prism.languages[detectedLanguage.toLowerCase()]; + + if (!prismLanguage) { + const script = document.createElement('script'); + + script.src = `https://cdnjs.cloudflare.com/ajax/libs/prism/1.26.0/components/prism-${detectedLanguage.toLowerCase()}.min.js`; + document.head.appendChild(script); + script.onload = () => { + addCodeGrammars?.(node.blockId, detectedLanguage); + }; + } else { + addCodeGrammars?.(node.blockId, detectedLanguage); + } + }, [addCodeGrammars, editor, language, node]); + const handleChangeLanguage = useCallback( (newLang: string) => { const path = ReactEditor.findPath(editor, node); diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/code/Code.tsx b/frontend/appflowy_web_app/src/components/editor/components/blocks/code/Code.tsx index 5ef2279da3..b26d735db5 100644 --- a/frontend/appflowy_web_app/src/components/editor/components/blocks/code/Code.tsx +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/code/Code.tsx @@ -1,27 +1,60 @@ +import { notify } from '@/components/_shared/notify'; +import RightTopActionsToolbar from '@/components/editor/components/block-actions/RightTopActionsToolbar'; import { useCodeBlock } from '@/components/editor/components/blocks/code/Code.hooks'; import { CodeNode, EditorElementProps } from '@/components/editor/editor.type'; -import { forwardRef, memo } from 'react'; +import { copyTextToClipboard } from '@/utils/copy'; +import React, { forwardRef, memo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { ReactEditor, useSlateStatic } from 'slate-react'; import LanguageSelect from './SelectLanguage'; export const CodeBlock = memo( forwardRef<HTMLDivElement, EditorElementProps<CodeNode>>(({ node, children, ...attributes }, ref) => { const { language, handleChangeLanguage } = useCodeBlock(node); + const [showToolbar, setShowToolbar] = useState(false); + const { t } = useTranslation(); + const editor = useSlateStatic(); return ( - <> - <div contentEditable={false} className={'absolute mt-2 flex h-20 w-full select-none items-center px-6'}> + <div + className={'relative w-full'} + onMouseEnter={() => { + setShowToolbar(true); + }} + onMouseLeave={() => setShowToolbar(false)} + > + <div contentEditable={false} className={'absolute mt-2 flex h-20 w-full select-none items-center px-6'}> <LanguageSelect readOnly language={language} onChangeLanguage={handleChangeLanguage} /> </div> <div {...attributes} ref={ref} className={`${attributes.className ?? ''} flex w-full bg-bg-body py-2`}> <pre spellCheck={false} - className={`flex w-full rounded border border-line-divider bg-fill-list-active p-5 pt-20`} + className={`flex w-full overflow-hidden rounded border border-line-divider bg-fill-list-active p-5 pt-20`} > <code>{children}</code> </pre> </div> - </> + {showToolbar && ( + <RightTopActionsToolbar + style={{ + top: '16px', + }} + onCopy={async () => { + try { + const at = ReactEditor.findPath(editor, node); + const text = editor.string(at); + + await copyTextToClipboard(text); + notify.success(t('publish.copy.codeBlock')); + } catch (_) { + // do nothing + } + }} + /> + )} + </div> ); }), (prevProps, nextProps) => JSON.stringify(prevProps.node) === JSON.stringify(nextProps.node) ); +export default CodeBlock; diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/code/SelectLanguage.tsx b/frontend/appflowy_web_app/src/components/editor/components/blocks/code/SelectLanguage.tsx index f249a19951..c3c05ef36f 100644 --- a/frontend/appflowy_web_app/src/components/editor/components/blocks/code/SelectLanguage.tsx +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/code/SelectLanguage.tsx @@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next'; function SelectLanguage({ readOnly, - language = 'json', + language = 'Auto', }: { readOnly?: boolean; language: string; diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/code/useDecorate.ts b/frontend/appflowy_web_app/src/components/editor/components/blocks/code/useDecorate.ts index 1ec1a2e980..c424934cb2 100644 --- a/frontend/appflowy_web_app/src/components/editor/components/blocks/code/useDecorate.ts +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/code/useDecorate.ts @@ -1,13 +1,17 @@ import { BlockType } from '@/application/collab.type'; -import { decorateCode } from '@/components/editor/components/blocks/code/utils'; +import { useEditorContext } from '@/components/editor/EditorContext'; +import { decorateCode } from './utils'; import { CodeNode } from '@/components/editor/editor.type'; -import { useCallback } from 'react'; +import { useMemo } from 'react'; import { BaseRange, Editor, NodeEntry, Element } from 'slate'; import { ReactEditor } from 'slate-react'; export function useDecorate(editor: ReactEditor) { - return useCallback( - (entry: NodeEntry): BaseRange[] => { + const grammars = useEditorContext().codeGrammars; + + return useMemo(() => { + return (entry: NodeEntry): BaseRange[] => { + if (!entry) return []; const path = entry[1]; const blockEntry = editor.above({ @@ -20,14 +24,11 @@ export function useDecorate(editor: ReactEditor) { const block = blockEntry[0] as CodeNode; - if (block.type === BlockType.CodeBlock) { - const language = block.data.language; - - return decorateCode(entry, language, false); + if (block.type === BlockType.CodeBlock && grammars?.[block.blockId]) { + return decorateCode(entry, grammars[block.blockId]); } return []; - }, - [editor] - ); + }; + }, [editor, grammars]); } diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/code/utils.ts b/frontend/appflowy_web_app/src/components/editor/components/blocks/code/utils.ts index 458d9e8d7b..12d5070c7b 100644 --- a/frontend/appflowy_web_app/src/components/editor/components/blocks/code/utils.ts +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/code/utils.ts @@ -1,46 +1,9 @@ -import Prism from 'prismjs'; - -import 'prismjs/components/prism-bash'; -import 'prismjs/components/prism-basic'; -import 'prismjs/components/prism-c'; -import 'prismjs/components/prism-clojure'; -import 'prismjs/components/prism-cpp'; -import 'prismjs/components/prism-csp'; -import 'prismjs/components/prism-css'; -import 'prismjs/components/prism-dart'; -import 'prismjs/components/prism-elixir'; -import 'prismjs/components/prism-elm'; -import 'prismjs/components/prism-erlang'; -import 'prismjs/components/prism-fortran'; -import 'prismjs/components/prism-go'; -import 'prismjs/components/prism-graphql'; -import 'prismjs/components/prism-haskell'; -import 'prismjs/components/prism-java'; -import 'prismjs/components/prism-javascript'; -import 'prismjs/components/prism-json'; -import 'prismjs/components/prism-kotlin'; -import 'prismjs/components/prism-lisp'; -import 'prismjs/components/prism-lua'; -import 'prismjs/components/prism-markdown'; -import 'prismjs/components/prism-matlab'; -import 'prismjs/components/prism-ocaml'; -import 'prismjs/components/prism-perl'; -import 'prismjs/components/prism-php'; -import 'prismjs/components/prism-powershell'; -import 'prismjs/components/prism-python'; -import 'prismjs/components/prism-r'; -import 'prismjs/components/prism-ruby'; -import 'prismjs/components/prism-rust'; -import 'prismjs/components/prism-scala'; -import 'prismjs/components/prism-shell-session'; -import 'prismjs/components/prism-sql'; -import 'prismjs/components/prism-swift'; -import 'prismjs/components/prism-typescript'; -import 'prismjs/components/prism-xml-doc'; -import 'prismjs/components/prism-yaml'; - import { BaseRange, NodeEntry, Text, Path } from 'slate'; +const Prism = window.Prism; + +Prism.plugins.autoloader.languages_path = 'https://cdnjs.cloudflare.com/ajax/libs/prism/1.26.0/components/'; + const push_string = ( token: string | Prism.Token, path: Path, @@ -112,7 +75,9 @@ function switchCodeTheme(isDark: boolean) { document.head.appendChild(newLink); } -export const decorateCode = ([node, path]: NodeEntry, language: string, isDark: boolean) => { +export const decorateCode = ([node, path]: NodeEntry, language: string) => { + const isDark = document.documentElement.getAttribute('data-dark-mode') === 'true'; + switchCodeTheme(isDark); const ranges: BaseRange[] = []; @@ -121,17 +86,21 @@ export const decorateCode = ([node, path]: NodeEntry, language: string, isDark: return ranges; } - try { - const tokens = Prism.tokenize(node.text, Prism.languages[language.toLowerCase()]); + const highlightCode = (code: string, language: string) => { + try { + const tokens = Prism.tokenize(code, language); - let start = 0; + let start = 0; - for (const token of tokens) { - start = recurseTokenize(token, path, ranges, start) || 0; + for (const token of tokens) { + start = recurseTokenize(token, path, ranges, start) || 0; + } + + return ranges; + } catch { + return ranges; } + }; - return ranges; - } catch { - return ranges; - } + return highlightCode(node.text, Prism.languages[language.toLowerCase()]); }; diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/database/DatabaseBlock.tsx b/frontend/appflowy_web_app/src/components/editor/components/blocks/database/DatabaseBlock.tsx index 1e6bbb151a..e5c36eed2d 100644 --- a/frontend/appflowy_web_app/src/components/editor/components/blocks/database/DatabaseBlock.tsx +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/database/DatabaseBlock.tsx @@ -1,25 +1,26 @@ import { ReactComponent as ExpandMoreIcon } from '$icons/16x/full_view.svg'; -import { useNavigateToView } from '@/application/folder-yjs'; -import { getCurrentWorkspace } from 'src/application/services/js-services/session'; -import { IdProvider } from '@/components/_shared/context-provider/IdProvider'; +import { useEditorContext } from '@/components/editor/EditorContext'; import { Database } from '@/components/database'; -import { useGetDatabaseId, useLoadDatabase } from '@/components/database/Database.hooks'; -import { DatabaseContextProvider } from '@/components/database/DatabaseContext'; import { DatabaseNode, EditorElementProps } from '@/components/editor/editor.type'; import { Tooltip } from '@mui/material'; import CircularProgress from '@mui/material/CircularProgress'; -import React, { forwardRef, memo, useCallback, useMemo, useState } from 'react'; +import React, { forwardRef, memo, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { BlockType } from '@/application/collab.type'; +import { BlockType, YDoc } from '@/application/collab.type'; export const DatabaseBlock = memo( forwardRef<HTMLDivElement, EditorElementProps<DatabaseNode>>(({ node, children, ...attributes }, ref) => { const { t } = useTranslation(); const viewId = node.data.view_id; const type = node.type; - const navigateToView = useNavigateToView(); + const navigateToView = useEditorContext()?.navigateToView; + const loadView = useEditorContext()?.loadView; + const getViewRowsMap = useEditorContext()?.getViewRowsMap; + const loadViewMeta = useEditorContext()?.loadViewMeta; + + const [notFound, setNotFound] = useState(false); + const [doc, setDoc] = useState<YDoc | null>(null); const [isHovering, setIsHovering] = useState(false); - const [databaseViewId, setDatabaseViewId] = useState<string | undefined>(viewId); const style = useMemo(() => { const style = {}; @@ -39,23 +40,22 @@ export const DatabaseBlock = memo( return style; }, [type]); - const handleNavigateToRow = useCallback( - async (rowId: string) => { - const workspace = await getCurrentWorkspace(); + useEffect(() => { + if (!viewId) return; + void (async () => { + try { + const view = await loadView?.(viewId); - if (!workspace) return; + if (!view) { + throw new Error('View not found'); + } - const url = `/view/${workspace.id}/${databaseViewId}?r=${rowId}`; - - window.open(url, '_blank'); - }, - [databaseViewId] - ); - const databaseId = useGetDatabaseId(viewId); - - const { doc, rows, notFound } = useLoadDatabase({ - databaseId, - }); + setDoc(view); + } catch (e) { + setNotFound(true); + } + })(); + }, [viewId, loadView]); return ( <> @@ -69,17 +69,15 @@ export const DatabaseBlock = memo( {children} </div> <div contentEditable={false} style={style} className={`container-bg relative flex w-full flex-col px-3`}> - {viewId && doc && rows ? ( - <IdProvider objectId={viewId}> - <DatabaseContextProvider - navigateToRow={handleNavigateToRow} - viewId={databaseViewId || viewId} - databaseDoc={doc} - rowDocMap={rows} - readOnly={true} - > - <Database iidIndex={viewId} viewId={databaseViewId || viewId} onNavigateToView={setDatabaseViewId} /> - </DatabaseContextProvider> + {viewId && doc ? ( + <> + <Database + doc={doc} + getViewRowsMap={getViewRowsMap} + loadView={loadView} + navigateToView={navigateToView} + loadViewMeta={loadViewMeta} + /> {isHovering && ( <div className={'absolute right-4 top-1'}> <Tooltip placement={'bottom'} title={t('tooltip.openAsPage')}> @@ -87,7 +85,7 @@ export const DatabaseBlock = memo( color={'primary'} className={'rounded border border-line-divider bg-bg-body p-1 hover:bg-fill-list-hover'} onClick={() => { - navigateToView?.(viewId); + void navigateToView?.(viewId); }} > <ExpandMoreIcon /> @@ -95,15 +93,16 @@ export const DatabaseBlock = memo( </Tooltip> </div> )} - </IdProvider> + </> ) : ( <div - className={'mt-[10%] flex h-full w-full flex-col items-center gap-2 px-16 text-text-caption max-md:px-4'} + className={ + 'flex h-full w-full flex-col items-center justify-center gap-2 rounded border border-line-divider bg-fill-list-active px-16 text-text-caption max-md:px-4' + } > {notFound ? ( <> - <div className={'text-sm font-medium'}>{t('document.plugins.database.noDataSource')}</div> - <div className={'text-xs'}>{t('grid.relation.noDatabaseSelected')}</div> + <div className={'text-base font-medium'}>{t('publish.databaseHasNotBeenPublished')}</div> </> ) : ( <CircularProgress /> diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/divider/DividerNode.tsx b/frontend/appflowy_web_app/src/components/editor/components/blocks/divider/DividerNode.tsx index 450f865b79..f3dff23aeb 100644 --- a/frontend/appflowy_web_app/src/components/editor/components/blocks/divider/DividerNode.tsx +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/divider/DividerNode.tsx @@ -1,17 +1,12 @@ import { EditorElementProps, DividerNode as DividerBlock } from '@/components/editor/editor.type'; import React, { forwardRef, memo, useMemo } from 'react'; -import { useSelected } from 'slate-react'; export const DividerNode = memo( forwardRef<HTMLDivElement, EditorElementProps<DividerBlock>>( ({ node: _node, children: children, ...attributes }, ref) => { - const selected = useSelected(); - const className = useMemo(() => { - return `${attributes.className ?? ''} divider-node relative w-full rounded ${ - selected ? 'bg-content-blue-100' : '' - }`; - }, [attributes.className, selected]); + return `${attributes.className ?? ''} divider-node relative w-full rounded`; + }, [attributes.className]); return ( <div {...attributes} className={className}> diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/heading/Heading.tsx b/frontend/appflowy_web_app/src/components/editor/components/blocks/heading/Heading.tsx index 8d4351a2d0..2aa530ad07 100644 --- a/frontend/appflowy_web_app/src/components/editor/components/blocks/heading/Heading.tsx +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/heading/Heading.tsx @@ -1,3 +1,4 @@ +import { HEADER_HEIGHT } from '@/components/publish/header'; import { getHeadingCssProperty } from './utils'; import { EditorElementProps, HeadingNode } from '@/components/editor/editor.type'; import React, { forwardRef, memo } from 'react'; @@ -10,7 +11,16 @@ export const Heading = memo( const className = `${attributes.className ?? ''} ${fontSizeCssProperty} level-${level}`; return ( - <div {...attributes} ref={ref} id={`heading-${node.blockId}`} className={className}> + <div + {...attributes} + ref={ref} + id={`heading-${node.blockId}`} + style={{ + scrollMarginTop: HEADER_HEIGHT, + ...attributes.style, + }} + className={className} + > {children} </div> ); diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/heading/utils.ts b/frontend/appflowy_web_app/src/components/editor/components/blocks/heading/utils.ts index 22fe53980a..d31200ae2e 100644 --- a/frontend/appflowy_web_app/src/components/editor/components/blocks/heading/utils.ts +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/heading/utils.ts @@ -1,17 +1,17 @@ export function getHeadingCssProperty(level: number) { switch (level) { case 1: - return 'text-3xl pt-[10px] max-md:pt-[1.5vw] pb-[4px] max-md:pb-[1vw] font-bold max-sm:text-[6vw]'; + return 'text-[1.75rem] pt-[10px] max-md:pt-[1.5vw] pb-[4px] max-md:pb-[1vw] font-bold max-sm:text-[6vw]'; case 2: - return 'text-2xl pt-[8px] max-md:pt-[1vw] pb-[2px] max-md:pb-[0.5vw] font-bold max-sm:text-[5vw]'; + return 'text-[1.55rem] pt-[8px] max-md:pt-[1vw] pb-[2px] max-md:pb-[0.5vw] font-bold max-sm:text-[5vw]'; case 3: - return 'text-xl pt-[4px] font-bold max-sm:text-[4vw]'; + return 'text-[1.35rem] pt-[4px] font-bold max-sm:text-[4vw]'; case 4: - return 'text-lg pt-[4px] font-bold'; + return 'text-[1.25rem] pt-[4px] font-bold'; case 5: - return 'pt-[4px] font-bold'; + return 'text-[1.15rem] pt-[4px] font-bold'; case 6: - return 'pt-[4px] font-bold'; + return 'text-[1.05rem] pt-[4px] font-bold'; default: return ''; } diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/image/ImageBlock.tsx b/frontend/appflowy_web_app/src/components/editor/components/blocks/image/ImageBlock.tsx index 50de92cc66..e0ff9218d1 100644 --- a/frontend/appflowy_web_app/src/components/editor/components/blocks/image/ImageBlock.tsx +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/image/ImageBlock.tsx @@ -1,6 +1,6 @@ import { AlignType } from '@/application/collab.type'; import { EditorElementProps, ImageBlockNode } from '@/components/editor/editor.type'; -import React, { forwardRef, memo, useCallback, useMemo, useRef } from 'react'; +import React, { forwardRef, memo, useCallback, useMemo, useRef, useState } from 'react'; import { ReactEditor, useSelected, useSlateStatic } from 'slate-react'; import ImageEmpty from './ImageEmpty'; import ImageRender from './ImageRender'; @@ -23,27 +23,25 @@ export const ImageBlock = memo( return align === AlignType.Center ? 'justify-center' : align === AlignType.Right ? 'justify-end' : 'justify-start'; }, [align]); + const [showToolbar, setShowToolbar] = useState(false); return ( <div {...attributes} ref={containerRef} - onClick={() => { - if (!selected) onFocusNode(); + onMouseEnter={() => { + if (!url) return; + setShowToolbar(true); }} - className={`${className || ''} image-block relative w-full cursor-pointer py-1`} + onMouseLeave={() => setShowToolbar(false)} + className={`${className || ''} image-block relative w-full cursor-default`} > - <div ref={ref} className={'absolute left-0 top-0 h-full w-full select-none caret-transparent'}> + <div ref={ref} className={'absolute left-0 top-0 h-full w-full select-none caret-transparent'}> {children} </div> - <div - contentEditable={false} - className={`flex w-full select-none ${url ? '' : 'rounded border'} ${ - selected ? 'border-fill-list-hover' : 'border-line-divider' - } ${alignCss}`} - > + <div contentEditable={false} className={`flex w-full select-none ${url ? '' : 'rounded border'} ${alignCss}`}> {url ? ( - <ImageRender selected={selected} node={node} /> + <ImageRender showToolbar={showToolbar} selected={selected} node={node} /> ) : ( <ImageEmpty node={node} onEscape={onFocusNode} containerRef={containerRef} /> )} diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/image/ImageEmpty.tsx b/frontend/appflowy_web_app/src/components/editor/components/blocks/image/ImageEmpty.tsx index 32ea2881f9..e528dd4daa 100644 --- a/frontend/appflowy_web_app/src/components/editor/components/blocks/image/ImageEmpty.tsx +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/image/ImageEmpty.tsx @@ -10,7 +10,7 @@ function ImageEmpty(_: { containerRef: React.RefObject<HTMLDivElement>; onEscape <> <div className={ - 'container-bg flex h-[48px] w-full cursor-pointer select-none items-center gap-[10px] bg-content-blue-50 px-4 text-text-caption' + 'container-bg flex h-[48px] w-full cursor-pointer select-none items-center gap-[10px] bg-fill-list-active px-4 text-text-caption' } > <ImageIcon /> diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/image/ImageRender.tsx b/frontend/appflowy_web_app/src/components/editor/components/blocks/image/ImageRender.tsx index 55677506b3..9ace998260 100644 --- a/frontend/appflowy_web_app/src/components/editor/components/blocks/image/ImageRender.tsx +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/image/ImageRender.tsx @@ -1,4 +1,7 @@ +import { notify } from '@/components/_shared/notify'; +import RightTopActionsToolbar from '@/components/editor/components/block-actions/RightTopActionsToolbar'; import { ImageBlockNode } from '@/components/editor/editor.type'; +import { copyTextToClipboard } from '@/utils/copy'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { CircularProgress } from '@mui/material'; @@ -6,7 +9,15 @@ import { ErrorOutline } from '@mui/icons-material'; const MIN_WIDTH = 100; -function ImageRender({ selected, node }: { selected: boolean; node: ImageBlockNode }) { +function ImageRender({ + selected, + node, + showToolbar, +}: { + selected: boolean; + node: ImageBlockNode; + showToolbar?: boolean; +}) { const [loading, setLoading] = useState(true); const [hasError, setHasError] = useState(false); @@ -62,6 +73,19 @@ function ImageRender({ selected, node }: { selected: boolean; node: ImageBlockNo className={`image-render relative min-h-[48px] ${hasError ? 'w-full' : ''}`} > <img loading={'lazy'} {...imageProps} alt={`image-${blockId}`} /> + {showToolbar && url && ( + <RightTopActionsToolbar + onCopy={async () => { + if (!url) return; + try { + await copyTextToClipboard(url); + notify.success(t('publish.copy.imageBlock')); + } catch (_) { + // do nothing + } + }} + /> + )} {hasError ? ( renderErrorNode() ) : loading ? ( diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/link-preview/LinkPreview.tsx b/frontend/appflowy_web_app/src/components/editor/components/blocks/link-preview/LinkPreview.tsx new file mode 100644 index 0000000000..91f36fb02c --- /dev/null +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/link-preview/LinkPreview.tsx @@ -0,0 +1,90 @@ +import { EditorElementProps, LinkPreviewNode } from '@/components/editor/editor.type'; +import axios from 'axios'; +import React, { forwardRef, memo, useEffect, useState } from 'react'; + +export const LinkPreview = memo( + forwardRef<HTMLDivElement, EditorElementProps<LinkPreviewNode>>(({ node, children, ...attributes }, ref) => { + const [data, setData] = useState<{ + image: { url: string }; + title: string; + description: string; + } | null>(null); + const [notFound, setNotFound] = useState<boolean>(false); + const url = node.data.url; + + useEffect(() => { + if (!url) return; + + setData(null); + void (async () => { + try { + setNotFound(false); + const response = await axios.get(`https://api.microlink.io/?url=${url}`); + + if (response.data.statusCode !== 200) { + setNotFound(true); + return; + } + + const data = response.data.data; + + setData(data); + } catch (_) { + setNotFound(true); + } + })(); + }, [url]); + return ( + <div + onClick={() => { + window.open(url, '_blank'); + }} + {...attributes} + ref={ref} + className={`link-preview-block relative w-full cursor-pointer py-1`} + > + <div + className={ + 'container-bg flex w-full cursor-pointer select-none items-center gap-4 overflow-hidden rounded border border-line-divider bg-fill-list-active p-3' + } + > + {notFound ? ( + <div className={'flex w-full items-center justify-center'}> + <div className={'text-text-title'}>Could not load preview</div> + <div className={'text-sm text-text-caption'}>{url}</div> + </div> + ) : ( + <> + <img + src={data?.image.url} + alt={data?.title} + className={'container h-full min-h-[48px] w-[25%] rounded bg-cover bg-center'} + /> + <div className={'flex flex-col justify-center gap-2 overflow-hidden'}> + <div + className={ + 'max-h-[48px] overflow-hidden whitespace-pre-wrap break-words text-base font-bold text-text-title' + } + > + {data?.title} + </div> + <div + className={ + 'max-h-[64px] overflow-hidden truncate whitespace-pre-wrap break-words text-sm text-text-title' + } + > + {data?.description} + </div> + <div className={'truncate whitespace-nowrap text-xs text-text-caption'}>{url}</div> + </div> + </> + )} + </div> + <div ref={ref} className={'absolute left-0 top-0 h-full w-full caret-transparent'}> + {children} + </div> + </div> + ); + }) +); +export default LinkPreview; diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/link-preview/index.ts b/frontend/appflowy_web_app/src/components/editor/components/blocks/link-preview/index.ts new file mode 100644 index 0000000000..67a3e2187a --- /dev/null +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/link-preview/index.ts @@ -0,0 +1 @@ +export * from './LinkPreview'; diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/math-equation/MathEquation.tsx b/frontend/appflowy_web_app/src/components/editor/components/blocks/math-equation/MathEquation.tsx index 6f6ba420e1..045780c432 100644 --- a/frontend/appflowy_web_app/src/components/editor/components/blocks/math-equation/MathEquation.tsx +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/math-equation/MathEquation.tsx @@ -1,7 +1,10 @@ import KatexMath from '@/components/_shared/katex-math/KatexMath'; +import { notify } from '@/components/_shared/notify'; +import RightTopActionsToolbar from '@/components/editor/components/block-actions/RightTopActionsToolbar'; import { EditorElementProps, MathEquationNode } from '@/components/editor/editor.type'; -import { FunctionsOutlined } from '@mui/icons-material'; -import { forwardRef, memo, useRef } from 'react'; +import { copyTextToClipboard } from '@/utils/copy'; +import { ReactComponent as MathSvg } from '@/assets/math.svg'; +import React, { forwardRef, memo, useMemo, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; export const MathEquation = memo( @@ -10,30 +13,61 @@ export const MathEquation = memo( const formula = node.data.formula; const { t } = useTranslation(); const containerRef = useRef<HTMLDivElement>(null); + const [showToolbar, setShowToolbar] = useState(false); + const newClassName = useMemo(() => { + const classList = [ + className, + 'math-equation-block relative w-full container-bg w-full py-1 select-none rounded', + ]; + + if (formula) { + classList.push('border border-transparent hover:border-line-divider hover:bg-fill-list-active cursor-pointer'); + } + + return classList.join(' '); + }, [formula, className]); return ( <> <div {...attributes} ref={containerRef} - className={`${className} math-equation-block relative w-full cursor-pointer py-2`} + contentEditable={false} + onMouseEnter={() => { + if (!formula) return; + setShowToolbar(true); + }} + onMouseLeave={() => setShowToolbar(false)} + className={newClassName} > - <div - contentEditable={false} - className={`container-bg w-full select-none rounded border border-line-divider bg-fill-list-active px-3`} - > - {formula ? ( - <KatexMath latex={formula} /> - ) : ( - <div className={'flex h-[48px] w-full items-center gap-[10px] text-text-caption'}> - <FunctionsOutlined /> - {t('document.plugins.mathEquation.addMathEquation')} - </div> - )} - </div> + {formula ? ( + <KatexMath latex={formula} /> + ) : ( + <div + className={ + 'flex h-[48px] w-full items-center gap-[10px] rounded border border-line-divider bg-fill-list-active px-4 text-text-caption' + } + > + <MathSvg className={'h-4 w-4'} /> + {t('document.plugins.mathEquation.addMathEquation')} + </div> + )} <div ref={ref} className={'absolute left-0 top-0 h-full w-full caret-transparent'}> {children} </div> + {showToolbar && ( + <RightTopActionsToolbar + onCopy={async () => { + if (!formula) return; + try { + await copyTextToClipboard(formula); + notify.success(t('publish.copy.mathBlock')); + } catch (_) { + // do nothing + } + }} + /> + )} </div> </> ); diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/numbered-list/NumberListIcon.tsx b/frontend/appflowy_web_app/src/components/editor/components/blocks/numbered-list/NumberListIcon.tsx index 12b4c5d0e2..d8eb22a2ec 100644 --- a/frontend/appflowy_web_app/src/components/editor/components/blocks/numbered-list/NumberListIcon.tsx +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/numbered-list/NumberListIcon.tsx @@ -25,10 +25,16 @@ export function NumberListIcon({ block, className }: { block: NumberedListNode; const staticEditor = useSlateStatic(); const path = ReactEditor.findPath(editor, block); + const index = useMemo(() => { let index = 1; let topNode; + + if (path.length === 1 && path[0] === 0) { + return index; + } + let prevPath = Path.previous(path); while (prevPath) { @@ -43,6 +49,10 @@ export function NumberListIcon({ block, className }: { block: NumberedListNode; break; } + if (prevPath.length === 1 && prevPath[0] === 0) { + return index; + } + prevPath = Path.previous(prevPath); } @@ -78,7 +88,7 @@ export function NumberListIcon({ block, className }: { block: NumberedListNode; }} contentEditable={false} data-number={dataNumber} - className={`${className} numbered-icon flex w-[24px] min-w-[24px] justify-center pr-1 font-medium`} + className={`${className} numbered-icon flex w-fit min-w-[24px] justify-center whitespace-nowrap pr-1 font-medium`} /> ); } diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/outline/Outline.tsx b/frontend/appflowy_web_app/src/components/editor/components/blocks/outline/Outline.tsx index e26c066a71..b08ed5090f 100644 --- a/frontend/appflowy_web_app/src/components/editor/components/blocks/outline/Outline.tsx +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/outline/Outline.tsx @@ -23,10 +23,12 @@ export const Outline = memo( const element = document.getElementById(id); if (element) { - void smoothScrollIntoViewIfNeeded(element, { - behavior: 'smooth', - block: 'center', - }); + void (async () => { + await smoothScrollIntoViewIfNeeded(element, { + behavior: 'smooth', + block: 'start', + }); + })(); } }, []); diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/quote/Quote.tsx b/frontend/appflowy_web_app/src/components/editor/components/blocks/quote/Quote.tsx index 0ddc0af985..6e69990fbf 100644 --- a/frontend/appflowy_web_app/src/components/editor/components/blocks/quote/Quote.tsx +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/quote/Quote.tsx @@ -4,7 +4,7 @@ import React, { forwardRef, memo, useMemo } from 'react'; export const Quote = memo( forwardRef<HTMLDivElement, EditorElementProps<QuoteNode>>(({ node: _, children, ...attributes }, ref) => { const className = useMemo(() => { - return `flex w-full flex-col ml-3 border-l-[4px] border-fill-default pl-2 ${attributes.className ?? ''}`; + return `my-1 ${attributes.className ?? ''}`; }, [attributes.className]); return ( diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/quote/QuoteIcon.tsx b/frontend/appflowy_web_app/src/components/editor/components/blocks/quote/QuoteIcon.tsx new file mode 100644 index 0000000000..fec907ced6 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/quote/QuoteIcon.tsx @@ -0,0 +1,11 @@ +import React from 'react'; + +function QuoteIcon({ className }: { className: string }) { + return ( + <span data-playwright-selected={false} contentEditable={false} draggable={false} className={`${className}`}> + <div className={'h-full w-[4px] bg-fill-default'}></div> + </span> + ); +} + +export default QuoteIcon; diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/table/Table.tsx b/frontend/appflowy_web_app/src/components/editor/components/blocks/table/Table.tsx index ae0522a1da..07e4363878 100644 --- a/frontend/appflowy_web_app/src/components/editor/components/blocks/table/Table.tsx +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/table/Table.tsx @@ -38,17 +38,23 @@ const Table = memo( }, [rowGroup, rowDefaultHeight]); return ( - <div ref={ref} {...attributes} className={`table-block relative my-2 w-full px-1 ${className || ''}`}> - <Grid - id={`table-${node.blockId}`} - rowGap='space.0' - autoFlow='column' - columnGap='space.0' - templateRows={templateRows} - templateColumns={templateColumns} - > - {children} - </Grid> + <div + ref={ref} + {...attributes} + className={`table-block relative my-2 w-full overflow-hidden px-1 ${className || ''}`} + > + <div className={'h-full w-full overflow-x-auto overflow-y-hidden'}> + <Grid + id={`table-${node.blockId}`} + rowGap='space.0' + autoFlow='column' + columnGap='space.0' + templateRows={templateRows} + templateColumns={templateColumns} + > + {children} + </Grid> + </div> </div> ); }), diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/table/TableCell.tsx b/frontend/appflowy_web_app/src/components/editor/components/blocks/table/TableCell.tsx index b2a01d5c8c..81ba6fc509 100644 --- a/frontend/appflowy_web_app/src/components/editor/components/blocks/table/TableCell.tsx +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/table/TableCell.tsx @@ -1,16 +1,28 @@ import { EditorElementProps, TableCellNode } from '@/components/editor/editor.type'; +import { renderColor } from '@/utils/color'; import React, { forwardRef, memo } from 'react'; const TableCell = memo( - forwardRef<HTMLDivElement, EditorElementProps<TableCellNode>>( - ({ node: _, children, className, ...attributes }, ref) => { - return ( - <div ref={ref} {...attributes} className={`relative table-cell text-left ${className || ''}`}> - {children} - </div> - ); - } - ) + forwardRef<HTMLDivElement, EditorElementProps<TableCellNode>>(({ node, children, className, ...attributes }, ref) => { + const { data } = node; + const rowBackgroundColor = data.rowBackgroundColor; + const colBackgroundColor = data.colBackgroundColor; + + return ( + <div + ref={ref} + {...attributes} + style={{ + ...attributes.style, + backgroundColor: + rowBackgroundColor || colBackgroundColor ? renderColor(colBackgroundColor || rowBackgroundColor) : undefined, + }} + className={`relative table-cell text-left ${className || ''}`} + > + {children} + </div> + ); + }) ); export default TableCell; diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/table/table.scss b/frontend/appflowy_web_app/src/components/editor/components/blocks/table/table.scss index 1aa812f6c5..f437d49742 100644 --- a/frontend/appflowy_web_app/src/components/editor/components/blocks/table/table.scss +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/table/table.scss @@ -1,4 +1,9 @@ .table-block { + ::-webkit-scrollbar { + width: 8px !important; + height: 8px !important; + } + [id^=table-] { width: fit-content; @apply border-t border-l border-line-border; diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/text/StartIcon.hooks.tsx b/frontend/appflowy_web_app/src/components/editor/components/blocks/text/StartIcon.hooks.tsx index 2ab5996b09..faae3fb358 100644 --- a/frontend/appflowy_web_app/src/components/editor/components/blocks/text/StartIcon.hooks.tsx +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/text/StartIcon.hooks.tsx @@ -1,6 +1,7 @@ import { BlockType } from '@/application/collab.type'; import { BulletedListIcon } from '@/components/editor/components/blocks/bulleted-list'; import { NumberListIcon } from '@/components/editor/components/blocks/numbered-list'; +import QuoteIcon from '@/components/editor/components/blocks/quote/QuoteIcon'; import ToggleIcon from '@/components/editor/components/blocks/toggle-list/ToggleIcon'; import { TextNode } from '@/components/editor/editor.type'; import React, { FC, useCallback, useMemo } from 'react'; @@ -27,6 +28,8 @@ export function useStartIcon(node: TextNode) { return NumberListIcon; case BlockType.BulletedListBlock: return BulletedListIcon; + case BlockType.QuoteBlock: + return QuoteIcon; default: return null; } @@ -37,7 +40,15 @@ export function useStartIcon(node: TextNode) { return null; } - return <Component className={`text-block-icon relative h-[24px] w-[24px]`} block={block} />; + const classList = ['text-block-icon relative w-[24px]']; + + if (block.type === BlockType.QuoteBlock) { + classList.push('h-full'); + } else { + classList.push('h-6'); + } + + return <Component className={classList.join(' ')} block={block} />; }, [Component, block]); return { diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/text/Text.tsx b/frontend/appflowy_web_app/src/components/editor/components/blocks/text/Text.tsx index a9843570ea..8a500c1689 100644 --- a/frontend/appflowy_web_app/src/components/editor/components/blocks/text/Text.tsx +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/text/Text.tsx @@ -2,30 +2,27 @@ import Placeholder from '@/components/editor/components/blocks/text/Placeholder' import { useSlateStatic } from 'slate-react'; import { useStartIcon } from './StartIcon.hooks'; import { EditorElementProps, TextNode } from '@/components/editor/editor.type'; -import React, { forwardRef, memo, useMemo } from 'react'; +import React, { forwardRef, useMemo } from 'react'; -export const Text = memo( - forwardRef<HTMLSpanElement, EditorElementProps<TextNode>>( - ({ node, children, className: classNameProp, ...attributes }, ref) => { - const { hasStartIcon, renderIcon } = useStartIcon(node); - const editor = useSlateStatic(); - const isEmpty = editor.isEmpty(node); - const className = useMemo(() => { - const classList = ['text-element', 'relative', 'flex', 'w-full', 'whitespace-pre-wrap', 'break-all', 'px-1']; +export const Text = forwardRef<HTMLSpanElement, EditorElementProps<TextNode>>( + ({ node, children, className: classNameProp, ...attributes }, ref) => { + const { hasStartIcon, renderIcon } = useStartIcon(node); + const editor = useSlateStatic(); + const isEmpty = editor.isEmpty(node); + const className = useMemo(() => { + const classList = ['text-element', 'relative', 'flex', 'w-full', 'whitespace-pre-wrap', 'break-word', 'px-1']; - if (classNameProp) classList.push(classNameProp); - if (hasStartIcon) classList.push('has-start-icon'); - return classList.join(' '); - }, [classNameProp, hasStartIcon]); + if (classNameProp) classList.push(classNameProp); + if (hasStartIcon) classList.push('has-start-icon'); + return classList.join(' '); + }, [classNameProp, hasStartIcon]); - return ( - <span {...attributes} ref={ref} className={className}> - {renderIcon()} - {isEmpty && <Placeholder node={node} />} - <span className={`text-content ${isEmpty ? 'empty-text' : ''}`}>{children}</span> - </span> - ); - } - ), - (prevProps, nextProps) => JSON.stringify(prevProps.node) === JSON.stringify(nextProps.node) + return ( + <span {...attributes} ref={ref} className={className}> + {renderIcon()} + {isEmpty && <Placeholder node={node} />} + <span className={`text-content ${isEmpty ? 'empty-text' : ''}`}>{children}</span> + </span> + ); + } ); diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/todo-list/CheckboxIcon.tsx b/frontend/appflowy_web_app/src/components/editor/components/blocks/todo-list/CheckboxIcon.tsx index 007903b89a..034a3987bf 100644 --- a/frontend/appflowy_web_app/src/components/editor/components/blocks/todo-list/CheckboxIcon.tsx +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/todo-list/CheckboxIcon.tsx @@ -14,7 +14,7 @@ function CheckboxIcon({ block, className }: { block: TodoListNode; className: st onMouseDown={(e) => { e.preventDefault(); }} - className={`${className} cursor-pointer pr-1 text-xl text-fill-default`} + className={`${className} pr-1 text-xl text-fill-default`} > {checked ? <CheckboxCheckSvg /> : <CheckboxUncheckSvg />} </span> diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/toggle-list/ToggleIcon.tsx b/frontend/appflowy_web_app/src/components/editor/components/blocks/toggle-list/ToggleIcon.tsx index 51ed844e82..b6ebfffe03 100644 --- a/frontend/appflowy_web_app/src/components/editor/components/blocks/toggle-list/ToggleIcon.tsx +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/toggle-list/ToggleIcon.tsx @@ -12,7 +12,7 @@ function ToggleIcon({ block, className }: { block: ToggleListNode; className: st onMouseDown={(e) => { e.preventDefault(); }} - className={`${className} cursor-pointer pr-1 text-xl hover:text-fill-default`} + className={`${className} pr-1 text-xl hover:text-fill-default`} > {collapsed ? <ExpandSvg className={'-rotate-90 transform'} /> : <ExpandSvg />} </span> diff --git a/frontend/appflowy_web_app/src/components/editor/components/element/Element.tsx b/frontend/appflowy_web_app/src/components/editor/components/element/Element.tsx index d117a14221..597bf25469 100644 --- a/frontend/appflowy_web_app/src/components/editor/components/element/Element.tsx +++ b/frontend/appflowy_web_app/src/components/editor/components/element/Element.tsx @@ -2,9 +2,11 @@ import { BlockData, BlockType, InlineBlockType, YjsEditorKey } from '@/applicati import { BulletedList } from '@/components/editor/components/blocks/bulleted-list'; import { Callout } from '@/components/editor/components/blocks/callout'; import { CodeBlock } from '@/components/editor/components/blocks/code'; +import { DatabaseBlock } from '@/components/editor/components/blocks/database'; import { DividerNode } from '@/components/editor/components/blocks/divider'; import { Heading } from '@/components/editor/components/blocks/heading'; import { ImageBlock } from '@/components/editor/components/blocks/image'; +import { LinkPreview } from '@/components/editor/components/blocks/link-preview'; import { MathEquation } from '@/components/editor/components/blocks/math-equation'; import { NumberedList } from '@/components/editor/components/blocks/numbered-list'; import { Outline } from '@/components/editor/components/blocks/outline'; @@ -14,7 +16,6 @@ import { Quote } from '@/components/editor/components/blocks/quote'; import { TableBlock, TableCellBlock } from '@/components/editor/components/blocks/table'; import { Text } from '@/components/editor/components/blocks/text'; import { ElementFallbackRender } from '@/components/error/ElementFallbackRender'; -import { Skeleton } from '@mui/material'; import { ErrorBoundary } from 'react-error-boundary'; import { TodoList } from 'src/components/editor/components/blocks/todo-list'; import { ToggleList } from 'src/components/editor/components/blocks/toggle-list'; @@ -23,116 +24,111 @@ import { Formula } from '@/components/editor/components/leaf/formula'; import { Mention } from '@/components/editor/components/leaf/mention'; import { EditorElementProps, TextNode } from '@/components/editor/editor.type'; import { renderColor } from '@/utils/color'; -import React, { FC, memo, Suspense, useMemo } from 'react'; +import React, { FC, useMemo } from 'react'; import { RenderElementProps } from 'slate-react'; -import { DatabaseBlock } from 'src/components/editor/components/blocks/database'; -import isEqual from 'lodash-es/isEqual'; -export const Element = memo( - ({ - element: node, - attributes, - children, - }: RenderElementProps & { - element: EditorElementProps['node']; - }) => { - const Component = useMemo(() => { - switch (node.type) { - case BlockType.HeadingBlock: - return Heading; - case BlockType.TodoListBlock: - return TodoList; - case BlockType.ToggleListBlock: - return ToggleList; - case BlockType.Paragraph: - return Paragraph; - case BlockType.DividerBlock: - return DividerNode; - case BlockType.Page: - return Page; - case BlockType.QuoteBlock: - return Quote; - case BlockType.BulletedListBlock: - return BulletedList; - case BlockType.NumberedListBlock: - return NumberedList; - case BlockType.CodeBlock: - return CodeBlock; - case BlockType.CalloutBlock: - return Callout; - case BlockType.EquationBlock: - return MathEquation; - case BlockType.ImageBlock: - return ImageBlock; - case BlockType.OutlineBlock: - return Outline; - case BlockType.TableBlock: - return TableBlock; - case BlockType.TableCell: - return TableCellBlock; - case BlockType.GridBlock: - case BlockType.BoardBlock: - case BlockType.CalendarBlock: - return DatabaseBlock; - default: - return UnSupportedBlock; - } - }, [node.type]) as FC<EditorElementProps>; - - const InlineComponent = useMemo(() => { - switch (node.type) { - case InlineBlockType.Formula: - return Formula; - case InlineBlockType.Mention: - return Mention; - default: - return null; - } - }, [node.type]) as FC<EditorElementProps>; - - const className = useMemo(() => { - const data = (node.data as BlockData) || {}; - const align = data.align; - - return `block-element flex rounded ${align ? `block-align-${align}` : ''}`; - }, [node.data]); - - const style = useMemo(() => { - const data = (node.data as BlockData) || {}; - - return { - backgroundColor: data.bg_color ? renderColor(data.bg_color) : undefined, - color: data.font_color ? renderColor(data.font_color) : undefined, - }; - }, [node.data]); - - if (InlineComponent) { - return ( - <InlineComponent {...attributes} node={node}> - {children} - </InlineComponent> - ); +export const Element = ({ + element: node, + attributes, + children, +}: RenderElementProps & { + element: EditorElementProps['node']; +}) => { + const Component = useMemo(() => { + switch (node.type) { + case BlockType.HeadingBlock: + return Heading; + case BlockType.TodoListBlock: + return TodoList; + case BlockType.ToggleListBlock: + return ToggleList; + case BlockType.Paragraph: + return Paragraph; + case BlockType.DividerBlock: + return DividerNode; + case BlockType.Page: + return Page; + case BlockType.QuoteBlock: + return Quote; + case BlockType.BulletedListBlock: + return BulletedList; + case BlockType.NumberedListBlock: + return NumberedList; + case BlockType.CodeBlock: + return CodeBlock; + case BlockType.CalloutBlock: + return Callout; + case BlockType.EquationBlock: + return MathEquation; + case BlockType.ImageBlock: + return ImageBlock; + case BlockType.OutlineBlock: + return Outline; + case BlockType.TableBlock: + return TableBlock; + case BlockType.TableCell: + return TableCellBlock; + case BlockType.GridBlock: + case BlockType.BoardBlock: + case BlockType.CalendarBlock: + return DatabaseBlock; + case BlockType.LinkPreview: + return LinkPreview; + default: + return UnSupportedBlock; } + }, [node.type]) as FC<EditorElementProps>; - if (node.type === YjsEditorKey.text) { - return ( - <Text {...attributes} node={node as TextNode}> - {children} - </Text> - ); + const InlineComponent = useMemo(() => { + switch (node.type) { + case InlineBlockType.Formula: + return Formula; + case InlineBlockType.Mention: + return Mention; + default: + return null; } + }, [node.type]) as FC<EditorElementProps>; + const className = useMemo(() => { + const data = (node.data as BlockData) || {}; + const align = data.align; + + return `block-element relative flex rounded ${align ? `block-align-${align}` : ''}`; + }, [node.data]); + + const style = useMemo(() => { + const data = (node.data as BlockData) || {}; + + return { + backgroundColor: data.bgColor ? renderColor(data.bgColor) : undefined, + color: data.font_color ? renderColor(data.font_color) : undefined, + }; + }, [node.data]); + + if (InlineComponent) { return ( - <Suspense fallback={<Skeleton width={'100%'} height={24} />}> - <ErrorBoundary fallbackRender={ElementFallbackRender}> - <div {...attributes} data-block-type={node.type} className={className}> - <Component style={style} className={`flex w-full flex-col`} node={node}> - {children} - </Component> - </div> - </ErrorBoundary> - </Suspense> + <InlineComponent {...attributes} node={node}> + {children} + </InlineComponent> ); - }, - (prevProps, nextProps) => isEqual(prevProps.element, nextProps.element) -); + } + + if (node.type === YjsEditorKey.text) { + return ( + <Text {...attributes} node={node as TextNode}> + {children} + </Text> + ); + } + + return ( + <ErrorBoundary fallbackRender={ElementFallbackRender}> + <div {...attributes} data-block-type={node.type} className={className}> + <Component style={style} className={`flex w-full flex-col`} node={node}> + {children} + </Component> + </div> + </ErrorBoundary> + ); +}; diff --git a/frontend/appflowy_web_app/src/components/editor/components/leaf/formula/Formula.tsx b/frontend/appflowy_web_app/src/components/editor/components/leaf/formula/Formula.tsx index 3731fc7216..8efd1b0345 100644 --- a/frontend/appflowy_web_app/src/components/editor/components/leaf/formula/Formula.tsx +++ b/frontend/appflowy_web_app/src/components/editor/components/leaf/formula/Formula.tsx @@ -12,12 +12,12 @@ export const Formula = memo( <span ref={ref} {...attributes} - contentEditable={false} - className={`${attributes.className ?? ''} formula-inline relative cursor-pointer rounded px-1 py-0.5 ${ + // contentEditable={false} + className={`${attributes.className ?? ''} formula-inline relative cursor-pointer rounded py-0.5 ${ selected ? 'selected' : '' }`} > - <span className={'select-none'} contentEditable={false}> + <span className={''}> <KatexMath latex={formula || ''} isInline /> </span> diff --git a/frontend/appflowy_web_app/src/components/editor/components/leaf/mention/Mention.tsx b/frontend/appflowy_web_app/src/components/editor/components/leaf/mention/Mention.tsx index eba846b9c1..540a7ce17e 100644 --- a/frontend/appflowy_web_app/src/components/editor/components/leaf/mention/Mention.tsx +++ b/frontend/appflowy_web_app/src/components/editor/components/leaf/mention/Mention.tsx @@ -11,7 +11,7 @@ export const Mention = memo( return ( <span {...attributes} - contentEditable={false} + // contentEditable={false} className={`mention relative cursor-pointer ${selected ? 'selected' : ''}`} ref={ref} > diff --git a/frontend/appflowy_web_app/src/components/editor/components/leaf/mention/MentionDate.tsx b/frontend/appflowy_web_app/src/components/editor/components/leaf/mention/MentionDate.tsx index c430968b52..1ea166b3b1 100644 --- a/frontend/appflowy_web_app/src/components/editor/components/leaf/mention/MentionDate.tsx +++ b/frontend/appflowy_web_app/src/components/editor/components/leaf/mention/MentionDate.tsx @@ -9,7 +9,7 @@ function MentionDate({ date, reminder }: { date: string; reminder?: { id: string }, [date]); return ( - <span className={'mention-inline'}> + <span className={'mention-inline cursor-text'}> {reminder ? <ReminderSvg className={'mention-icon'} /> : <DateSvg className={'mention-icon'} />} <span className={'mention-content'}>{dateFormat}</span> diff --git a/frontend/appflowy_web_app/src/components/editor/components/leaf/mention/MentionPage.tsx b/frontend/appflowy_web_app/src/components/editor/components/leaf/mention/MentionPage.tsx index 40e6d31e23..0d5a53a983 100644 --- a/frontend/appflowy_web_app/src/components/editor/components/leaf/mention/MentionPage.tsx +++ b/frontend/appflowy_web_app/src/components/editor/components/leaf/mention/MentionPage.tsx @@ -1,22 +1,56 @@ -import { useNavigateToView } from '@/application/folder-yjs'; -import { usePageInfo } from '@/components/_shared/page/usePageInfo'; -import React from 'react'; +import { ViewLayout } from '@/application/collab.type'; +import { ViewMeta } from '@/application/db/tables/view_metas'; +import { ViewIcon } from '@/components/_shared/view-icon'; +import { useEditorContext } from '@/components/editor/EditorContext'; +import React, { useEffect, useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; function MentionPage({ pageId }: { pageId: string }) { - const onNavigateToView = useNavigateToView(); - const { icon, name } = usePageInfo(pageId); + const context = useEditorContext(); + const { navigateToView, loadViewMeta } = context; + const [unPublished, setUnPublished] = useState(false); + const [meta, setMeta] = useState<ViewMeta | null>(null); + + useEffect(() => { + void (async () => { + if (loadViewMeta) { + setUnPublished(false); + try { + const meta = await loadViewMeta(pageId); + + setMeta(meta); + } catch (e) { + setUnPublished(true); + } + } + })(); + }, [loadViewMeta, pageId]); + + const icon = useMemo(() => { + return meta?.icon; + }, [meta?.icon]); + + const { t } = useTranslation(); return ( <span onClick={() => { - onNavigateToView?.(pageId); + void navigateToView?.(pageId); }} className={`mention-inline px-1 underline`} contentEditable={false} > - <span className={'mention-icon'}>{icon}</span> + {unPublished ? ( + <span className={'mention-unpublished cursor-text font-semibold text-text-caption'}>No Access</span> + ) : ( + <> + <span className={'mention-icon icon'}> + {icon?.value || <ViewIcon layout={meta?.layout || ViewLayout.Document} size={'small'} />} + </span> - <span className={'mention-content'}>{name}</span> + <span className={'mention-content'}>{meta?.name || t('menuAppHeader.defaultNewPageName')}</span> + </> + )} </span> ); } diff --git a/frontend/appflowy_web_app/src/components/editor/editor.scss b/frontend/appflowy_web_app/src/components/editor/editor.scss index 71832ec330..a3700d3288 100644 --- a/frontend/appflowy_web_app/src/components/editor/editor.scss +++ b/frontend/appflowy_web_app/src/components/editor/editor.scss @@ -1,3 +1,4 @@ +@use "src/styles/mixin.scss"; .block-element:not([data-block-type="table/cell"]) { @apply my-[4px]; @@ -195,12 +196,6 @@ span[data-slate-placeholder="true"]:not(.inline-block-content) { } } -.bulleted-icon { - &:after { - content: attr(data-letter); - font-weight: 500; - } -} .numbered-icon { &:after { @@ -234,11 +229,11 @@ span[data-slate-placeholder="true"]:not(.inline-block-content) { @apply bg-transparent; } - &:hover { - .container-bg { - background: var(--fill-list-hover) !important; - } - } + //&:hover { + // .container-bg { + // background: var(--fill-list-hover) !important; + // } + //} } [data-block-type='heading'] { @@ -257,7 +252,7 @@ span[data-slate-placeholder="true"]:not(.inline-block-content) { .mention-inline { height: inherit; overflow: hidden; - @apply inline-flex select-none gap-1 relative; + @apply inline-flex gap-1 relative; .mention-icon { @apply absolute top-1/2 transform -translate-y-1/2; @@ -301,3 +296,7 @@ span[data-slate-placeholder="true"]:not(.inline-block-content) { margin-bottom: 0px; } } + +.table-block { + @include mixin.scrollbar-style; +} \ No newline at end of file diff --git a/frontend/appflowy_web_app/src/components/editor/editor.type.ts b/frontend/appflowy_web_app/src/components/editor/editor.type.ts index d21f75cd3a..eea2c5c329 100644 --- a/frontend/appflowy_web_app/src/components/editor/editor.type.ts +++ b/frontend/appflowy_web_app/src/components/editor/editor.type.ts @@ -17,6 +17,7 @@ import { BlockId, BlockData, DatabaseNodeData, + LinkPreviewBlockData, } from '@/application/collab.type'; import { HTMLAttributes } from 'react'; import { Element } from 'slate'; @@ -91,6 +92,12 @@ export interface CalloutNode extends BlockNode { data: CalloutBlockData; } +export interface LinkPreviewNode extends BlockNode { + type: BlockType.LinkPreview; + blockId: string; + data: LinkPreviewBlockData; +} + export interface MathEquationNode extends BlockNode { type: BlockType.EquationBlock; blockId: string; diff --git a/frontend/appflowy_web_app/src/components/error/Error.hooks.ts b/frontend/appflowy_web_app/src/components/error/Error.hooks.ts deleted file mode 100644 index a9da4ed829..0000000000 --- a/frontend/appflowy_web_app/src/components/error/Error.hooks.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { useAppDispatch, useAppSelector } from '@/stores/store'; -import { useCallback, useEffect, useState } from 'react'; -import {errorActions} from "@/stores/error/slice"; - -export const useError = (e: Error) => { - const dispatch = useAppDispatch(); - const error = useAppSelector((state) => state.error); - const [errorMessage, setErrorMessage] = useState(''); - const [displayError, setDisplayError] = useState(false); - - useEffect(() => { - setDisplayError(error.display); - setErrorMessage(error.message); - }, [error]); - - const showError = useCallback( - (msg: string) => { - dispatch(errorActions.showError(msg)); - }, - [dispatch] - ); - - useEffect(() => { - if (e) { - showError(e.message); - } - }, [e, showError]); - - const hideError = () => { - dispatch(errorActions.hideError()); - }; - - return { - showError, - hideError, - errorMessage, - displayError, - }; -}; diff --git a/frontend/appflowy_web_app/src/components/error/ErrorHandlerPage.tsx b/frontend/appflowy_web_app/src/components/error/ErrorHandlerPage.tsx index 1bb15f2ca3..078650a7e2 100644 --- a/frontend/appflowy_web_app/src/components/error/ErrorHandlerPage.tsx +++ b/frontend/appflowy_web_app/src/components/error/ErrorHandlerPage.tsx @@ -1,8 +1,26 @@ -import { useError } from './Error.hooks'; +import { useCallback, useEffect, useState } from 'react'; import { ErrorModal } from './ErrorModal'; export const ErrorHandlerPage = ({ error }: { error: Error }) => { - const { hideError, errorMessage, displayError } = useError(error); + const [displayError, setDisplayError] = useState(true); + const [errorMessage, setErrorMessage] = useState(error.message); + + const hideError = () => { + setDisplayError(false); + }; + + const showError = useCallback((msg: string) => { + setErrorMessage(msg); + setDisplayError(true); + }, []); + + useEffect(() => { + if (error) { + showError(error.message); + } else { + setDisplayError(false); + } + }, [error, showError]); return displayError ? <ErrorModal message={errorMessage} onClose={hideError}></ErrorModal> : <></>; }; diff --git a/frontend/appflowy_web_app/src/components/error/NotFound.tsx b/frontend/appflowy_web_app/src/components/error/NotFound.tsx new file mode 100644 index 0000000000..1d2810fdc0 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/error/NotFound.tsx @@ -0,0 +1,49 @@ +import React from 'react'; +import { Typography, Button } from '@mui/material'; +import { useTranslation } from 'react-i18next'; +import { Link } from 'react-router-dom'; +import { ReactComponent as Logo } from '@/assets/logo.svg'; +import { ReactComponent as AppflowyLogo } from '@/assets/appflowy.svg'; + +const NotFound = () => { + const { t } = useTranslation(); + + return ( + <div className={'m-0 flex h-screen w-screen items-center justify-center bg-bg-body p-0'}> + <div className={'flex flex-col items-center gap-1 text-center'}> + <Typography + variant='h3' + className={'mb-[27px] flex items-center gap-4 text-text-title'} + component='h2' + gutterBottom + > + <Logo className={'w-9'} /> + <AppflowyLogo className={'w-32'} /> + </Typography> + <Typography className={' text-[52px] font-semibold leading-[128%] text-text-title'} gutterBottom> + {t('publish.noAccessToVisit')} + </Typography> + <Typography className={'text-[20px] leading-[152%]'} gutterBottom> + <div className={''}>{t('publish.createWithAppFlowy')}</div> + <div className={'flex items-center gap-1'}> + <span className={'font-semibold text-fill-default'}>{t('publish.fastWithAI')}</span> + <span>{t('publish.tryItNow')}</span> + </div> + </Typography> + <Button + component={Link} + to='https://appflowy.io/download' + variant='contained' + color='primary' + className={ + 'mt-[32px] mb-[48px] h-[68px] rounded-[20px] px-[44px] py-[18px] text-[20px] font-medium leading-[120%] text-content-on-fill' + } + > + {t('publish.downloadApp')} + </Button> + </div> + </div> + ); +}; + +export default NotFound; diff --git a/frontend/appflowy_web_app/src/components/folder/Folder.tsx b/frontend/appflowy_web_app/src/components/folder/Folder.tsx deleted file mode 100644 index f3b9641723..0000000000 --- a/frontend/appflowy_web_app/src/components/folder/Folder.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { useViewsIdSelector } from '@/application/folder-yjs'; -import ViewItem from '@/components/folder/ViewItem'; -import React from 'react'; - -export function Folder() { - const { viewsId } = useViewsIdSelector(); - - return ( - <div className={'m-10 p-10'}> - {viewsId.map((viewId) => { - return <ViewItem key={viewId} id={viewId} />; - })} - </div> - ); -} - -export default Folder; diff --git a/frontend/appflowy_web_app/src/components/folder/ViewItem.tsx b/frontend/appflowy_web_app/src/components/folder/ViewItem.tsx deleted file mode 100644 index 49feb382e2..0000000000 --- a/frontend/appflowy_web_app/src/components/folder/ViewItem.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { useNavigateToView } from '@/application/folder-yjs'; -import React from 'react'; -import Page from '@/components/_shared/page/Page'; - -function ViewItem({ id }: { id: string }) { - const onNavigateToView = useNavigateToView(); - - return ( - <div className={'cursor-pointer border-b border-line-border py-4 px-2'}> - <Page - onClick={() => { - onNavigateToView?.(id); - }} - id={id} - /> - </div> - ); -} - -export default ViewItem; diff --git a/frontend/appflowy_web_app/src/components/folder/index.ts b/frontend/appflowy_web_app/src/components/folder/index.ts deleted file mode 100644 index 569707cd4f..0000000000 --- a/frontend/appflowy_web_app/src/components/folder/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './Folder'; diff --git a/frontend/appflowy_web_app/src/components/layout/Header.tsx b/frontend/appflowy_web_app/src/components/layout/Header.tsx deleted file mode 100644 index df87892b42..0000000000 --- a/frontend/appflowy_web_app/src/components/layout/Header.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import { downloadPage, openAppFlowySchema, openUrl } from '@/utils/url'; -import { Button } from '@mui/material'; -import React from 'react'; -import { useTranslation } from 'react-i18next'; -import { ReactComponent as Logo } from '@/assets/logo.svg'; -import Popover, { PopoverOrigin } from '@mui/material/Popover'; -import Breadcrumb from 'src/components/layout/breadcrumb/Breadcrumb'; - -const popoverOrigin: { - anchorOrigin: PopoverOrigin; - transformOrigin: PopoverOrigin; -} = { - anchorOrigin: { - vertical: 'bottom', - horizontal: 'right', - }, - transformOrigin: { - vertical: -10, - horizontal: 'right', - }, -}; - -function Header() { - const { t } = useTranslation(); - const [anchorEl, setAnchorEl] = React.useState<HTMLButtonElement | null>(null); - - return ( - <div className={'appflowy-top-bar flex h-[64px] p-4'}> - <div className={'flex w-full items-center justify-between overflow-hidden'}> - <Breadcrumb /> - - <Button - className={'border-line-border'} - onClick={(e) => { - setAnchorEl(e.currentTarget); - }} - variant={'outlined'} - color={'inherit'} - endIcon={<Logo />} - > - Built with - </Button> - </div> - <Popover open={Boolean(anchorEl)} anchorEl={anchorEl} {...popoverOrigin} onClose={() => setAnchorEl(null)}> - <div className={'flex w-fit flex-col gap-2 p-4'}> - <Button - onClick={() => { - void openUrl(openAppFlowySchema); - }} - className={'w-full'} - variant={'outlined'} - > - {`🥳 Open AppFlowy`} - </Button> - <div className={'flex w-full items-center justify-center gap-2 text-xs text-text-caption'}> - <div className={'h-px flex-1 bg-line-divider'} /> - {t('signIn.or')} - <div className={'h-px flex-1 bg-line-divider'} /> - </div> - <Button - onClick={() => { - void openUrl(downloadPage, '_blank'); - }} - variant={'contained'} - > - {`Download AppFlowy`} - </Button> - </div> - </Popover> - </div> - ); -} - -export default Header; diff --git a/frontend/appflowy_web_app/src/components/layout/Layout.hooks.ts b/frontend/appflowy_web_app/src/components/layout/Layout.hooks.ts deleted file mode 100644 index 3ab11d17e9..0000000000 --- a/frontend/appflowy_web_app/src/components/layout/Layout.hooks.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { YFolder, YjsEditorKey, YjsFolderKey } from '@/application/collab.type'; -import { Crumb } from '@/application/folder-yjs'; -import { AFConfigContext } from '@/components/app/AppConfig'; -import { useCallback, useContext, useEffect, useState } from 'react'; -import { useNavigate, useParams, useSearchParams } from 'react-router-dom'; - -export function useLayout() { - const { workspaceId, objectId } = useParams(); - const [search] = useSearchParams(); - const folderService = useContext(AFConfigContext)?.service?.folderService; - const [folder, setFolder] = useState<YFolder | null>(null); - const views = folder?.get(YjsFolderKey.views); - const view = objectId ? views?.get(objectId) : null; - const [crumbs, setCrumbs] = useState<Crumb[]>([]); - - const getFolder = useCallback( - async (workspaceId: string) => { - const folder = (await folderService?.openWorkspace(workspaceId)) - ?.getMap(YjsEditorKey.data_section) - .get(YjsEditorKey.folder); - - if (!folder) return; - - console.log(folder.toJSON()); - setFolder(folder); - }, - [folderService] - ); - - useEffect(() => { - if (!workspaceId) return; - - void getFolder(workspaceId); - }, [getFolder, workspaceId]); - - const navigate = useNavigate(); - - const handleNavigateToView = useCallback( - (viewId: string) => { - const view = folder?.get(YjsFolderKey.views)?.get(viewId); - - if (!view) return; - navigate(`/view/${workspaceId}/${viewId}`); - }, - [folder, navigate, workspaceId] - ); - - const onChangeBreadcrumb = useCallback(() => { - if (!view) return; - const queue = [view]; - let parentId = view.get(YjsFolderKey.bid); - - while (parentId) { - const parent = views?.get(parentId); - - if (!parent) break; - - queue.unshift(parent); - parentId = parent?.get(YjsFolderKey.bid); - } - - setCrumbs( - queue - .map((view) => { - let icon = view.get(YjsFolderKey.icon); - - try { - icon = JSON.parse(icon || '')?.value; - } catch (e) { - // do nothing - } - - return { - viewId: view.get(YjsFolderKey.id), - name: view.get(YjsFolderKey.name), - icon: icon || view.get(YjsFolderKey.layout), - }; - }) - .slice(1) - ); - }, [view, views]); - - useEffect(() => { - onChangeBreadcrumb(); - - view?.observe(onChangeBreadcrumb); - views?.observe(onChangeBreadcrumb); - - return () => { - view?.unobserve(onChangeBreadcrumb); - views?.unobserve(onChangeBreadcrumb); - }; - }, [search, onChangeBreadcrumb, view, views]); - - return { folder, handleNavigateToView, crumbs, setCrumbs }; -} diff --git a/frontend/appflowy_web_app/src/components/layout/Layout.tsx b/frontend/appflowy_web_app/src/components/layout/Layout.tsx deleted file mode 100644 index dc3f075f69..0000000000 --- a/frontend/appflowy_web_app/src/components/layout/Layout.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { FolderProvider } from '@/components/_shared/context-provider/FolderProvider'; -import Header from '@/components/layout/Header'; -import { AFScroller } from '@/components/_shared/scroller'; -import { useLayout } from '@/components/layout/Layout.hooks'; -import React from 'react'; -import './layout.scss'; -import { ReactComponent as Logo } from '@/assets/logo.svg'; - -function Layout({ children }: { children: React.ReactNode }) { - const { folder, handleNavigateToView, crumbs, setCrumbs } = useLayout(); - - if (!folder) - return ( - <div className={'flex h-screen w-screen items-center justify-center'}> - <Logo className={'h-20 w-20'} /> - </div> - ); - - return ( - <FolderProvider setCrumbs={setCrumbs} crumbs={crumbs} onNavigateToView={handleNavigateToView} folder={folder}> - <Header /> - <AFScroller - overflowXHidden - style={{ - height: 'calc(100vh - 64px)', - }} - className={'appflowy-layout appflowy-scroll-container'} - > - {children} - </AFScroller> - </FolderProvider> - ); -} - -export default Layout; diff --git a/frontend/appflowy_web_app/src/components/layout/breadcrumb/Breadcrumb.tsx b/frontend/appflowy_web_app/src/components/layout/breadcrumb/Breadcrumb.tsx deleted file mode 100644 index 02e682514e..0000000000 --- a/frontend/appflowy_web_app/src/components/layout/breadcrumb/Breadcrumb.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { useCrumbs } from '@/application/folder-yjs'; -import Item from '@/components/layout/breadcrumb/Item'; -import React, { useMemo } from 'react'; - -export function Breadcrumb() { - const crumbs = useCrumbs(); - - const renderCrumb = useMemo(() => { - return crumbs?.map((crumb, index) => { - const isLast = index === crumbs.length - 1; - const key = crumb.rowId ? `${crumb.viewId}-${crumb.rowId}` : `${crumb.viewId}`; - - return ( - <React.Fragment key={key}> - <Item crumb={crumb} disableClick={isLast} /> - {!isLast && <span>/</span>} - </React.Fragment> - ); - }); - }, [crumbs]); - - return <div className={'flex flex-1 items-center gap-2 overflow-hidden'}>{renderCrumb}</div>; -} - -export default Breadcrumb; diff --git a/frontend/appflowy_web_app/src/components/layout/breadcrumb/Item.tsx b/frontend/appflowy_web_app/src/components/layout/breadcrumb/Item.tsx deleted file mode 100644 index 7d857ac893..0000000000 --- a/frontend/appflowy_web_app/src/components/layout/breadcrumb/Item.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { ViewLayout } from '@/application/collab.type'; -import { Crumb, useNavigateToView } from '@/application/folder-yjs'; -import React from 'react'; -import { useTranslation } from 'react-i18next'; -import { ReactComponent as DocumentSvg } from '$icons/16x/document.svg'; -import { ReactComponent as GridSvg } from '$icons/16x/grid.svg'; -import { ReactComponent as BoardSvg } from '$icons/16x/board.svg'; -import { ReactComponent as CalendarSvg } from '$icons/16x/date.svg'; - -const renderCrumbIcon = (icon: string) => { - if (Number(icon) === ViewLayout.Grid) { - return <GridSvg className={'h-4 w-4'} />; - } - - if (Number(icon) === ViewLayout.Board) { - return <BoardSvg className={'h-4 w-4'} />; - } - - if (Number(icon) === ViewLayout.Calendar) { - return <CalendarSvg className={'h-4 w-4'} />; - } - - if (Number(icon) === ViewLayout.Document) { - return <DocumentSvg className={'h-4 w-4'} />; - } - - return icon; -}; - -function Item({ crumb, disableClick = false }: { crumb: Crumb; disableClick?: boolean }) { - const { viewId, icon, name } = crumb; - - const { t } = useTranslation(); - const onNavigateToView = useNavigateToView(); - - return ( - <div - className={`flex items-center gap-1 ${!disableClick ? 'cursor-pointer' : 'flex-1 overflow-hidden'}`} - onClick={() => { - if (disableClick) return; - onNavigateToView?.(viewId); - }} - > - {renderCrumbIcon(icon)} - <span - className={!disableClick ? 'max-w-[250px] truncate hover:text-fill-default hover:underline' : 'flex-1 truncate'} - > - {name || t('menuAppHeader.defaultNewPageName')} - </span> - </div> - ); -} - -export default Item; diff --git a/frontend/appflowy_web_app/src/components/layout/breadcrumb/index.ts b/frontend/appflowy_web_app/src/components/layout/breadcrumb/index.ts deleted file mode 100644 index 116446358b..0000000000 --- a/frontend/appflowy_web_app/src/components/layout/breadcrumb/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './Breadcrumb'; diff --git a/frontend/appflowy_web_app/src/components/publish/CollabView.tsx b/frontend/appflowy_web_app/src/components/publish/CollabView.tsx new file mode 100644 index 0000000000..d5ae4d9d11 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/publish/CollabView.tsx @@ -0,0 +1,69 @@ +import { ViewLayout, YDoc } from '@/application/collab.type'; +import { ViewMeta } from '@/application/db/tables/view_metas'; +import { usePublishContext } from '@/application/publish'; +import ComponentLoading from '@/components/_shared/progress/ComponentLoading'; +import { useAppThemeMode } from '@/components/app/useAppThemeMode'; +import { Database } from '@/components/database'; +import { useViewMeta } from '@/components/publish/useViewMeta'; +import { ViewMetaProps } from 'src/components/view-meta'; +import React, { useMemo } from 'react'; +import { Document } from '@/components/document'; +import Y from 'yjs'; + +export interface CollabViewProps { + doc?: YDoc; +} + +function CollabView({ doc }: CollabViewProps) { + const { viewId, layout, icon, cover, layoutClassName, style, name } = useViewMeta(); + const { isDark } = useAppThemeMode(); + const View = useMemo(() => { + switch (layout) { + case ViewLayout.Document: + return Document; + case ViewLayout.Grid: + case ViewLayout.Board: + case ViewLayout.Calendar: + return Database; + default: + return null; + } + }, [layout]) as React.FC< + { + doc: YDoc; + isDark: boolean; + navigateToView?: (viewId: string) => Promise<void>; + loadViewMeta?: (viewId: string) => Promise<ViewMeta>; + getViewRowsMap?: (viewId: string, rowIds: string[]) => Promise<{ rows: Y.Map<YDoc>; destroy: () => void }>; + loadView?: (id: string) => Promise<YDoc>; + } & ViewMetaProps + >; + + const navigateToView = usePublishContext()?.toView; + const loadViewMeta = usePublishContext()?.loadViewMeta; + const getViewRowsMap = usePublishContext()?.getViewRowsMap; + const loadView = usePublishContext()?.loadView; + + if (!doc || !View) { + return <ComponentLoading />; + } + + return ( + <div style={style} className={`relative w-full ${layoutClassName}`}> + <View + doc={doc} + loadViewMeta={loadViewMeta} + getViewRowsMap={getViewRowsMap} + navigateToView={navigateToView} + loadView={loadView} + icon={icon} + cover={cover} + viewId={viewId} + name={name} + isDark={isDark} + /> + </div> + ); +} + +export default CollabView; diff --git a/frontend/appflowy_web_app/src/components/publish/PublishView.tsx b/frontend/appflowy_web_app/src/components/publish/PublishView.tsx new file mode 100644 index 0000000000..38e194f641 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/publish/PublishView.tsx @@ -0,0 +1,94 @@ +import { YDoc } from '@/application/collab.type'; +import { PublishProvider } from '@/application/publish'; +import { AFScroller } from '@/components/_shared/scroller'; +import { AFConfigContext } from '@/components/app/AppConfig'; +import CollabView from '@/components/publish/CollabView'; +import OutlineDrawer from '@/components/publish/outline/OutlineDrawer'; +import { createHotkey, HOT_KEY_NAME } from '@/utils/hotkeys'; +import React, { useCallback, useContext, useEffect, useState } from 'react'; +import { PublishViewHeader } from 'src/components/publish/header'; +import NotFound from '@/components/error/NotFound'; + +export interface PublishViewProps { + namespace: string; + publishName: string; +} + +const drawerWidth = 268; + +export function PublishView({ namespace, publishName }: PublishViewProps) { + const [doc, setDoc] = useState<YDoc | undefined>(); + const [notFound, setNotFound] = useState<boolean>(false); + + const service = useContext(AFConfigContext)?.service; + const openPublishView = useCallback(async () => { + let doc; + + setNotFound(false); + setDoc(undefined); + try { + doc = await service?.getPublishView(namespace, publishName); + } catch (e) { + setNotFound(true); + return; + } + + setDoc(doc); + }, [namespace, publishName, service]); + + useEffect(() => { + void openPublishView(); + }, [openPublishView]); + + const [open, setOpen] = useState(false); + + const onKeyDown = useCallback((e: KeyboardEvent) => { + switch (true) { + case createHotkey(HOT_KEY_NAME.TOGGLE_SIDEBAR)(e): + e.preventDefault(); + // setOpen((prev) => !prev); + break; + default: + break; + } + }, []); + + useEffect(() => { + window.addEventListener('keydown', onKeyDown); + return () => { + window.removeEventListener('keydown', onKeyDown); + }; + }, [onKeyDown]); + + if (notFound && !doc) { + return <NotFound />; + } + + return ( + <PublishProvider namespace={namespace} publishName={publishName}> + <div className={'h-screen w-screen'}> + <AFScroller + overflowXHidden + style={{ + transform: open ? `translateX(${drawerWidth}px)` : 'none', + width: open ? `calc(100% - ${drawerWidth}px)` : '100%', + transition: 'width 0.2s ease-in-out, transform 0.2s ease-in-out', + }} + className={'appflowy-layout appflowy-scroll-container'} + > + <PublishViewHeader + onOpenDrawer={() => { + setOpen(true); + }} + openDrawer={open} + /> + + <CollabView doc={doc} /> + </AFScroller> + {open && <OutlineDrawer width={drawerWidth} open={open} onClose={() => setOpen(false)} />} + </div> + </PublishProvider> + ); +} + +export default PublishView; diff --git a/frontend/appflowy_web_app/src/components/publish/header/Breadcrumb.tsx b/frontend/appflowy_web_app/src/components/publish/header/Breadcrumb.tsx new file mode 100644 index 0000000000..b59af17e91 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/publish/header/Breadcrumb.tsx @@ -0,0 +1,30 @@ +import { AFScroller } from '@/components/_shared/scroller'; +import BreadcrumbItem, { Crumb } from '@/components/publish/header/BreadcrumbItem'; +import React, { useMemo } from 'react'; +import { ReactComponent as RightIcon } from '@/assets/arrow_right.svg'; + +export function Breadcrumb({ crumbs }: { crumbs: Crumb[] }) { + const renderCrumb = useMemo(() => { + return crumbs?.map((crumb, index) => { + const isLast = index === crumbs.length - 1; + const key = crumb.rowId ? `${crumb.viewId}-${crumb.rowId}` : `${crumb.viewId}`; + + return ( + <div className={`${isLast ? 'text-text-title' : 'text-text-caption'} flex items-center gap-2`} key={key}> + <BreadcrumbItem crumb={crumb} disableClick={isLast} /> + {!isLast && <RightIcon className={'h-4 w-4'} />} + </div> + ); + }); + }, [crumbs]); + + return ( + <div className={'relative h-full w-full flex-1 overflow-hidden'}> + <AFScroller overflowYHidden className={'flex w-full items-center gap-2'}> + {renderCrumb}{' '} + </AFScroller> + </div> + ); +} + +export default Breadcrumb; diff --git a/frontend/appflowy_web_app/src/components/publish/header/BreadcrumbItem.tsx b/frontend/appflowy_web_app/src/components/publish/header/BreadcrumbItem.tsx new file mode 100644 index 0000000000..d8c2f93b5b --- /dev/null +++ b/frontend/appflowy_web_app/src/components/publish/header/BreadcrumbItem.tsx @@ -0,0 +1,80 @@ +import { ViewLayout } from '@/application/collab.type'; +import { usePublishContext } from '@/application/publish'; +import { notify } from '@/components/_shared/notify'; +import { ViewIcon } from '@/components/_shared/view-icon'; +import SpaceIcon from '@/components/publish/header/SpaceIcon'; +import { renderColor } from '@/utils/color'; +import { Tooltip } from '@mui/material'; +import React, { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; + +export interface Crumb { + viewId: string; + rowId?: string; + name: string; + icon: string; + layout: ViewLayout; + extra?: string | null; +} + +function BreadcrumbItem({ crumb, disableClick = false }: { crumb: Crumb; disableClick?: boolean }) { + const { viewId, icon, name, layout, extra } = crumb; + + const extraObj: { + is_space?: boolean; + space_icon?: string; + space_icon_color?: string; + } = useMemo(() => { + try { + return extra ? JSON.parse(extra) : {}; + } catch (e) { + return {}; + } + }, [extra]); + + const { t } = useTranslation(); + const onNavigateToView = usePublishContext()?.toView; + + return ( + <Tooltip title={name} placement={'bottom'} enterDelay={1000} enterNextDelay={1000}> + <div + className={`flex items-center gap-1 text-sm ${!disableClick ? 'cursor-pointer' : 'flex-1 overflow-hidden'}`} + onClick={async () => { + if (disableClick) return; + try { + await onNavigateToView?.(viewId); + } catch (e) { + notify.default(t('publish.hasNotBeenPublished')); + } + }} + > + {extraObj && extraObj.is_space ? ( + <span + className={'icon h-5 w-5'} + style={{ + backgroundColor: extraObj.space_icon_color ? renderColor(extraObj.space_icon_color) : undefined, + borderRadius: '8px', + }} + > + <SpaceIcon value={extraObj.space_icon || ''} /> + </span> + ) : ( + <span className={'icon flex h-5 w-5 items-center justify-center'}> + {icon || <ViewIcon layout={layout} size={'small'} />} + </span> + )} + + <span + className={ + 'max-w-[250px] overflow-hidden truncate ' + + (!disableClick ? 'hover:text-text-title hover:underline' : 'flex-1') + } + > + {name || t('menuAppHeader.defaultNewPageName')} + </span> + </div> + </Tooltip> + ); +} + +export default BreadcrumbItem; diff --git a/frontend/appflowy_web_app/src/components/publish/header/MoreActions.tsx b/frontend/appflowy_web_app/src/components/publish/header/MoreActions.tsx new file mode 100644 index 0000000000..4497eb6314 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/publish/header/MoreActions.tsx @@ -0,0 +1,106 @@ +import { Popover } from '@/components/_shared/popover'; +import { ThemeModeContext } from '@/components/app/useAppThemeMode'; +import { openUrl } from '@/utils/url'; +import { IconButton } from '@mui/material'; +import React, { useContext, useMemo } from 'react'; +import { ReactComponent as MoreIcon } from '@/assets/more.svg'; +import { ReactComponent as MoonIcon } from '@/assets/moon.svg'; +import { ReactComponent as SunIcon } from '@/assets/sun.svg'; +import { ReactComponent as ReportIcon } from '@/assets/report.svg'; +import { useTranslation } from 'react-i18next'; +import { ReactComponent as Logo } from '@/assets/logo.svg'; +import { ReactComponent as AppflowyLogo } from '@/assets/appflowy.svg'; + +function MoreActions() { + const { isDark, setDark } = useContext(ThemeModeContext) || {}; + const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null); + const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => { + setAnchorEl(event.currentTarget); + }; + + const handleClose = () => { + setAnchorEl(null); + }; + + const open = Boolean(anchorEl); + + const { t } = useTranslation(); + + const actions = useMemo(() => { + return [ + isDark + ? { + Icon: SunIcon, + label: t('settings.appearance.themeMode.light'), + onClick: () => { + setDark?.(false); + }, + } + : { + Icon: MoonIcon, + label: t('settings.appearance.themeMode.dark'), + onClick: () => { + setDark?.(true); + }, + }, + { + Icon: ReportIcon, + label: t('publish.reportPage'), + onClick: () => { + void openUrl('https://report.appflowy.io/', '_blank'); + }, + }, + ]; + }, [isDark, t, setDark]); + + return ( + <> + <IconButton onClick={handleClick}> + <MoreIcon className={'text-text-caption'} /> + </IconButton> + <Popover + anchorOrigin={{ + vertical: 'bottom', + horizontal: 'right', + }} + transformOrigin={{ + vertical: 'top', + horizontal: 'right', + }} + open={open} + anchorEl={anchorEl} + onClose={handleClose} + > + <div className={'flex w-[240px] flex-col gap-2 px-2 py-2'}> + {actions.map((action, index) => ( + <button + onClick={() => { + action.onClick(); + handleClose(); + }} + key={index} + className={ + 'flex items-center gap-2 rounded-[8px] p-1.5 text-sm hover:bg-content-blue-50 focus:bg-content-blue-50 focus:outline-none' + } + > + <action.Icon /> + <span>{action.label}</span> + </button> + ))} + <div + onClick={() => { + window.open('https://appflowy.io', '_blank'); + }} + className={'flex w-full cursor-pointer items-center justify-center py-2 text-sm text-text-title opacity-50'} + > + Powered by + <Logo className={'ml-3 h-4 w-4'} /> + <AppflowyLogo className={'w-20'} /> + </div> + </div> + </Popover> + </> + ); +} + +export default MoreActions; diff --git a/frontend/appflowy_web_app/src/components/publish/header/PublishViewHeader.tsx b/frontend/appflowy_web_app/src/components/publish/header/PublishViewHeader.tsx new file mode 100644 index 0000000000..9075ba162c --- /dev/null +++ b/frontend/appflowy_web_app/src/components/publish/header/PublishViewHeader.tsx @@ -0,0 +1,107 @@ +import { usePublishContext } from '@/application/publish'; +import { openOrDownload } from '@/components/publish/header/utils'; +import { Divider, IconButton, Tooltip } from '@mui/material'; +import { debounce } from 'lodash-es'; +import React, { useCallback, useMemo } from 'react'; +import OutlinePopover from '@/components/publish/outline/OutlinePopover'; +import { useTranslation } from 'react-i18next'; +import Breadcrumb from './Breadcrumb'; +import { ReactComponent as Logo } from '@/assets/logo.svg'; +import MoreActions from './MoreActions'; +import { ReactComponent as SideOutlined } from '@/assets/side_outlined.svg'; + +export const HEADER_HEIGHT = 48; + +export function PublishViewHeader({ onOpenDrawer, openDrawer }: { onOpenDrawer: () => void; openDrawer: boolean }) { + const { t } = useTranslation(); + const viewMeta = usePublishContext()?.viewMeta; + const crumbs = useMemo(() => { + const ancestors = viewMeta?.ancestor_views.slice(1) || []; + + return ancestors.map((ancestor) => { + let icon; + + try { + const extra = ancestor?.extra ? JSON.parse(ancestor.extra) : {}; + + icon = extra.icon?.value || ancestor.icon?.value; + } catch (e) { + // ignore + } + + return { + viewId: ancestor.view_id, + name: ancestor.name, + icon: icon, + layout: ancestor.layout, + extra: ancestor.extra, + }; + }); + }, [viewMeta]); + const [openPopover, setOpenPopover] = React.useState(false); + + const debounceClosePopover = useMemo(() => { + return debounce(() => { + setOpenPopover(false); + }, 200); + }, []); + + const handleOpenPopover = useCallback(() => { + debounceClosePopover.cancel(); + if (openDrawer) { + return; + } + + setOpenPopover(true); + }, [openDrawer, debounceClosePopover]); + + return ( + <div + style={{ + backdropFilter: 'saturate(180%) blur(16px)', + background: 'var(--header)', + height: HEADER_HEIGHT, + }} + className={'appflowy-top-bar sticky top-0 z-10 flex px-5'} + > + <div className={'flex w-full items-center justify-between gap-2 overflow-hidden'}> + {!openDrawer && ( + <OutlinePopover + onMouseEnter={handleOpenPopover} + onMouseLeave={debounceClosePopover} + open={openPopover} + onClose={debounceClosePopover} + > + <IconButton + className={'hidden'} + onClick={() => { + setOpenPopover(false); + onOpenDrawer(); + }} + onMouseEnter={handleOpenPopover} + onMouseLeave={debounceClosePopover} + > + <SideOutlined className={'h-4 w-4'} /> + </IconButton> + </OutlinePopover> + )} + + <div className={'h-full flex-1 overflow-hidden'}> + <Breadcrumb crumbs={crumbs} /> + </div> + + <div className={'flex items-center gap-2'}> + <MoreActions /> + <Divider orientation={'vertical'} className={'mx-2'} flexItem /> + <Tooltip title={t('publish.downloadApp')}> + <button onClick={openOrDownload}> + <Logo className={'h-6 w-6'} /> + </button> + </Tooltip> + </div> + </div> + </div> + ); +} + +export default PublishViewHeader; diff --git a/frontend/appflowy_web_app/src/components/publish/header/SpaceIcon.tsx b/frontend/appflowy_web_app/src/components/publish/header/SpaceIcon.tsx new file mode 100644 index 0000000000..de4baba493 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/publish/header/SpaceIcon.tsx @@ -0,0 +1,61 @@ +import React from 'react'; +import { ReactComponent as SpaceIcon1 } from '@/assets/space_icon/space_icon_1.svg'; +import { ReactComponent as SpaceIcon2 } from '@/assets/space_icon/space_icon_2.svg'; +import { ReactComponent as SpaceIcon3 } from '@/assets/space_icon/space_icon_3.svg'; +import { ReactComponent as SpaceIcon4 } from '@/assets/space_icon/space_icon_4.svg'; +import { ReactComponent as SpaceIcon5 } from '@/assets/space_icon/space_icon_5.svg'; +import { ReactComponent as SpaceIcon6 } from '@/assets/space_icon/space_icon_6.svg'; +import { ReactComponent as SpaceIcon7 } from '@/assets/space_icon/space_icon_7.svg'; +import { ReactComponent as SpaceIcon8 } from '@/assets/space_icon/space_icon_8.svg'; +import { ReactComponent as SpaceIcon9 } from '@/assets/space_icon/space_icon_9.svg'; +import { ReactComponent as SpaceIcon10 } from '@/assets/space_icon/space_icon_10.svg'; +import { ReactComponent as SpaceIcon11 } from '@/assets/space_icon/space_icon_11.svg'; +import { ReactComponent as SpaceIcon12 } from '@/assets/space_icon/space_icon_12.svg'; +import { ReactComponent as SpaceIcon13 } from '@/assets/space_icon/space_icon_13.svg'; +import { ReactComponent as SpaceIcon14 } from '@/assets/space_icon/space_icon_14.svg'; +import { ReactComponent as SpaceIcon15 } from '@/assets/space_icon/space_icon_15.svg'; + +export const getIconComponent = (icon: string) => { + switch (icon) { + case 'space_icon_1': + return SpaceIcon1; + case 'space_icon_2': + return SpaceIcon2; + case 'space_icon_3': + return SpaceIcon3; + case 'space_icon_4': + return SpaceIcon4; + case 'space_icon_5': + return SpaceIcon5; + case 'space_icon_6': + return SpaceIcon6; + case 'space_icon_7': + return SpaceIcon7; + case 'space_icon_8': + return SpaceIcon8; + case 'space_icon_9': + return SpaceIcon9; + case 'space_icon_10': + return SpaceIcon10; + case 'space_icon_11': + return SpaceIcon11; + case 'space_icon_12': + return SpaceIcon12; + case 'space_icon_13': + return SpaceIcon13; + case 'space_icon_14': + return SpaceIcon14; + case 'space_icon_15': + return SpaceIcon15; + default: + return SpaceIcon1; + } +}; + +function SpaceIcon({ value }: { value: string }) { + const IconComponent = getIconComponent(value); + + return <IconComponent className={'h-5 w-5'} />; +} + +export default SpaceIcon; diff --git a/frontend/appflowy_web_app/src/components/publish/header/index.ts b/frontend/appflowy_web_app/src/components/publish/header/index.ts new file mode 100644 index 0000000000..7c617af56f --- /dev/null +++ b/frontend/appflowy_web_app/src/components/publish/header/index.ts @@ -0,0 +1 @@ +export * from './PublishViewHeader'; diff --git a/frontend/appflowy_web_app/src/components/publish/header/utils.ts b/frontend/appflowy_web_app/src/components/publish/header/utils.ts new file mode 100644 index 0000000000..05cf87e9cc --- /dev/null +++ b/frontend/appflowy_web_app/src/components/publish/header/utils.ts @@ -0,0 +1,99 @@ +export function openOrDownload() { + const getDeviceType = () => { + const ua = navigator.userAgent; + + if (/(iPad|iPhone|iPod)/g.test(ua)) { + return 'iOS'; + } else if (/Android/g.test(ua)) { + return 'Android'; + } else { + return 'Desktop'; + } + }; + + const deviceType = getDeviceType(); + const isMobile = deviceType !== 'Desktop'; + const getFallbackLink = () => { + if (deviceType === 'iOS') { + return 'https://testflight.apple.com/join/6CexvkDz'; + } else if (deviceType === 'Android') { + return 'https://play.google.com/store/apps/details?id=io.appflowy.appflowy'; + } else { + return 'https://appflowy.io/download/#pop'; + } + }; + + const getDuration = () => { + switch (deviceType) { + case 'iOS': + return 250; + default: + return 1500; + } + }; + + const APPFLOWY_SCHEME = 'appflowy-flutter://'; + + const iframe = document.createElement('iframe'); + + iframe.style.display = 'none'; + iframe.src = APPFLOWY_SCHEME; + + const openSchema = () => { + if (isMobile) return (window.location.href = APPFLOWY_SCHEME); + document.body.appendChild(iframe); + setTimeout(() => { + document.body.removeChild(iframe); + }, 1000); + }; + + const openAppFlowy = () => { + openSchema(); + + const initialTime = Date.now(); + let interactTime = initialTime; + let waitTime = 0; + const duration = getDuration(); + + const updateInteractTime = () => { + interactTime = Date.now(); + }; + + document.removeEventListener('mousemove', updateInteractTime); + document.removeEventListener('mouseenter', updateInteractTime); + + const checkOpen = setInterval(() => { + waitTime = Date.now() - initialTime; + + if (waitTime > duration) { + clearInterval(checkOpen); + if (isMobile || Date.now() - interactTime < duration) { + window.open(getFallbackLink(), '_current'); + } + } + }, 20); + + if (!isMobile) { + document.addEventListener('mouseenter', updateInteractTime); + document.addEventListener('mousemove', updateInteractTime); + } + + document.addEventListener('visibilitychange', () => { + const isHidden = document.hidden; + + if (isHidden) { + clearInterval(checkOpen); + } + }); + + window.onpagehide = () => { + clearInterval(checkOpen); + }; + + window.onbeforeunload = () => { + clearInterval(checkOpen); + }; + }; + + openAppFlowy(); +} diff --git a/frontend/appflowy_web_app/src/components/publish/index.ts b/frontend/appflowy_web_app/src/components/publish/index.ts new file mode 100644 index 0000000000..796b67fd63 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/publish/index.ts @@ -0,0 +1 @@ +export * from './PublishView'; diff --git a/frontend/appflowy_web_app/src/components/publish/outline/Outline.tsx b/frontend/appflowy_web_app/src/components/publish/outline/Outline.tsx new file mode 100644 index 0000000000..69ac61158a --- /dev/null +++ b/frontend/appflowy_web_app/src/components/publish/outline/Outline.tsx @@ -0,0 +1,64 @@ +import { PublishViewInfo, ViewLayout } from '@/application/collab.type'; +import OutlineItem from '@/components/publish/outline/OutlineItem'; +import SearchInput from '@/components/publish/outline/SearchInput'; +import { filterViews } from '@/components/publish/outline/utils'; +import { CircularProgress } from '@mui/material'; +import React, { useCallback, useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; + +function Outline({ viewMeta, width }: { viewMeta?: PublishViewInfo; width: number }) { + const hasChildren = Boolean(viewMeta?.child_views?.length); + const { t } = useTranslation(); + const [children, setChildren] = React.useState<PublishViewInfo[]>([]); + + useEffect(() => { + if (viewMeta) { + setChildren(viewMeta.child_views || []); + } + }, [viewMeta]); + + const handleSearch = useCallback( + (val: string) => { + if (!val) { + return setChildren(viewMeta?.child_views || []); + } + + setChildren(filterViews(viewMeta?.child_views || [], val)); + }, + [viewMeta] + ); + + if (!viewMeta) { + return <CircularProgress />; + } + + return ( + <div className={'flex w-full flex-1 flex-col items-start justify-between gap-2'}> + <div + style={{ + position: 'sticky', + top: 0, + width: '100%', + height: '44px', + }} + className={'z-10 flex items-center justify-center gap-3 bg-bg-body'} + > + <SearchInput onSearch={handleSearch} /> + </div> + + {hasChildren ? ( + <div className={'flex w-full flex-1 flex-col'}> + {children + .filter((view) => view.layout === ViewLayout.Document) + .map((view: PublishViewInfo) => ( + <OutlineItem width={width} key={view.view_id} view={view} /> + ))} + </div> + ) : ( + <div className={'flex w-full flex-1 items-center justify-center text-text-caption'}>{t('noPagesInside')}</div> + )} + </div> + ); +} + +export default Outline; diff --git a/frontend/appflowy_web_app/src/components/publish/outline/OutlineDrawer.tsx b/frontend/appflowy_web_app/src/components/publish/outline/OutlineDrawer.tsx new file mode 100644 index 0000000000..2376db484d --- /dev/null +++ b/frontend/appflowy_web_app/src/components/publish/outline/OutlineDrawer.tsx @@ -0,0 +1,65 @@ +import { usePublishContext } from '@/application/publish'; +import { ReactComponent as AppflowyLogo } from '@/assets/appflowy.svg'; +import { ReactComponent as Logo } from '@/assets/logo.svg'; +import { ReactComponent as SideOutlined } from '@/assets/side_outlined.svg'; +import Outline from '@/components/publish/outline/Outline'; +import { createHotKeyLabel, HOT_KEY_NAME } from '@/utils/hotkeys'; +import { Drawer, IconButton, Tooltip } from '@mui/material'; +import { useTranslation } from 'react-i18next'; + +function OutlineDrawer({ open, width, onClose }: { open: boolean; width: number; onClose: () => void }) { + const { t } = useTranslation(); + const viewMeta = usePublishContext()?.viewMeta; + + return ( + <Drawer + sx={{ + width, + flexShrink: 0, + boxShadow: 'var(--shadow)', + '& .MuiDrawer-paper': { + width, + boxSizing: 'border-box', + borderColor: 'var(--line-divider)', + boxShadow: 'none', + }, + }} + variant='persistent' + anchor='left' + open={open} + tabIndex={0} + autoFocus + > + <div className={'flex h-full flex-col'}> + <div className={'flex h-[48px] items-center justify-between p-4'}> + <div + className={'flex cursor-pointer items-center gap-1 text-text-title'} + onClick={() => { + window.open('https://appflowy.io', '_blank'); + }} + > + <Logo className={'h-5 w-5'} /> + <AppflowyLogo className={'w-24'} /> + </div> + <Tooltip + title={ + <div className={'flex flex-col'}> + <span>{t('sideBar.closeSidebar')}</span> + <span className={'text-xs text-text-caption'}>{createHotKeyLabel(HOT_KEY_NAME.TOGGLE_SIDEBAR)}</span> + </div> + } + > + <IconButton onClick={onClose}> + <SideOutlined className={'h-4 w-4 rotate-180 transform'} /> + </IconButton> + </Tooltip> + </div> + <div className={'flex flex-1 flex-col overflow-y-auto px-4 pb-4'}> + <Outline width={width} viewMeta={viewMeta} /> + </div> + </div> + </Drawer> + ); +} + +export default OutlineDrawer; diff --git a/frontend/appflowy_web_app/src/components/publish/outline/OutlineItem.tsx b/frontend/appflowy_web_app/src/components/publish/outline/OutlineItem.tsx new file mode 100644 index 0000000000..63e92afea5 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/publish/outline/OutlineItem.tsx @@ -0,0 +1,99 @@ +import { PublishViewInfo, ViewLayout } from '@/application/collab.type'; +import { PublishContext } from '@/application/publish'; +import { notify } from '@/components/_shared/notify'; +import { ViewIcon } from '@/components/_shared/view-icon'; +import React, { useCallback, useContext } from 'react'; +import { ReactComponent as ChevronDownIcon } from '@/assets/chevron_down.svg'; +import { useTranslation } from 'react-i18next'; + +function OutlineItem({ view, level = 0, width }: { view: PublishViewInfo; width: number; level?: number }) { + const [isExpanded, setIsExpanded] = React.useState(false); + const getIcon = useCallback(() => { + if (isExpanded) { + return ( + <button + style={{ + paddingLeft: 1.125 * level + 'rem', + }} + onClick={() => { + setIsExpanded(false); + }} + > + <ChevronDownIcon className={'h-4 w-4'} /> + </button> + ); + } + + return ( + <button + style={{ + paddingLeft: 1.125 * level + 'rem', + }} + onClick={() => { + setIsExpanded(true); + }} + > + <ChevronDownIcon className={'h-4 w-4 -rotate-90 transform'} /> + </button> + ); + }, [isExpanded, level]); + const { t } = useTranslation(); + + const navigateToView = useContext(PublishContext)?.toView; + const renderItem = (item: PublishViewInfo) => { + const { icon, layout, name, view_id } = item; + + return ( + <div className={'flex h-fit flex-col gap-2'}> + <div + style={{ + width: width - 32, + }} + className={ + 'flex items-center gap-0.5 rounded-[8px] p-1.5 text-sm hover:bg-content-blue-50 focus:bg-content-blue-50 focus:outline-none' + } + > + {item.child_views?.length ? getIcon() : null} + <div + onClick={async () => { + try { + await navigateToView?.(view_id); + } catch (e) { + notify.error(t('publish.hasNotBeenPublished')); + } + }} + style={{ + paddingLeft: item.child_views?.length ? 0 : 1.125 * (level + 1) + 'rem', + }} + className={'flex flex-1 cursor-pointer items-center gap-1.5 overflow-hidden'} + > + <div className={'icon'}>{icon?.value || <ViewIcon layout={layout} size={'small'} />}</div> + <div className={'flex-1 truncate'}>{name}</div> + </div> + </div> + </div> + ); + }; + + const children = view.child_views || []; + + return ( + <div className={'flex h-fit flex-col'}> + {renderItem(view)} + <div + className={'flex transform flex-col gap-2 transition-all'} + style={{ + display: isExpanded ? 'block' : 'none', + }} + > + {children + .filter((view) => view.layout === ViewLayout.Document) + .map((item, index) => ( + <OutlineItem level={level + 1} width={width} key={index} view={item} /> + ))} + </div> + </div> + ); +} + +export default OutlineItem; diff --git a/frontend/appflowy_web_app/src/components/publish/outline/OutlinePopover.tsx b/frontend/appflowy_web_app/src/components/publish/outline/OutlinePopover.tsx new file mode 100644 index 0000000000..0812adbcc0 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/publish/outline/OutlinePopover.tsx @@ -0,0 +1,66 @@ +import { usePublishContext } from '@/application/publish'; +import Outline from '@/components/publish/outline/Outline'; +import { Divider, PopperPlacementType } from '@mui/material'; +import React, { ReactElement, useMemo } from 'react'; +import RichTooltip from 'src/components/_shared/popover/RichTooltip'; +import { ReactComponent as Logo } from '@/assets/logo.svg'; +import { ReactComponent as AppflowyLogo } from '@/assets/appflowy.svg'; + +export function OutlinePopover({ + children, + open, + onClose, + placement, + onMouseEnter, + onMouseLeave, +}: { + open: boolean; + onClose: () => void; + children: ReactElement; + placement?: PopperPlacementType; + onMouseEnter?: () => void; + onMouseLeave?: () => void; +}) { + const viewMeta = usePublishContext()?.viewMeta; + + const content = useMemo(() => { + return ( + <div + onMouseEnter={onMouseEnter} + onMouseLeave={onMouseLeave} + className={'flex h-fit max-h-[500px] w-[268px] flex-col overflow-y-auto overflow-x-hidden p-2'} + > + <Outline width={268} viewMeta={viewMeta} /> + <div + style={{ + position: 'sticky', + bottom: 0, + width: '100%', + height: '44px', + }} + className={'flex flex-col items-center justify-center gap-3 bg-bg-body'} + > + {Boolean(viewMeta?.child_views?.length) && <Divider className={'w-full'} />} + + <div + onClick={() => { + window.open('https://appflowy.io', '_blank'); + }} + className={'flex w-full cursor-pointer items-center justify-center text-sm text-text-title opacity-50'} + > + <Logo className={'h-4 w-4'} /> + <AppflowyLogo className={'w-20'} /> + </div> + </div> + </div> + ); + }, [onMouseEnter, onMouseLeave, viewMeta]); + + return ( + <RichTooltip open={open} onClose={onClose} content={content} placement={placement}> + {children} + </RichTooltip> + ); +} + +export default OutlinePopover; diff --git a/frontend/appflowy_web_app/src/components/publish/outline/SearchInput.tsx b/frontend/appflowy_web_app/src/components/publish/outline/SearchInput.tsx new file mode 100644 index 0000000000..f3dbadc906 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/publish/outline/SearchInput.tsx @@ -0,0 +1,39 @@ +import { InputAdornment, OutlinedInput } from '@mui/material'; +import { debounce } from 'lodash-es'; +import React from 'react'; +import { ReactComponent as SearchIcon } from '@/assets/search.svg'; +import { useTranslation } from 'react-i18next'; + +function SearchInput({ onSearch }: { onSearch: (value: string) => void }) { + const [value, setValue] = React.useState(''); + + const debounceSearch = React.useMemo(() => { + return debounce((value: string) => { + onSearch(value); + }, 200); + }, [onSearch]); + const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => { + setValue(event.target.value); + debounceSearch(event.target.value); + }; + + const { t } = useTranslation(); + + return ( + <OutlinedInput + spellCheck={false} + startAdornment={ + <InputAdornment className={'text-text-caption'} position='start'> + <SearchIcon className={'h-4 w-4'} /> + </InputAdornment> + } + onChange={handleChange} + placeholder={t('search.label')} + className={'h-[30px] w-full rounded-lg bg-bg-body'} + value={value} + size={'small'} + /> + ); +} + +export default SearchInput; diff --git a/frontend/appflowy_web_app/src/components/publish/outline/index.ts b/frontend/appflowy_web_app/src/components/publish/outline/index.ts new file mode 100644 index 0000000000..626a625ee5 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/publish/outline/index.ts @@ -0,0 +1 @@ +export * from './OutlinePopover'; diff --git a/frontend/appflowy_web_app/src/components/publish/outline/utils.ts b/frontend/appflowy_web_app/src/components/publish/outline/utils.ts new file mode 100644 index 0000000000..284c24505f --- /dev/null +++ b/frontend/appflowy_web_app/src/components/publish/outline/utils.ts @@ -0,0 +1,21 @@ +import { PublishViewInfo } from '@/application/collab.type'; + +export function filterViews(views: PublishViewInfo[], keyword: string): PublishViewInfo[] { + const filterAndFlatten = (views: PublishViewInfo[]): PublishViewInfo[] => { + let result: PublishViewInfo[] = []; + + for (const view of views) { + if (view.name.toLowerCase().includes(keyword.toLowerCase())) { + result.push(view); + } else if (view.child_views) { + const filteredChildren = filterAndFlatten(view.child_views); + + result = result.concat(filteredChildren); + } + } + + return result; + }; + + return filterAndFlatten(views); +} diff --git a/frontend/appflowy_web_app/src/components/publish/useViewMeta.ts b/frontend/appflowy_web_app/src/components/publish/useViewMeta.ts new file mode 100644 index 0000000000..18122ef445 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/publish/useViewMeta.ts @@ -0,0 +1,82 @@ +import { usePublishContext } from '@/application/publish'; +import { EditorLayoutStyle } from '@/components/editor/EditorContext'; +import { ViewMetaCover } from '@/components/view-meta'; +import { useEffect, useMemo } from 'react'; + +export function useViewMeta() { + const viewMeta = usePublishContext()?.viewMeta; + + const extra = useMemo(() => { + try { + return viewMeta?.extra ? JSON.parse(viewMeta.extra) : null; + } catch (e) { + return null; + } + }, [viewMeta?.extra]); + + const layoutStyle: EditorLayoutStyle = useMemo(() => { + return { + font: extra?.font || '', + fontLayout: extra?.fontLayout, + lineHeightLayout: extra?.lineHeightLayout, + }; + }, [extra]); + + const layout = viewMeta?.layout; + const style = useMemo(() => { + const fontSizeMap = { + small: '14px', + normal: '16px', + large: '20px', + }; + + return { + fontFamily: layoutStyle.font, + fontSize: fontSizeMap[layoutStyle.fontLayout], + }; + }, [layoutStyle]); + + const layoutClassName = useMemo(() => { + const classList = []; + + if (layoutStyle.fontLayout === 'large') { + classList.push('font-large'); + } else if (layoutStyle.fontLayout === 'small') { + classList.push('font-small'); + } + + if (layoutStyle.lineHeightLayout === 'large') { + classList.push('line-height-large'); + } else if (layoutStyle.lineHeightLayout === 'small') { + classList.push('line-height-small'); + } + + return classList.join(' '); + }, [layoutStyle]); + + useEffect(() => { + if (!layoutStyle.font) return; + void window.WebFont?.load({ + google: { + families: [layoutStyle.font], + }, + }); + }, [layoutStyle.font]); + + const icon = viewMeta?.icon || undefined; + + const cover = extra?.cover as ViewMetaCover; + + const viewId = viewMeta?.view_id; + const name = viewMeta?.name; + + return { + icon, + cover, + style, + layoutClassName, + layout, + viewId, + name, + }; +} diff --git a/frontend/appflowy_web_app/src/components/tauri/SignInAsAnonymous.tsx b/frontend/appflowy_web_app/src/components/tauri/SignInAsAnonymous.tsx index ff241f728b..0676bb449a 100644 --- a/frontend/appflowy_web_app/src/components/tauri/SignInAsAnonymous.tsx +++ b/frontend/appflowy_web_app/src/components/tauri/SignInAsAnonymous.tsx @@ -1,10 +1,8 @@ import React from 'react'; import { Button } from '@mui/material'; -import { useAuth } from '@/components/auth/auth.hooks'; import { useTranslation } from 'react-i18next'; function SignInAsAnonymous() { - const { signInAsAnonymous } = useAuth(); const { t } = useTranslation(); return ( @@ -14,7 +12,6 @@ function SignInAsAnonymous() { color={'inherit'} className={'border-transparent bg-line-divider py-3'} variant={'outlined'} - onClick={signInAsAnonymous} > {t('signIn.loginStartWithAnonymous')} </Button> diff --git a/frontend/appflowy_web_app/src/components/tauri/tauri.hooks.ts b/frontend/appflowy_web_app/src/components/tauri/tauri.hooks.ts index f95c2ca696..56e0b7997f 100644 --- a/frontend/appflowy_web_app/src/components/tauri/tauri.hooks.ts +++ b/frontend/appflowy_web_app/src/components/tauri/tauri.hooks.ts @@ -1,11 +1,7 @@ import { useCallback } from 'react'; import { notify } from '@/components/_shared/notify'; -import { useAuth } from '@/components/auth/auth.hooks'; export function useDeepLink() { - const { - signInWithOAuth, - } = useAuth(); const onDeepLink = useCallback(async () => { const { event } = await import('@tauri-apps/api'); @@ -21,15 +17,12 @@ export function useDeepLink() { // update login state to error return; } - - await signInWithOAuth(payload); }); - }, [signInWithOAuth]); + }, []); return { onDeepLink, }; - } function parseHash(hash: string) { diff --git a/frontend/appflowy_web_app/src/components/view-meta/ViewCover.tsx b/frontend/appflowy_web_app/src/components/view-meta/ViewCover.tsx new file mode 100644 index 0000000000..df7c495bb3 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/view-meta/ViewCover.tsx @@ -0,0 +1,42 @@ +import { renderColor } from '@/utils/color'; +import React, { useCallback } from 'react'; + +function ViewCover({ coverValue, coverType }: { coverValue?: string; coverType?: string }) { + const renderCoverColor = useCallback((color: string) => { + return ( + <div + style={{ + background: renderColor(color), + }} + className={`h-full w-full`} + /> + ); + }, []); + + const renderCoverImage = useCallback((url: string) => { + return <img draggable={false} src={url} alt={''} className={'h-full w-full object-cover'} />; + }, []); + + if (!coverType || !coverValue) { + return null; + } + + return ( + <div className={'relative flex h-[208px] w-full max-sm:h-[180px]'}> + {coverType === 'color' && renderCoverColor(coverValue)} + {(coverType === 'custom' || coverType === 'built_in') && renderCoverImage(coverValue)} + </div> + ); +} + +export default ViewCover; + +export enum CoverType { + NormalColor = 'color', + GradientColor = 'gradient', + BuildInImage = 'built_in', + CustomImage = 'custom', + LocalImage = 'local', + UpsplashImage = 'unsplash', + None = 'none', +} diff --git a/frontend/appflowy_web_app/src/components/view-meta/ViewMetaPreview.tsx b/frontend/appflowy_web_app/src/components/view-meta/ViewMetaPreview.tsx new file mode 100644 index 0000000000..6c555dd641 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/view-meta/ViewMetaPreview.tsx @@ -0,0 +1,75 @@ +import BuiltInImage1 from '@/assets/cover/m_cover_image_1.png'; +import BuiltInImage2 from '@/assets/cover/m_cover_image_2.png'; +import BuiltInImage3 from '@/assets/cover/m_cover_image_3.png'; +import BuiltInImage4 from '@/assets/cover/m_cover_image_4.png'; +import BuiltInImage5 from '@/assets/cover/m_cover_image_5.png'; +import BuiltInImage6 from '@/assets/cover/m_cover_image_6.png'; +import ViewCover, { CoverType } from '@/components/view-meta/ViewCover'; +import React, { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { ViewMetaIcon } from '@/application/collab.type'; + +export interface ViewMetaCover { + type: CoverType; + value: string; +} + +export interface ViewMetaProps { + icon?: ViewMetaIcon; + cover?: ViewMetaCover; + name?: string; + viewId?: string; +} + +export function ViewMetaPreview({ icon, cover, name }: ViewMetaProps) { + const coverType = useMemo(() => { + if (cover && [CoverType.NormalColor, CoverType.GradientColor].includes(cover.type)) { + return 'color'; + } + + if (CoverType.BuildInImage === cover?.type) { + return 'built_in'; + } + + if (cover && [CoverType.CustomImage, CoverType.UpsplashImage].includes(cover.type)) { + return 'custom'; + } + }, [cover]); + + const coverValue = useMemo(() => { + if (coverType === 'built_in') { + return { + 1: BuiltInImage1, + 2: BuiltInImage2, + 3: BuiltInImage3, + 4: BuiltInImage4, + 5: BuiltInImage5, + 6: BuiltInImage6, + }[cover?.value as string]; + } + + return cover?.value; + }, [coverType, cover?.value]); + const { t } = useTranslation(); + + return ( + <div className={'flex w-full flex-col items-center'}> + {cover && <ViewCover coverType={coverType} coverValue={coverValue} />} + <div className={`relative mx-16 mb-6 mt-[52px] w-[964px] min-w-0 max-w-full overflow-visible max-md:mx-4`}> + <div + className={ + 'flex gap-4 overflow-hidden whitespace-pre-wrap break-words break-all px-16 text-[2.25rem] font-bold leading-[1.5em] max-md:px-4 max-sm:text-[7vw]' + } + > + {icon?.value ? <div className={'view-icon'}>{icon?.value}</div> : null} + + <div className={'relative top-1.5'}> + {name || <span className={'text-text-placeholder'}>{t('menuAppHeader.defaultNewPageName')}</span>} + </div> + </div> + </div> + </div> + ); +} + +export default ViewMetaPreview; diff --git a/frontend/appflowy_web_app/src/components/view-meta/index.ts b/frontend/appflowy_web_app/src/components/view-meta/index.ts new file mode 100644 index 0000000000..9272c11393 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/view-meta/index.ts @@ -0,0 +1 @@ +export * from './ViewMetaPreview'; diff --git a/frontend/appflowy_web_app/src/pages/DatabasePage.tsx b/frontend/appflowy_web_app/src/pages/DatabasePage.tsx deleted file mode 100644 index 10e9e5c015..0000000000 --- a/frontend/appflowy_web_app/src/pages/DatabasePage.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { useId } from '@/components/_shared/context-provider/IdProvider'; -import { DatabaseHeader } from '@/components/database/components/header'; -import { useGetDatabaseId, useLoadDatabase } from '@/components/database/Database.hooks'; -import { DatabaseContextProvider } from '@/components/database/DatabaseContext'; -import CircularProgress from '@mui/material/CircularProgress'; -import React, { useCallback, useMemo } from 'react'; -import { useSearchParams } from 'react-router-dom'; -import DatabaseRow from '@/components/database/DatabaseRow'; -import Database from '@/components/database/Database'; -import RecordNotFound from 'src/components/_shared/not-found/RecordNotFound'; - -function DatabasePage() { - const { objectId } = useId() || {}; - const [search, setSearch] = useSearchParams(); - const rowId = search.get('r'); - - const viewId = search.get('v') || undefined; - const handleChangeView = useCallback( - (viewId: string) => { - setSearch({ v: viewId }); - }, - [setSearch] - ); - const handleNavigateToRow = useCallback( - (rowId: string) => { - setSearch({ r: rowId }); - }, - [setSearch] - ); - - const databaseId = useGetDatabaseId(objectId); - const rowIds = useMemo(() => (rowId ? [rowId] : undefined), [rowId]); - - const { doc, rows, notFound } = useLoadDatabase({ - databaseId, - rowIds, - }); - - if (notFound || !objectId) { - return <RecordNotFound open={notFound} />; - } - - if (!rows || !doc) { - return ( - <div className={'flex h-full w-full items-center justify-center'}> - <CircularProgress /> - </div> - ); - } - - return ( - <DatabaseContextProvider - isDatabaseRowPage={!!rowId} - navigateToRow={handleNavigateToRow} - viewId={viewId || objectId} - databaseDoc={doc} - rowDocMap={rows} - readOnly={true} - > - {rowId ? ( - <DatabaseRow rowId={rowId} /> - ) : ( - <div className={'relative flex h-full w-full flex-col'}> - <DatabaseHeader viewId={objectId} /> - <Database iidIndex={objectId} viewId={viewId || objectId} onNavigateToView={handleChangeView} /> - </div> - )} - </DatabaseContextProvider> - ); -} - -export default DatabasePage; diff --git a/frontend/appflowy_web_app/src/pages/DocumentPage.tsx b/frontend/appflowy_web_app/src/pages/DocumentPage.tsx deleted file mode 100644 index 0a9a359afc..0000000000 --- a/frontend/appflowy_web_app/src/pages/DocumentPage.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import { Document } from '@/components/document'; -import React from 'react'; - -function DocumentPage() { - return <Document />; -} - -export default DocumentPage; diff --git a/frontend/appflowy_web_app/src/pages/FolderPage.tsx b/frontend/appflowy_web_app/src/pages/FolderPage.tsx deleted file mode 100644 index 6381fe4ace..0000000000 --- a/frontend/appflowy_web_app/src/pages/FolderPage.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react'; -import { Folder } from 'src/components/folder'; - -function FolderPage() { - return <Folder />; -} - -export default FolderPage; diff --git a/frontend/appflowy_web_app/src/pages/LoginPage.tsx b/frontend/appflowy_web_app/src/pages/LoginPage.tsx deleted file mode 100644 index a4ded1d5e3..0000000000 --- a/frontend/appflowy_web_app/src/pages/LoginPage.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import React, { useEffect } from 'react'; -import Welcome from '@/components/auth/Welcome'; -import { useNavigate } from 'react-router-dom'; -import { useAppSelector } from '@/stores/store'; - -function LoginPage() { - const currentUser = useAppSelector((state) => state.currentUser); - const navigate = useNavigate(); - - useEffect(() => { - if (currentUser.isAuthenticated) { - const redirect = new URLSearchParams(window.location.search).get('redirect'); - const workspaceId = currentUser.user?.workspaceId; - - if (!redirect || redirect === '/') { - return navigate(`/view/${workspaceId}`); - } - - navigate(`${redirect}`); - } - }, [currentUser, navigate]); - return <Welcome />; -} - -export default LoginPage; diff --git a/frontend/appflowy_web_app/src/pages/ProductPage.tsx b/frontend/appflowy_web_app/src/pages/ProductPage.tsx deleted file mode 100644 index 1df649b077..0000000000 --- a/frontend/appflowy_web_app/src/pages/ProductPage.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { ViewLayout } from '@/application/collab.type'; -import { useViewLayout } from '@/application/folder-yjs'; -import { IdProvider } from '@/components/_shared/context-provider/IdProvider'; -import React, { lazy, useMemo } from 'react'; -import { useParams } from 'react-router-dom'; -import DocumentPage from '@/pages/DocumentPage'; - -const DatabasePage = lazy(() => import('./DatabasePage')); - -function ProductPage() { - const { workspaceId, objectId } = useParams(); - const type = useViewLayout(); - - const PageComponent = useMemo(() => { - switch (type) { - case ViewLayout.Document: - return DocumentPage; - case ViewLayout.Grid: - case ViewLayout.Board: - case ViewLayout.Calendar: - return DatabasePage; - default: - return null; - } - }, [type]); - - if (!workspaceId || !objectId) return null; - - return <IdProvider objectId={objectId}>{PageComponent && <PageComponent />}</IdProvider>; -} - -export default ProductPage; diff --git a/frontend/appflowy_web_app/src/pages/PublishPage.tsx b/frontend/appflowy_web_app/src/pages/PublishPage.tsx new file mode 100644 index 0000000000..b3b1ce3978 --- /dev/null +++ b/frontend/appflowy_web_app/src/pages/PublishPage.tsx @@ -0,0 +1,13 @@ +import NotFound from '@/components/error/NotFound'; +import React from 'react'; +import { useParams } from 'react-router-dom'; +import { PublishView } from '@/components/publish'; + +function PublishPage() { + const { namespace, publishName } = useParams(); + + if (!namespace || !publishName) return <NotFound />; + return <PublishView namespace={namespace} publishName={publishName} />; +} + +export default PublishPage; diff --git a/frontend/appflowy_web_app/src/stores/app/slice.ts b/frontend/appflowy_web_app/src/stores/app/slice.ts deleted file mode 100644 index dee62a6fc7..0000000000 --- a/frontend/appflowy_web_app/src/stores/app/slice.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { AFServiceConfig } from '@/application/services/services.type'; -import { createSlice, PayloadAction } from '@reduxjs/toolkit'; - -const defaultConfig: AFServiceConfig = { - cloudConfig: { - baseURL: import.meta.env.AF_BASE_URL - ? import.meta.env.AF_BASE_URL - : import.meta.env.DEV - ? 'https://test.appflowy.cloud' - : 'https://beta.appflowy.cloud', - gotrueURL: import.meta.env.AF_GOTRUE_URL - ? import.meta.env.AF_GOTRUE_URL - : import.meta.env.DEV - ? 'https://test.appflowy.cloud/gotrue' - : 'https://beta.appflowy.cloud/gotrue', - wsURL: import.meta.env.AF_WS_URL - ? import.meta.env.AF_WS_URL - : import.meta.env.DEV - ? 'wss://test.appflowy.cloud/ws/v1' - : 'wss://beta.appflowy.cloud/ws/v1', - }, -}; - -export interface AppState { - appConfig: AFServiceConfig; -} - -const initialState: AppState = { - appConfig: defaultConfig, -}; - -export const slice = createSlice({ - name: 'app', - initialState, - reducers: { - setAppConfig: (state, action: PayloadAction<AFServiceConfig>) => { - state.appConfig = action.payload; - }, - }, -}); - -export const { setAppConfig } = slice.actions; diff --git a/frontend/appflowy_web_app/src/stores/currentUser/slice.ts b/frontend/appflowy_web_app/src/stores/currentUser/slice.ts deleted file mode 100644 index ecd40a433e..0000000000 --- a/frontend/appflowy_web_app/src/stores/currentUser/slice.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { createSlice, PayloadAction } from '@reduxjs/toolkit'; -import { UserProfile, UserSetting } from '@/application/user.type'; - -export enum LoginState { - IDLE = 'idle', - LOADING = 'loading', - SUCCESS = 'success', - ERROR = 'error', -} - -export interface InitialState { - user?: UserProfile; - isAuthenticated: boolean; - userSetting?: UserSetting; - loginState?: LoginState; -} - -const initialState: InitialState = { - isAuthenticated: false, -}; - -export const currentUserSlice = createSlice({ - name: 'currentUser', - initialState: initialState, - reducers: { - updateUser: (state, action: PayloadAction<UserProfile>) => { - state.user = action.payload; - state.isAuthenticated = true; - }, - logout: (state) => { - state.user = undefined; - state.isAuthenticated = false; - }, - setUserSetting: (state, action: PayloadAction<UserSetting>) => { - state.userSetting = action.payload; - }, - loginStart: (state) => { - state.loginState = LoginState.LOADING; - }, - loginSuccess: (state) => { - state.loginState = LoginState.SUCCESS; - }, - loginError: (state) => { - state.loginState = LoginState.ERROR; - }, - resetLoginState: (state) => { - state.loginState = LoginState.IDLE; - }, - - }, -}); - -export const currentUserActions = currentUserSlice.actions; diff --git a/frontend/appflowy_web_app/src/stores/error/slice.ts b/frontend/appflowy_web_app/src/stores/error/slice.ts deleted file mode 100644 index 9b47df7777..0000000000 --- a/frontend/appflowy_web_app/src/stores/error/slice.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { createSlice, PayloadAction } from '@reduxjs/toolkit'; - -export interface IErrorOptions { - display: boolean; - message: string; -} - -const initialState: IErrorOptions = { - display: false, - message: '', -}; - -export const errorSlice = createSlice({ - name: 'error', - initialState: initialState, - reducers: { - showError(state, action: PayloadAction<string>) { - return { - display: true, - message: action.payload, - }; - }, - hideError() { - return { - display: false, - message: '', - }; - }, - }, -}); - -export const errorActions = errorSlice.actions; diff --git a/frontend/appflowy_web_app/src/stores/store.ts b/frontend/appflowy_web_app/src/stores/store.ts deleted file mode 100644 index b75363e911..0000000000 --- a/frontend/appflowy_web_app/src/stores/store.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'; -import { - configureStore, - createListenerMiddleware, - TypedStartListening, - TypedAddListener, - ListenerEffectAPI, - addListener, -} from '@reduxjs/toolkit'; -import { errorSlice } from '@/stores/error/slice'; -import { currentUserSlice } from '@/stores/currentUser/slice'; -import { slice as appSlice } from '@/stores/app/slice'; - -const listenerMiddlewareInstance = createListenerMiddleware({ - onError: () => console.error, -}); - -const store = configureStore({ - reducer: { - [appSlice.name]: appSlice.reducer, - [errorSlice.name]: errorSlice.reducer, - [currentUserSlice.name]: currentUserSlice.reducer, - }, - middleware: (gDM) => gDM({ serializableCheck: false }).prepend(listenerMiddlewareInstance.middleware), -}); - -export { store }; - -// Infer the `RootState` and `AppDispatch` types from the store itself -export type RootState = ReturnType<typeof store.getState>; -// @see https://redux-toolkit.js.org/usage/usage-with-typescript#getting-the-dispatch-type -export type AppDispatch = typeof store.dispatch; - -export type AppListenerEffectAPI = ListenerEffectAPI<RootState, AppDispatch>; - -// @see https://redux-toolkit.js.org/api/createListenerMiddleware#typescript-usage -export type AppStartListening = TypedStartListening<RootState, AppDispatch>; -export type AppAddListener = TypedAddListener<RootState, AppDispatch>; - -export const startAppListening = listenerMiddlewareInstance.startListening as AppStartListening; -export const addAppListener = addListener as AppAddListener; - -// Use throughout your app instead of plain `useDispatch` and `useSelector` -export const useAppDispatch = () => useDispatch<AppDispatch>(); -export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector; diff --git a/frontend/appflowy_web_app/src/components/layout/layout.scss b/frontend/appflowy_web_app/src/styles/app.scss similarity index 87% rename from frontend/appflowy_web_app/src/components/layout/layout.scss rename to frontend/appflowy_web_app/src/styles/app.scss index b51a842e43..f69936e2e6 100644 --- a/frontend/appflowy_web_app/src/components/layout/layout.scss +++ b/frontend/appflowy_web_app/src/styles/app.scss @@ -19,7 +19,11 @@ body { + ::selection { + @apply bg-content-blue-100; + } + @apply bg-bg-body text-text-title; &[data-os="windows"]:not([data-browser="firefox"]) { .appflowy-custom-scroller { @include mixin.hidden-scrollbar @@ -47,10 +51,13 @@ body { opacity: 60%; } +.icon { + font-family: 'Apple Color Emoji', 'Noto Color Emoji', 'Segoe UI Emoji', 'Twemoji Mozilla', sans-serif; +} .view-icon { @apply flex w-fit leading-[1.5em] cursor-pointer rounded-lg py-2 text-[1.5em]; - font-family: "Apple Color Emoji", "Segoe UI Emoji", NotoColorEmoji, "Noto Color Emoji", "Segoe UI Symbol", "Android Emoji", EmojiSymbols; + font-family: 'Apple Color Emoji', 'Noto Color Emoji', 'Segoe UI Emoji', 'Twemoji Mozilla', sans-serif; line-height: 1em; white-space: nowrap; } diff --git a/frontend/appflowy_web_app/src/styles/variables/dark.variables.css b/frontend/appflowy_web_app/src/styles/variables/dark.variables.css index de8fcf9824..373ef8ee79 100644 --- a/frontend/appflowy_web_app/src/styles/variables/dark.variables.css +++ b/frontend/appflowy_web_app/src/styles/variables/dark.variables.css @@ -19,7 +19,7 @@ --fill-toolbar: #0F111C; --fill-selector: #232b38; --fill-list-active: #3c4557; - --fill-list-hover: #005174; + --fill-list-hover: rgba(255, 255, 255, 0.1); --content-blue-400: #00bcf0; --content-blue-300: #52d1f4; --content-blue-600: #009fd1; @@ -55,4 +55,5 @@ --gradient5: linear-gradient(56.2deg, #5749CA 0%, #BB4A97 100%); --gradient6: linear-gradient(180deg, #036FFA 0%, #00B8E5 100%); --gradient7: linear-gradient(38.2deg, #F0C6CF 0%, #DECCE2 40.4754%, #CAD3F9 100%); + --header: rgba(0, 0, 0, 0.7); } \ No newline at end of file diff --git a/frontend/appflowy_web_app/src/styles/variables/light.variables.css b/frontend/appflowy_web_app/src/styles/variables/light.variables.css index cd4ffee0f6..613577ef71 100644 --- a/frontend/appflowy_web_app/src/styles/variables/light.variables.css +++ b/frontend/appflowy_web_app/src/styles/variables/light.variables.css @@ -20,7 +20,7 @@ --fill-hover: #52d1f4; --fill-pressed: #009fd1; --fill-active: #e0f8ff; - --fill-list-hover: #e0f8ff; + --fill-list-hover: rgba(31, 35, 41, 6%); --fill-list-active: #f9fafd; --content-blue-400: #00bcf0; --content-blue-300: #52d1f4; @@ -58,4 +58,5 @@ --gradient5: linear-gradient(56.2deg, #5749CA 0%, #BB4A97 100%); --gradient6: linear-gradient(180deg, #036FFA 0%, #00B8E5 100%); --gradient7: linear-gradient(38.2deg, #F0C6CF 0%, #DECCE2 40.4754%, #CAD3F9 100%); + --header: rgba(255, 255, 255, 0.8); } \ No newline at end of file diff --git a/frontend/appflowy_web_app/src/utils/copy.ts b/frontend/appflowy_web_app/src/utils/copy.ts new file mode 100644 index 0000000000..ce3d6aa1e4 --- /dev/null +++ b/frontend/appflowy_web_app/src/utils/copy.ts @@ -0,0 +1,3 @@ +export async function copyTextToClipboard(text: string) { + await navigator.clipboard.writeText(text); +} diff --git a/frontend/appflowy_web_app/src/utils/hotkeys.ts b/frontend/appflowy_web_app/src/utils/hotkeys.ts new file mode 100644 index 0000000000..20aa05db27 --- /dev/null +++ b/frontend/appflowy_web_app/src/utils/hotkeys.ts @@ -0,0 +1,134 @@ +import isHotkey from 'is-hotkey'; + +export const isMac = () => { + return navigator.userAgent.includes('Mac OS X'); +}; + +const MODIFIERS = { + control: 'Ctrl', + meta: '⌘', +}; + +export const getModifier = () => { + return isMac() ? MODIFIERS.meta : MODIFIERS.control; +}; + +export enum HOT_KEY_NAME { + LEFT = 'left', + RIGHT = 'right', + SELECT_ALL = 'select-all', + ESCAPE = 'escape', + ALIGN_LEFT = 'align-left', + ALIGN_CENTER = 'align-center', + ALIGN_RIGHT = 'align-right', + BOLD = 'bold', + ITALIC = 'italic', + UNDERLINE = 'underline', + STRIKETHROUGH = 'strikethrough', + CODE = 'code', + TOGGLE_TODO = 'toggle-todo', + TOGGLE_COLLAPSE = 'toggle-collapse', + INDENT_BLOCK = 'indent-block', + OUTDENT_BLOCK = 'outdent-block', + INSERT_SOFT_BREAK = 'insert-soft-break', + SPLIT_BLOCK = 'split-block', + BACKSPACE = 'backspace', + OPEN_LINK = 'open-link', + OPEN_LINKS = 'open-links', + EXTEND_LINE_BACKWARD = 'extend-line-backward', + EXTEND_LINE_FORWARD = 'extend-line-forward', + PASTE = 'paste', + PASTE_PLAIN_TEXT = 'paste-plain-text', + HIGH_LIGHT = 'high-light', + EXTEND_DOCUMENT_BACKWARD = 'extend-document-backward', + EXTEND_DOCUMENT_FORWARD = 'extend-document-forward', + SCROLL_TO_TOP = 'scroll-to-top', + SCROLL_TO_BOTTOM = 'scroll-to-bottom', + FORMAT_LINK = 'format-link', + FIND_REPLACE = 'find-replace', + /** + * Navigation + */ + TOGGLE_THEME = 'toggle-theme', + TOGGLE_SIDEBAR = 'toggle-sidebar', +} + +const defaultHotKeys = { + [HOT_KEY_NAME.ALIGN_LEFT]: ['control+shift+l'], + [HOT_KEY_NAME.ALIGN_CENTER]: ['control+shift+e'], + [HOT_KEY_NAME.ALIGN_RIGHT]: ['control+shift+r'], + [HOT_KEY_NAME.BOLD]: ['mod+b'], + [HOT_KEY_NAME.ITALIC]: ['mod+i'], + [HOT_KEY_NAME.UNDERLINE]: ['mod+u'], + [HOT_KEY_NAME.STRIKETHROUGH]: ['mod+shift+s', 'mod+shift+x'], + [HOT_KEY_NAME.CODE]: ['mod+e'], + [HOT_KEY_NAME.TOGGLE_TODO]: ['mod+enter'], + [HOT_KEY_NAME.TOGGLE_COLLAPSE]: ['mod+enter'], + [HOT_KEY_NAME.SELECT_ALL]: ['mod+a'], + [HOT_KEY_NAME.ESCAPE]: ['esc'], + [HOT_KEY_NAME.INDENT_BLOCK]: ['tab'], + [HOT_KEY_NAME.OUTDENT_BLOCK]: ['shift+tab'], + [HOT_KEY_NAME.SPLIT_BLOCK]: ['enter'], + [HOT_KEY_NAME.INSERT_SOFT_BREAK]: ['shift+enter'], + [HOT_KEY_NAME.BACKSPACE]: ['backspace', 'shift+backspace'], + [HOT_KEY_NAME.OPEN_LINK]: ['opt+enter'], + [HOT_KEY_NAME.OPEN_LINKS]: ['opt+shift+enter'], + [HOT_KEY_NAME.EXTEND_LINE_BACKWARD]: ['opt+shift+left'], + [HOT_KEY_NAME.EXTEND_LINE_FORWARD]: ['opt+shift+right'], + [HOT_KEY_NAME.PASTE]: ['mod+v'], + [HOT_KEY_NAME.PASTE_PLAIN_TEXT]: ['mod+shift+v'], + [HOT_KEY_NAME.HIGH_LIGHT]: ['mod+shift+h'], + [HOT_KEY_NAME.EXTEND_DOCUMENT_BACKWARD]: ['mod+shift+up'], + [HOT_KEY_NAME.EXTEND_DOCUMENT_FORWARD]: ['mod+shift+down'], + [HOT_KEY_NAME.SCROLL_TO_TOP]: ['home'], + [HOT_KEY_NAME.SCROLL_TO_BOTTOM]: ['end'], + [HOT_KEY_NAME.TOGGLE_THEME]: ['mod+shift+l'], + [HOT_KEY_NAME.TOGGLE_SIDEBAR]: ['mod+.'], + [HOT_KEY_NAME.FORMAT_LINK]: ['mod+k'], + [HOT_KEY_NAME.LEFT]: ['left'], + [HOT_KEY_NAME.RIGHT]: ['right'], + [HOT_KEY_NAME.FIND_REPLACE]: ['mod+f'], +}; + +const replaceModifier = (hotkey: string) => { + return hotkey.replace('mod', getModifier()).replace('control', 'ctrl'); +}; + +/** + * Create a hotkey checker. + * @example trigger strike through when user press "Cmd + Shift + S" or "Cmd + Shift + X" + * @param hotkeyName + * @param customHotKeys + */ +export const createHotkey = (hotkeyName: HOT_KEY_NAME, customHotKeys?: Record<HOT_KEY_NAME, string[]>) => { + const keys = customHotKeys || defaultHotKeys; + const hotkeys = keys[hotkeyName]; + + return (event: KeyboardEvent) => { + return hotkeys.some((hotkey) => { + return isHotkey(hotkey, event); + }); + }; +}; + +/** + * Create a hotkey label. + * eg. "Ctrl + B / ⌘ + B" + * @param hotkeyName + * @param customHotKeys + */ +export const createHotKeyLabel = (hotkeyName: HOT_KEY_NAME, customHotKeys?: Record<HOT_KEY_NAME, string[]>) => { + const keys = customHotKeys || defaultHotKeys; + const hotkeys = keys[hotkeyName].map((key) => replaceModifier(key)); + + return hotkeys + .map((hotkey) => + hotkey + .split('+') + .map((key) => { + return key === ' ' ? 'Space' : key.charAt(0).toUpperCase() + key.slice(1); + }) + .join(' + ') + ) + .join(' / '); +}; diff --git a/frontend/appflowy_web_app/src/vite-env.d.ts b/frontend/appflowy_web_app/src/vite-env.d.ts index 5748ee1aed..2ec03900db 100644 --- a/frontend/appflowy_web_app/src/vite-env.d.ts +++ b/frontend/appflowy_web_app/src/vite-env.d.ts @@ -3,10 +3,41 @@ /// <reference types="vite-plugin-terminal/client" /> /// <reference types="cypress" /> /// <reference types="cypress-plugin-tab" /> + interface Window { refresh_token: (token: string) => void; invalid_token: () => void; WebFont?: { load: (options: { google: { families: string[] } }) => void; }; + toast: { + success: (message: string) => void; + error: (message: string) => void; + info: (message: string) => void; + clear: () => void; + default: (message: string) => void; + warning: (message: string) => void; + }; + + Prism: { + tokenize: (text: string, grammar: Prism.Grammar) => Prism.Token[]; + languages: Record<string, Prism.Grammar>; + plugins: { + autoloader: { + languages_path: string; + }; + }; + }; + hljs: { + highlightAuto: (code: string) => { language: string }; + }; +} + +namespace Prism { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + interface Token { + type: string; + content: string | Token[]; + length: number; + } } diff --git a/frontend/appflowy_web_app/test.env b/frontend/appflowy_web_app/test.env deleted file mode 100644 index 89e2936fab..0000000000 --- a/frontend/appflowy_web_app/test.env +++ /dev/null @@ -1,3 +0,0 @@ -AF_WS_URL=wss://test.appflowy.cloud/ws/v1 -AF_BASE_URL=https://test.appflowy.cloud -AF_GOTRUE_URL=https://test.appflowy.cloud/gotrue \ No newline at end of file diff --git a/frontend/appflowy_web_app/tsconfig.json b/frontend/appflowy_web_app/tsconfig.json index 875f06f3e5..96bf51d65b 100644 --- a/frontend/appflowy_web_app/tsconfig.json +++ b/frontend/appflowy_web_app/tsconfig.json @@ -47,7 +47,8 @@ "src", "vite.config.ts", "cypress.config.ts", - "cypress" + "cypress", + "deploy/server.ts" ], "exclude": [ "node_modules", diff --git a/frontend/resources/flowy_icons/16x/export_html.svg b/frontend/resources/flowy_icons/16x/export_html.svg new file mode 100644 index 0000000000..02ae35a2b4 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/export_html.svg @@ -0,0 +1,4 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M2.73332 1.33337H13.2667C13.6667 1.33337 14 1.66671 13.9333 2.06671L12.7333 12.8667C12.7333 13.1334 12.5333 13.3334 12.2667 13.4667L8.19998 14.6C8.06665 14.6667 7.93332 14.6667 7.86665 14.6L3.79998 13.4667C3.53332 13.4 3.33332 13.2 3.33332 12.8667L2.06665 2.06671C2.06665 1.66671 2.33332 1.33337 2.73332 1.33337Z" stroke="#171717" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M10.8 4.53333H5.19995L5.46662 7.46666H10.5333L10.1333 10.8L7.86662 11.4667L5.46662 10.8V9.46666" stroke="#171717" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/> +</svg> diff --git a/frontend/resources/flowy_icons/16x/export_markdown.svg b/frontend/resources/flowy_icons/16x/export_markdown.svg new file mode 100644 index 0000000000..a40e5baa26 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/export_markdown.svg @@ -0,0 +1,6 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M13.4071 4.26416L10.593 1.45009C10.0304 0.887482 9.26736 0.571411 8.47171 0.571411H3.71436C2.60979 0.571411 1.71436 1.46684 1.71436 2.57141V13.0938C1.71436 14.1984 2.60978 15.0938 3.71435 15.0938H12.2858C13.3904 15.0938 14.2858 14.1984 14.2858 13.0938V6.38548C14.2858 5.58983 13.9697 4.82677 13.4071 4.26416Z" stroke="#171717" stroke-width="0.995556" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M4.19995 12V7.70621C4.19995 7.55289 4.32425 7.42859 4.47757 7.42859C4.58683 7.42859 4.68593 7.49267 4.73075 7.5923L5.59171 9.50585C5.64879 9.63271 5.77497 9.7143 5.91409 9.7143C6.05319 9.7143 6.17936 9.63273 6.23644 9.50588L7.09768 7.59228C7.14252 7.49266 7.24161 7.42859 7.35085 7.42859C7.50419 7.42859 7.6285 7.55289 7.6285 7.70622V12" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M10.8572 7.42859V11.9999" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M9.5 10.8572L10.8 12.1572L12.1 10.8572" stroke="#171717" stroke-width="0.9" stroke-linecap="round" stroke-linejoin="round"/> +</svg> diff --git a/frontend/resources/flowy_icons/16x/published_checkmark.svg b/frontend/resources/flowy_icons/16x/published_checkmark.svg new file mode 100644 index 0000000000..097ace0524 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/published_checkmark.svg @@ -0,0 +1,11 @@ +<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg"> +<g clip-path="url(#clip0_391_9245)"> +<circle cx="6.99957" cy="6.99981" r="6.22222" fill="#00BCF0"/> +<path d="M4.27734 7.38884L6.37136 9.33329L10.1107 5.4444" stroke="white" stroke-width="1.2963" stroke-linecap="round" stroke-linejoin="round"/> +</g> +<defs> +<clipPath id="clip0_391_9245"> +<rect width="14" height="14" fill="white"/> +</clipPath> +</defs> +</svg> diff --git a/frontend/resources/flowy_icons/16x/share_publish.svg b/frontend/resources/flowy_icons/16x/share_publish.svg new file mode 100644 index 0000000000..345208ab90 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/share_publish.svg @@ -0,0 +1,3 @@ +<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M9 1.6875C13.0387 1.6875 16.3125 4.96125 16.3125 9C16.3125 13.0387 13.0387 16.3125 9 16.3125C4.96125 16.3125 1.6875 13.0387 1.6875 9C1.6875 4.96125 4.96125 1.6875 9 1.6875ZM8.4375 9.5625L6.1995 9.56288C6.21525 9.92288 6.24563 10.2731 6.2895 10.6117L6.32438 10.863L6.36787 11.1311C6.71512 13.1156 7.52325 14.607 8.43787 15.051L8.4375 9.5625ZM11.8005 9.56288L9.5625 9.5625V15.051C10.4565 14.6164 11.2489 13.1813 11.6081 11.2628L11.6321 11.1311L11.6756 10.8634C11.7402 10.4324 11.7819 9.99828 11.8005 9.56288ZM5.07337 9.56288H2.83763C3.04238 11.8316 4.47225 13.7479 6.4605 14.6441C6.07163 14.0415 5.751 13.3009 5.5155 12.4639L5.46225 12.2689L5.40038 12.0199L5.34337 11.7656C5.19117 11.0403 5.10084 10.3034 5.07337 9.56288ZM15.1624 9.56288H12.9262C12.9014 10.2245 12.827 10.8834 12.7035 11.5339L12.6562 11.7656L12.5992 12.0199L12.5374 12.2692C12.2974 13.1858 11.958 13.9954 11.5391 14.6441C13.5274 13.7479 14.9573 11.8316 15.1616 9.56288H15.1624ZM6.4605 3.3555L6.41025 3.37838C4.4475 4.28437 3.0405 6.1875 2.83763 8.4375H5.07337C5.10037 7.75125 5.1765 7.08938 5.29613 6.46613L5.34337 6.23438L5.40038 5.98012L5.46225 5.73075C5.70225 4.81425 6.04163 4.00463 6.4605 3.35588V3.3555ZM8.4375 2.949C7.54612 3.38175 6.75562 4.8105 6.39488 6.72113L6.36787 6.86888L6.32438 7.13662C6.25977 7.56774 6.21809 8.00197 6.1995 8.4375H8.4375V2.949ZM11.5395 3.35588L11.5845 3.42712C11.952 4.01475 12.2565 4.72762 12.4826 5.52863L12.5378 5.73112L12.5996 5.98012L12.6566 6.23438C12.8036 6.92438 12.8963 7.66538 12.9266 8.4375H15.1624C14.958 6.16838 13.5281 4.25213 11.5395 3.35625V3.35588ZM9.5625 2.949V8.4375H11.8005C11.7849 8.07668 11.7537 7.71672 11.7067 7.35862L11.6756 7.13662L11.6321 6.86888C11.2849 4.88475 10.4771 3.39338 9.56288 2.949H9.5625Z" fill="#171717"/> +</svg> diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 6c37938d97..17c7b750b3 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -82,7 +82,7 @@ "reachOut": "Reach out on Discord" }, "menuTitle": "Workspaces", - "deleteWorkspaceHintText": "Are you sure you want to delete the workspace? This action cannot be undone.", + "deleteWorkspaceHintText": "Are you sure you want to delete the workspace? This action cannot be undone, and any pages you have published will be unpublished.", "createSuccess": "Workspace created successfully", "createFailed": "Failed to create workspace", "createLimitExceeded": "You've reached the maximum workspace limit allowed for your account. If you need additional workspaces to continue your work, please request on Github", @@ -106,7 +106,15 @@ "html": "HTML", "clipboard": "Copy to clipboard", "csv": "CSV", - "copyLink": "Copy Link" + "copyLink": "Copy Link", + "publishToTheWeb": "Publish to Web", + "publishToTheWebHint": "Create a website with AppFlowy", + "publish": "Publish", + "unPublish": "Unpublish", + "visitSite": "Visit site", + "exportAsTab": "Export as", + "publishTab": "Publish", + "shareTab": "Share" }, "moreAction": { "small": "small", @@ -434,7 +442,7 @@ }, "deleteWorkspacePrompt": { "title": "Delete workspace", - "content": "Are you sure you want to delete this workspace? This action cannot be undone." + "content": "Are you sure you want to delete this workspace? This action cannot be undone, and any pages you have published will be unpublished." }, "leaveWorkspacePrompt": { "title": "Leave workspace", @@ -1373,6 +1381,9 @@ "imageUploadFailed": "Image upload failed", "errorCode": "Error code" }, + "math": { + "copiedToPasteBoard": "The math equation has been copied to the clipboard" + }, "urlPreview": { "copiedToPasteBoard": "The link has been copied to the clipboard", "convertToLink": "Convert to embed link" @@ -2000,7 +2011,7 @@ "space": { "delete": "Delete", "deleteConfirmation": "Delete: ", - "deleteConfirmationDescription": "All pages within this Space will be deleted and moved to Trash.", + "deleteConfirmationDescription": "All pages within this Space will be deleted and moved to the Trash, and any published pages will be unpublished.", "rename": "Rename Space", "changeIcon": "Change icon", "manage": "Manage Space", @@ -2027,8 +2038,30 @@ "upgrade": "Update", "upgradeYourSpace": "Create multiple Spaces", "quicklySwitch": "Quickly switch to the next space", + "duplicate": "Duplicate Space", "movePageToSpace": "Move page to space", "switchSpace": "Switch space" + }, + "publish": { + "hasNotBeenPublished": "This page hasn't been published yet", + "reportPage": "Report page", + "databaseHasNotBeenPublished": "Publishing a database is not supported yet.", + "createdWith": "Created with", + "downloadApp": "Download AppFlowy", + "copy": { + "codeBlock": "The content of code block has been copied to the clipboard", + "imageBlock": "The image link has been copied to the clipboard", + "mathBlock": "The math equation has been copied to the clipboard" + }, + "containsPublishedPage": "This page contains one or more published pages. If you continue, they will be unpublished. Do you want to proceed with deletion?", + "publishSuccessfully": "Published successfully", + "unpublishSuccessfully": "Unpublished successfully", + "publishFailed": "Failed to publish", + "unpublishFailed": "Failed to unpublish", + "noAccessToVisit": "No access to this page...", + "createWithAppFlowy": "Create a website with AppFlowy", + "fastWithAI": "Fast and easy with AI.", + "tryItNow": "Try it now" } } diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index b84e18e1db..ccc9df04ff 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -2033,12 +2033,14 @@ dependencies = [ "flowy-notification", "flowy-search-pub", "flowy-sqlite", + "futures", "lazy_static", "lib-dispatch", "lib-infra", "nanoid", "parking_lot 0.12.1", "protobuf", + "regex", "serde", "serde_json", "strum_macros 0.21.1", @@ -2059,6 +2061,7 @@ dependencies = [ "collab-entity", "collab-folder", "lib-infra", + "serde", "tokio", "uuid", ] diff --git a/frontend/rust-lib/event-integration-test/src/document/document_event.rs b/frontend/rust-lib/event-integration-test/src/document/document_event.rs index 4203dcc88c..8c278084e3 100644 --- a/frontend/rust-lib/event-integration-test/src/document/document_event.rs +++ b/frontend/rust-lib/event-integration-test/src/document/document_event.rs @@ -49,6 +49,19 @@ impl DocumentEventTest { guard.encode_collab().unwrap() } + pub async fn get_encoded_collab(&self, doc_id: &str) -> EncodedCollabPB { + let core = &self.event_test; + let payload = OpenDocumentPayloadPB { + document_id: doc_id.to_string(), + }; + EventBuilder::new(core.clone()) + .event(DocumentEvent::GetDocEncodedCollab) + .payload(payload) + .async_send() + .await + .parse::<EncodedCollabPB>() + } + pub async fn create_document(&self) -> ViewPB { let core = &self.event_test; let current_workspace = core.get_current_workspace().await; diff --git a/frontend/rust-lib/event-integration-test/src/document_event.rs b/frontend/rust-lib/event-integration-test/src/document_event.rs index 6194e10ab7..28f27bdedd 100644 --- a/frontend/rust-lib/event-integration-test/src/document_event.rs +++ b/frontend/rust-lib/event-integration-test/src/document_event.rs @@ -65,6 +65,7 @@ impl EventIntegrationTest { view } + pub async fn open_document(&self, doc_id: String) -> OpenDocumentData { let payload = OpenDocumentPayloadPB { document_id: doc_id.clone(), diff --git a/frontend/rust-lib/event-integration-test/src/folder_event.rs b/frontend/rust-lib/event-integration-test/src/folder_event.rs index 6168d4bd41..690cb81534 100644 --- a/frontend/rust-lib/event-integration-test/src/folder_event.rs +++ b/frontend/rust-lib/event-integration-test/src/folder_event.rs @@ -1,3 +1,4 @@ +use collab::entity::EncodedCollab; use std::sync::Arc; use collab_folder::{FolderData, View}; @@ -5,6 +6,7 @@ use flowy_folder::entities::icon::UpdateViewIconPayloadPB; use flowy_folder::event_map::FolderEvent; use flowy_folder::event_map::FolderEvent::*; use flowy_folder::{entities::*, ViewLayout}; +use flowy_folder_pub::entities::PublishViewPayload; use flowy_search::services::manager::{SearchHandler, SearchType}; use flowy_user::entities::{ AcceptWorkspaceInvitationPB, QueryWorkspacePB, RemoveWorkspaceMemberPB, @@ -172,6 +174,30 @@ impl EventIntegrationTest { folder.get_folder_data(&workspace_id).clone().unwrap() } + pub async fn get_publish_payload( + &self, + view_id: &str, + include_children: Option<bool>, + ) -> Vec<PublishViewPayload> { + let manager = self.folder_manager.clone(); + let payload = manager + .get_batch_publish_payload(view_id, None, include_children) + .await; + + if payload.is_err() { + panic!("Get publish payload failed") + } + + payload.unwrap() + } + + pub async fn encoded_collab_v1(&self, view_id: &str, layout: ViewLayout) -> EncodedCollab { + let manager = self.folder_manager.clone(); + let handlers = manager.get_operation_handlers(); + let handler = handlers.get(&layout).unwrap(); + handler.encoded_collab_v1(view_id, layout).await.unwrap() + } + pub async fn get_all_workspace_views(&self) -> Vec<ViewPB> { EventBuilder::new(self.clone()) .event(FolderEvent::ReadCurrentWorkspaceViews) diff --git a/frontend/rust-lib/event-integration-test/tests/document/local_test/edit_test.rs b/frontend/rust-lib/event-integration-test/tests/document/local_test/edit_test.rs index cfcefeb506..199c1b43c2 100644 --- a/frontend/rust-lib/event-integration-test/tests/document/local_test/edit_test.rs +++ b/frontend/rust-lib/event-integration-test/tests/document/local_test/edit_test.rs @@ -19,6 +19,16 @@ async fn get_document_event_test() { assert!(document_data.blocks.len() > 1); } +#[tokio::test] +async fn get_encoded_collab_event_test() { + let test = DocumentEventTest::new().await; + let view = test.create_document().await; + let doc_id = view.id.clone(); + let encoded_v1 = test.get_encoded_collab(&doc_id).await; + assert!(!encoded_v1.doc_state.is_empty()); + assert!(!encoded_v1.state_vector.is_empty()); +} + #[tokio::test] async fn apply_document_event_test() { let test = DocumentEventTest::new().await; diff --git a/frontend/rust-lib/event-integration-test/tests/folder/local_test/mod.rs b/frontend/rust-lib/event-integration-test/tests/folder/local_test/mod.rs index aa58a02baf..2c48d266f7 100644 --- a/frontend/rust-lib/event-integration-test/tests/folder/local_test/mod.rs +++ b/frontend/rust-lib/event-integration-test/tests/folder/local_test/mod.rs @@ -3,3 +3,4 @@ mod import_test; mod script; mod subscription_test; mod test; +mod view_publish_test; diff --git a/frontend/rust-lib/event-integration-test/tests/folder/local_test/view_publish_test.rs b/frontend/rust-lib/event-integration-test/tests/folder/local_test/view_publish_test.rs new file mode 100644 index 0000000000..6e6e6fa9cb --- /dev/null +++ b/frontend/rust-lib/event-integration-test/tests/folder/local_test/view_publish_test.rs @@ -0,0 +1,192 @@ +use collab_folder::ViewLayout; +use event_integration_test::EventIntegrationTest; +use flowy_folder::entities::{ViewLayoutPB, ViewPB}; +use flowy_folder::publish_util::generate_publish_name; +use flowy_folder_pub::entities::{ + PublishViewInfo, PublishViewMeta, PublishViewMetaData, PublishViewPayload, +}; + +async fn mock_single_document_view_publish_payload( + test: &EventIntegrationTest, + view: &ViewPB, + publish_name: String, +) -> Vec<PublishViewPayload> { + let view_id = &view.id; + let layout: ViewLayout = view.layout.clone().into(); + let view_encoded_collab = test.encoded_collab_v1(view_id, layout).await; + let publish_view_info = PublishViewInfo { + view_id: view_id.to_string(), + name: view.name.to_string(), + icon: None, + layout: ViewLayout::Document, + extra: None, + created_by: view.created_by, + last_edited_by: view.last_edited_by, + last_edited_time: view.last_edited, + created_at: view.create_time, + child_views: None, + }; + + vec![PublishViewPayload { + meta: PublishViewMeta { + metadata: PublishViewMetaData { + view: publish_view_info.clone(), + child_views: vec![], + ancestor_views: vec![publish_view_info], + }, + view_id: view_id.to_string(), + publish_name, + }, + data: Vec::from(view_encoded_collab.doc_state), + }] +} + +async fn mock_nested_document_view_publish_payload( + test: &EventIntegrationTest, + view: &ViewPB, + publish_name: String, +) -> Vec<PublishViewPayload> { + let view_id = &view.id; + let layout: ViewLayout = view.layout.clone().into(); + let view_encoded_collab = test.encoded_collab_v1(view_id, layout).await; + let publish_view_info = PublishViewInfo { + view_id: view_id.to_string(), + name: view.name.to_string(), + icon: None, + layout: ViewLayout::Document, + extra: None, + created_by: view.created_by, + last_edited_by: view.last_edited_by, + last_edited_time: view.last_edited, + created_at: view.create_time, + child_views: None, + }; + + let child_view_id = &view.child_views[0].id; + let child_view = test.get_view(child_view_id).await; + let child_layout: ViewLayout = child_view.layout.clone().into(); + let child_view_encoded_collab = test.encoded_collab_v1(child_view_id, child_layout).await; + let child_publish_view_info = PublishViewInfo { + view_id: child_view_id.to_string(), + name: child_view.name.to_string(), + icon: None, + layout: ViewLayout::Document, + extra: None, + created_by: child_view.created_by, + last_edited_by: child_view.last_edited_by, + last_edited_time: child_view.last_edited, + created_at: child_view.create_time, + child_views: None, + }; + let child_publish_name = generate_publish_name(&child_view.id, &child_view.name); + + vec![ + PublishViewPayload { + meta: PublishViewMeta { + metadata: PublishViewMetaData { + view: publish_view_info.clone(), + child_views: vec![child_publish_view_info.clone()], + ancestor_views: vec![publish_view_info.clone()], + }, + view_id: view_id.to_string(), + publish_name, + }, + data: Vec::from(view_encoded_collab.doc_state), + }, + PublishViewPayload { + meta: PublishViewMeta { + metadata: PublishViewMetaData { + view: child_publish_view_info.clone(), + child_views: vec![], + ancestor_views: vec![publish_view_info.clone(), child_publish_view_info.clone()], + }, + view_id: child_view_id.to_string(), + publish_name: child_publish_name, + }, + data: Vec::from(child_view_encoded_collab.doc_state), + }, + ] +} + +async fn create_single_document(test: &EventIntegrationTest, view_id: &str, name: &str) { + test + .create_orphan_view(name, view_id, ViewLayoutPB::Document) + .await; +} + +async fn create_nested_document(test: &EventIntegrationTest, view_id: &str, name: &str) { + create_single_document(test, view_id, name).await; + let child_name = "Child View"; + test.create_view(view_id, child_name.to_string()).await; +} +#[tokio::test] +async fn single_document_get_publish_view_payload_test() { + let test = EventIntegrationTest::new_anon().await; + let view_id = "20240521"; + let name = "Orphan View"; + create_single_document(&test, view_id, name).await; + let view = test.get_view(view_id).await; + let payload = test.get_publish_payload(view_id, Some(true)).await; + + let expect_payload = mock_single_document_view_publish_payload( + &test, + &view, + format!("{}-{}", "Orphan-View", view_id), + ) + .await; + + assert_eq!(payload, expect_payload); +} + +#[tokio::test] +async fn nested_document_get_publish_view_payload_test() { + let test = EventIntegrationTest::new_anon().await; + let name = "Orphan View"; + let view_id = "20240521"; + create_nested_document(&test, view_id, name).await; + let view = test.get_view(view_id).await; + let payload = test.get_publish_payload(view_id, Some(true)).await; + + let expect_payload = mock_nested_document_view_publish_payload( + &test, + &view, + format!("{}-{}", "Orphan-View", view_id), + ) + .await; + + assert_eq!(payload.len(), 2); + assert_eq!(payload, expect_payload); +} + +#[tokio::test] +async fn no_children_publish_view_payload_test() { + let test = EventIntegrationTest::new_anon().await; + let name = "Orphan View"; + let view_id = "20240521"; + create_nested_document(&test, view_id, name).await; + let view = test.get_view(view_id).await; + let payload = test.get_publish_payload(view_id, Some(false)).await; + + let data = mock_single_document_view_publish_payload( + &test, + &view, + format!("{}-{}", "Orphan-View", view_id), + ) + .await + .iter() + .map(|p| p.data.clone()) + .collect::<Vec<_>>(); + let meta = mock_nested_document_view_publish_payload( + &test, + &view, + format!("{}-{}", "Orphan-View", view_id), + ) + .await + .iter() + .map(|p| p.meta.clone()) + .collect::<Vec<_>>(); + + assert_eq!(payload.len(), 1); + assert_eq!(&payload[0].data, &data[0]); + assert_eq!(&payload[0].meta, &meta[0]); +} diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps.rs index 81739a51d8..cba9ea7d45 100644 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps.rs +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps.rs @@ -198,6 +198,21 @@ impl FolderOperationHandler for DocumentFolderOperation { }) } + fn encoded_collab_v1( + &self, + view_id: &str, + layout: ViewLayout, + ) -> FutureResult<EncodedCollab, FlowyError> { + debug_assert_eq!(layout, ViewLayout::Document); + let view_id = view_id.to_string(); + let manager = self.0.clone(); + FutureResult::new(async move { + let encoded_collab = manager.encode_collab(&view_id).await?; + + Ok(encoded_collab) + }) + } + /// Create a view with built-in data. fn create_built_in_view( &self, @@ -285,6 +300,15 @@ impl FolderOperationHandler for DatabaseFolderOperation { }) } + fn encoded_collab_v1( + &self, + _view_id: &str, + _layout: ViewLayout, + ) -> FutureResult<EncodedCollab, FlowyError> { + // Database view doesn't support collab + FutureResult::new(async move { Err(FlowyError::not_support()) }) + } + fn duplicate_view(&self, view_id: &str) -> FutureResult<Bytes, FlowyError> { let database_manager = self.0.clone(); let view_id = view_id.to_owned(); @@ -543,4 +567,13 @@ impl FolderOperationHandler for ChatFolderOperation { ) -> FutureResult<(), FlowyError> { FutureResult::new(async move { Err(FlowyError::not_support()) }) } + + fn encoded_collab_v1( + &self, + _view_id: &str, + _layout: ViewLayout, + ) -> FutureResult<EncodedCollab, FlowyError> { + // Chat view doesn't support collab + FutureResult::new(async move { Err(FlowyError::not_support()) }) + } } diff --git a/frontend/rust-lib/flowy-core/src/integrate/trait_impls.rs b/frontend/rust-lib/flowy-core/src/integrate/trait_impls.rs index b5decb151b..80fd3c2523 100644 --- a/frontend/rust-lib/flowy-core/src/integrate/trait_impls.rs +++ b/frontend/rust-lib/flowy-core/src/integrate/trait_impls.rs @@ -30,6 +30,7 @@ use flowy_error::{FlowyError, FlowyResult}; use flowy_folder_pub::cloud::{ FolderCloudService, FolderCollabParams, FolderData, FolderSnapshot, Workspace, WorkspaceRecord, }; +use flowy_folder_pub::entities::{PublishInfoResponse, PublishViewPayload}; use flowy_server_pub::af_cloud_config::AFCloudConfiguration; use flowy_server_pub::supabase_config::SupabaseConfiguration; use flowy_storage_pub::cloud::{ObjectIdentity, ObjectValue, StorageCloudService}; @@ -300,6 +301,65 @@ impl FolderCloudService for ServerProvider { .map(|provider| provider.folder_service().service_name()) .unwrap_or_default() } + + fn publish_view( + &self, + workspace_id: &str, + payload: Vec<PublishViewPayload>, + ) -> FutureResult<(), Error> { + let workspace_id = workspace_id.to_string(); + let server = self.get_server(); + FutureResult::new(async move { + server? + .folder_service() + .publish_view(&workspace_id, payload) + .await + }) + } + + fn unpublish_views(&self, workspace_id: &str, view_ids: Vec<String>) -> FutureResult<(), Error> { + let workspace_id = workspace_id.to_string(); + let server = self.get_server(); + FutureResult::new(async move { + server? + .folder_service() + .unpublish_views(&workspace_id, view_ids) + .await + }) + } + + fn get_publish_info(&self, view_id: &str) -> FutureResult<PublishInfoResponse, Error> { + let view_id = view_id.to_string(); + let server = self.get_server(); + FutureResult::new(async move { server?.folder_service().get_publish_info(&view_id).await }) + } + + fn set_publish_namespace( + &self, + workspace_id: &str, + new_namespace: &str, + ) -> FutureResult<(), Error> { + let workspace_id = workspace_id.to_string(); + let new_namespace = new_namespace.to_string(); + let server = self.get_server(); + FutureResult::new(async move { + server? + .folder_service() + .set_publish_namespace(&workspace_id, &new_namespace) + .await + }) + } + + fn get_publish_namespace(&self, workspace_id: &str) -> FutureResult<String, Error> { + let workspace_id = workspace_id.to_string(); + let server = self.get_server(); + FutureResult::new(async move { + server? + .folder_service() + .get_publish_namespace(&workspace_id) + .await + }) + } } impl DatabaseCloudService for ServerProvider { diff --git a/frontend/rust-lib/flowy-document/src/entities.rs b/frontend/rust-lib/flowy-document/src/entities.rs index 079d07cb0a..406300e2a2 100644 --- a/frontend/rust-lib/flowy-document/src/entities.rs +++ b/frontend/rust-lib/flowy-document/src/entities.rs @@ -16,6 +16,14 @@ use validator::Validate; use crate::parse::{NotEmptyStr, NotEmptyVec}; +#[derive(Default, ProtoBuf)] +pub struct EncodedCollabPB { + #[pb(index = 1)] + pub state_vector: Vec<u8>, + #[pb(index = 2)] + pub doc_state: Vec<u8>, +} + #[derive(Default, ProtoBuf)] pub struct OpenDocumentPayloadPB { #[pb(index = 1)] diff --git a/frontend/rust-lib/flowy-document/src/event_handler.rs b/frontend/rust-lib/flowy-document/src/event_handler.rs index 2eabbf639a..580ba85587 100644 --- a/frontend/rust-lib/flowy-document/src/event_handler.rs +++ b/frontend/rust-lib/flowy-document/src/event_handler.rs @@ -33,6 +33,22 @@ fn upgrade_document( Ok(manager) } +// Handler for getting the document state +#[instrument(level = "debug", skip_all, err)] +pub(crate) async fn get_encode_collab_handler( + data: AFPluginData<OpenDocumentPayloadPB>, + manager: AFPluginState<Weak<DocumentManager>>, +) -> DataResult<EncodedCollabPB, FlowyError> { + let manager = upgrade_document(manager)?; + let params: OpenDocumentParams = data.into_inner().try_into()?; + let doc_id = params.document_id; + let state = manager.encode_collab(&doc_id).await?; + data_result_ok(EncodedCollabPB { + state_vector: Vec::from(state.state_vector), + doc_state: Vec::from(state.doc_state), + }) +} + // Handler for creating a new document pub(crate) async fn create_document_handler( data: AFPluginData<CreateDocumentPayloadPB>, diff --git a/frontend/rust-lib/flowy-document/src/event_map.rs b/frontend/rust-lib/flowy-document/src/event_map.rs index 1e11db6356..a6ce6959d9 100644 --- a/frontend/rust-lib/flowy-document/src/event_map.rs +++ b/frontend/rust-lib/flowy-document/src/event_map.rs @@ -18,6 +18,10 @@ pub fn init(document_manager: Weak<DocumentManager>) -> AFPlugin { .event(DocumentEvent::CloseDocument, close_document_handler) .event(DocumentEvent::ApplyAction, apply_action_handler) .event(DocumentEvent::GetDocumentData, get_document_data_handler) + .event( + DocumentEvent::GetDocEncodedCollab, + get_encode_collab_handler, + ) .event( DocumentEvent::ConvertDataToDocument, convert_data_to_document, @@ -126,4 +130,7 @@ pub enum DocumentEvent { #[event(input = "UpdateDocumentAwarenessStatePB")] SetAwarenessState = 18, + + #[event(input = "OpenDocumentPayloadPB", output = "EncodedCollabPB")] + GetDocEncodedCollab = 19, } diff --git a/frontend/rust-lib/flowy-document/src/manager.rs b/frontend/rust-lib/flowy-document/src/manager.rs index b83afdfb21..9b13129b60 100644 --- a/frontend/rust-lib/flowy-document/src/manager.rs +++ b/frontend/rust-lib/flowy-document/src/manager.rs @@ -11,6 +11,7 @@ use collab_document::document_awareness::DocumentAwarenessState; use collab_document::document_awareness::DocumentAwarenessUser; use collab_document::document_data::default_document_data; use collab_entity::CollabType; +use collab_plugins::local_storage::kv::PersistenceError; use collab_plugins::CollabKVDB; use dashmap::DashMap; use lib_infra::util::timestamp; @@ -74,6 +75,20 @@ impl DocumentManager { } } + /// Get the encoded collab of the document. + pub async fn encode_collab(&self, doc_id: &str) -> FlowyResult<EncodedCollab> { + let doc_state = DataSource::Disk; + let uid = self.user_service.user_id()?; + let collab = self + .collab_for_document(uid, doc_id, doc_state, false) + .await?; + + let collab = collab.lock(); + collab + .encode_collab_v1(|_| Ok::<(), PersistenceError>(())) + .map_err(internal_error) + } + pub async fn initialize(&self, _uid: i64) -> FlowyResult<()> { trace!("initialize document manager"); self.documents.clear(); diff --git a/frontend/rust-lib/flowy-folder-pub/Cargo.toml b/frontend/rust-lib/flowy-folder-pub/Cargo.toml index 13f13935f7..fa1997aa9b 100644 --- a/frontend/rust-lib/flowy-folder-pub/Cargo.toml +++ b/frontend/rust-lib/flowy-folder-pub/Cargo.toml @@ -12,6 +12,7 @@ collab = { workspace = true } collab-entity = { workspace = true } uuid.workspace = true anyhow.workspace = true +serde = { version = "1.0.202", features = ["derive"] } [dev-dependencies] tokio.workspace = true diff --git a/frontend/rust-lib/flowy-folder-pub/src/cloud.rs b/frontend/rust-lib/flowy-folder-pub/src/cloud.rs index d88b4df203..a017215f88 100644 --- a/frontend/rust-lib/flowy-folder-pub/src/cloud.rs +++ b/frontend/rust-lib/flowy-folder-pub/src/cloud.rs @@ -3,6 +3,7 @@ use collab_entity::CollabType; pub use collab_folder::{Folder, FolderData, Workspace}; use uuid::Uuid; +use crate::entities::{PublishInfoResponse, PublishViewPayload}; use lib_infra::future::FutureResult; /// [FolderCloudService] represents the cloud service for folder. @@ -44,6 +45,24 @@ pub trait FolderCloudService: Send + Sync + 'static { ) -> FutureResult<(), Error>; fn service_name(&self) -> String; + + fn publish_view( + &self, + workspace_id: &str, + payload: Vec<PublishViewPayload>, + ) -> FutureResult<(), Error>; + + fn unpublish_views(&self, workspace_id: &str, view_ids: Vec<String>) -> FutureResult<(), Error>; + + fn get_publish_info(&self, view_id: &str) -> FutureResult<PublishInfoResponse, Error>; + + fn set_publish_namespace( + &self, + workspace_id: &str, + new_namespace: &str, + ) -> FutureResult<(), Error>; + + fn get_publish_namespace(&self, workspace_id: &str) -> FutureResult<String, Error>; } #[derive(Debug)] diff --git a/frontend/rust-lib/flowy-folder-pub/src/entities.rs b/frontend/rust-lib/flowy-folder-pub/src/entities.rs index 41163fae73..e67a780fba 100644 --- a/frontend/rust-lib/flowy-folder-pub/src/entities.rs +++ b/frontend/rust-lib/flowy-folder-pub/src/entities.rs @@ -1,4 +1,6 @@ use crate::folder_builder::ParentChildViews; +use collab_folder::{ViewIcon, ViewLayout}; +use serde::Serialize; use std::collections::HashMap; pub enum ImportData { @@ -39,3 +41,46 @@ pub struct SearchData { /// The data that is stored in the search index row. pub data: String, } + +#[derive(Serialize, Clone, Debug, Eq, PartialEq)] +pub struct PublishViewInfo { + pub view_id: String, + pub name: String, + pub icon: Option<ViewIcon>, + pub layout: ViewLayout, + pub extra: Option<String>, + pub created_by: Option<i64>, + pub last_edited_by: Option<i64>, + pub last_edited_time: i64, + pub created_at: i64, + pub child_views: Option<Vec<PublishViewInfo>>, +} + +#[derive(Serialize, Clone, Debug, Eq, PartialEq)] +pub struct PublishViewMetaData { + pub view: PublishViewInfo, + pub child_views: Vec<PublishViewInfo>, + pub ancestor_views: Vec<PublishViewInfo>, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct PublishViewMeta { + pub metadata: PublishViewMetaData, + pub view_id: String, + pub publish_name: String, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct PublishViewPayload { + pub meta: PublishViewMeta, + /// The doc_state of the encoded collab. + pub data: Vec<u8>, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct PublishInfoResponse { + pub view_id: String, + /// one part of publish url: /{namespace}/{publish_name} + pub publish_name: String, + pub namespace: Option<String>, +} diff --git a/frontend/rust-lib/flowy-folder/Cargo.toml b/frontend/rust-lib/flowy-folder/Cargo.toml index b8ed79720f..e0327a5044 100644 --- a/frontend/rust-lib/flowy-folder/Cargo.toml +++ b/frontend/rust-lib/flowy-folder/Cargo.toml @@ -39,6 +39,9 @@ serde = { workspace = true, features = ["derive"] } serde_json.workspace = true validator.workspace = true async-trait.workspace = true +regex = "1.9.5" +futures = "0.3.30" + [build-dependencies] flowy-codegen.workspace = true diff --git a/frontend/rust-lib/flowy-folder/src/entities/icon.rs b/frontend/rust-lib/flowy-folder/src/entities/icon.rs index 2342b02246..1169c320ea 100644 --- a/frontend/rust-lib/flowy-folder/src/entities/icon.rs +++ b/frontend/rust-lib/flowy-folder/src/entities/icon.rs @@ -39,7 +39,7 @@ pub struct ViewIconPB { pub value: String, } -impl std::convert::From<ViewIconPB> for ViewIcon { +impl From<ViewIconPB> for ViewIcon { fn from(rev: ViewIconPB) -> Self { ViewIcon { ty: rev.ty.into(), diff --git a/frontend/rust-lib/flowy-folder/src/entities/mod.rs b/frontend/rust-lib/flowy-folder/src/entities/mod.rs index b496f334b5..24e5475caa 100644 --- a/frontend/rust-lib/flowy-folder/src/entities/mod.rs +++ b/frontend/rust-lib/flowy-folder/src/entities/mod.rs @@ -1,12 +1,14 @@ pub mod icon; mod import; mod parser; +pub mod publish; pub mod trash; pub mod view; pub mod workspace; pub use icon::*; pub use import::*; +pub use publish::*; pub use trash::*; pub use view::*; pub use workspace::*; diff --git a/frontend/rust-lib/flowy-folder/src/entities/publish.rs b/frontend/rust-lib/flowy-folder/src/entities/publish.rs new file mode 100644 index 0000000000..740d58c366 --- /dev/null +++ b/frontend/rust-lib/flowy-folder/src/entities/publish.rs @@ -0,0 +1,48 @@ +use flowy_derive::ProtoBuf; +use flowy_folder_pub::entities::PublishInfoResponse; + +#[derive(Default, ProtoBuf)] +pub struct PublishViewParamsPB { + #[pb(index = 1)] + pub view_id: String, + #[pb(index = 2, one_of)] + pub publish_name: Option<String>, +} + +#[derive(Default, ProtoBuf)] +pub struct UnpublishViewsPayloadPB { + #[pb(index = 1)] + pub view_ids: Vec<String>, +} + +#[derive(Default, ProtoBuf)] +pub struct PublishInfoResponsePB { + #[pb(index = 1)] + pub view_id: String, + #[pb(index = 2)] + pub publish_name: String, + #[pb(index = 3, one_of)] + pub namespace: Option<String>, +} + +impl From<PublishInfoResponse> for PublishInfoResponsePB { + fn from(info: PublishInfoResponse) -> Self { + Self { + view_id: info.view_id, + publish_name: info.publish_name, + namespace: info.namespace, + } + } +} + +#[derive(Default, ProtoBuf)] +pub struct SetPublishNamespacePayloadPB { + #[pb(index = 1)] + pub new_namespace: String, +} + +#[derive(Default, ProtoBuf)] +pub struct PublishNamespacePB { + #[pb(index = 1)] + pub namespace: String, +} diff --git a/frontend/rust-lib/flowy-folder/src/event_handler.rs b/frontend/rust-lib/flowy-folder/src/event_handler.rs index 7af6d23f78..c646bd70c2 100644 --- a/frontend/rust-lib/flowy-folder/src/event_handler.rs +++ b/frontend/rust-lib/flowy-folder/src/event_handler.rs @@ -393,3 +393,58 @@ pub(crate) async fn update_view_visibility_status_handler( folder.set_views_visibility(params.view_ids, params.is_public); Ok(()) } + +#[tracing::instrument(level = "debug", skip(data, folder), err)] +pub(crate) async fn publish_view_handler( + data: AFPluginData<PublishViewParamsPB>, + folder: AFPluginState<Weak<FolderManager>>, +) -> Result<(), FlowyError> { + let folder = upgrade_folder(folder)?; + let params = data.into_inner(); + folder + .publish_view(params.view_id.as_str(), params.publish_name) + .await?; + Ok(()) +} + +#[tracing::instrument(level = "debug", skip(data, folder), err)] +pub(crate) async fn unpublish_views_handler( + data: AFPluginData<UnpublishViewsPayloadPB>, + folder: AFPluginState<Weak<FolderManager>>, +) -> Result<(), FlowyError> { + let folder = upgrade_folder(folder)?; + let params = data.into_inner(); + folder.unpublish_views(params.view_ids).await?; + Ok(()) +} + +#[tracing::instrument(level = "debug", skip(data, folder), err)] +pub(crate) async fn get_publish_info_handler( + data: AFPluginData<ViewIdPB>, + folder: AFPluginState<Weak<FolderManager>>, +) -> DataResult<PublishInfoResponsePB, FlowyError> { + let folder = upgrade_folder(folder)?; + let view_id = data.into_inner().value; + let info = folder.get_publish_info(&view_id).await?; + data_result_ok(PublishInfoResponsePB::from(info)) +} + +#[tracing::instrument(level = "debug", skip(data, folder), err)] +pub(crate) async fn set_publish_namespace_handler( + data: AFPluginData<SetPublishNamespacePayloadPB>, + folder: AFPluginState<Weak<FolderManager>>, +) -> Result<(), FlowyError> { + let folder = upgrade_folder(folder)?; + let namespace = data.into_inner().new_namespace; + folder.set_publish_namespace(namespace).await?; + Ok(()) +} + +#[tracing::instrument(level = "debug", skip(folder), err)] +pub(crate) async fn get_publish_namespace_handler( + folder: AFPluginState<Weak<FolderManager>>, +) -> DataResult<PublishNamespacePB, FlowyError> { + let folder = upgrade_folder(folder)?; + let namespace = folder.get_publish_namespace().await?; + data_result_ok(PublishNamespacePB { namespace }) +} diff --git a/frontend/rust-lib/flowy-folder/src/event_map.rs b/frontend/rust-lib/flowy-folder/src/event_map.rs index a09c304ec8..0b72c936c8 100644 --- a/frontend/rust-lib/flowy-folder/src/event_map.rs +++ b/frontend/rust-lib/flowy-folder/src/event_map.rs @@ -42,6 +42,11 @@ pub fn init(folder: Weak<FolderManager>) -> AFPlugin { .event(FolderEvent::ReadCurrentWorkspaceViews, get_current_workspace_views_handler) .event(FolderEvent::UpdateViewVisibilityStatus, update_view_visibility_status_handler) .event(FolderEvent::GetViewAncestors, get_view_ancestors_handler) + .event(FolderEvent::PublishView, publish_view_handler) + .event(FolderEvent::GetPublishInfo, get_publish_info_handler) + .event(FolderEvent::UnpublishViews, unpublish_views_handler) + .event(FolderEvent::SetPublishNamespace, set_publish_namespace_handler) + .event(FolderEvent::GetPublishNamespace, get_publish_namespace_handler) } #[derive(Clone, Copy, PartialEq, Eq, Debug, Display, Hash, ProtoBuf_Enum, Flowy_Event)] @@ -176,4 +181,19 @@ pub enum FolderEvent { /// Return the ancestors of the view #[event(input = "ViewIdPB", output = "RepeatedViewPB")] GetViewAncestors = 42, + + #[event(input = "PublishViewParamsPB")] + PublishView = 43, + + #[event(input = "ViewIdPB", output = "PublishInfoResponsePB")] + GetPublishInfo = 44, + + #[event(output = "PublishNamespacePB")] + GetPublishNamespace = 45, + + #[event(input = "SetPublishNamespacePayloadPB")] + SetPublishNamespace = 46, + + #[event(input = "UnpublishViewsPayloadPB")] + UnpublishViews = 47, } diff --git a/frontend/rust-lib/flowy-folder/src/lib.rs b/frontend/rust-lib/flowy-folder/src/lib.rs index bc927d20c7..55d447bdc7 100644 --- a/frontend/rust-lib/flowy-folder/src/lib.rs +++ b/frontend/rust-lib/flowy-folder/src/lib.rs @@ -14,6 +14,7 @@ mod manager_observer; #[cfg(debug_assertions)] pub mod manager_test_util; +pub mod publish_util; pub mod share; #[cfg(feature = "test_helper")] mod test_helper; diff --git a/frontend/rust-lib/flowy-folder/src/manager.rs b/frontend/rust-lib/flowy-folder/src/manager.rs index 39ee2fa5de..7cac89b721 100644 --- a/frontend/rust-lib/flowy-folder/src/manager.rs +++ b/frontend/rust-lib/flowy-folder/src/manager.rs @@ -3,7 +3,7 @@ use crate::entities::{ view_pb_with_child_views, view_pb_without_child_views, view_pb_without_child_views_from_arc, CreateViewParams, CreateWorkspaceParams, DeletedViewPB, DuplicateViewParams, FolderSnapshotPB, MoveNestedViewParams, RepeatedTrashPB, RepeatedViewIdPB, RepeatedViewPB, UpdateViewParams, - ViewPB, ViewSectionPB, WorkspacePB, WorkspaceSettingPB, + ViewLayoutPB, ViewPB, ViewSectionPB, WorkspacePB, WorkspaceSettingPB, }; use crate::manager_observer::{ notify_child_views_changed, notify_did_update_workspace, notify_parent_view_did_change, @@ -12,6 +12,7 @@ use crate::manager_observer::{ use crate::notification::{ send_notification, send_workspace_setting_notification, FolderNotification, }; +use crate::publish_util::{generate_publish_name, view_pb_to_publish_view}; use crate::share::{ImportParams, ImportValue}; use crate::util::{ folder_not_init_error, insert_parent_child_views, workspace_data_not_sync_error, @@ -28,9 +29,13 @@ use collab_integrate::collab_builder::{AppFlowyCollabBuilder, CollabBuilderConfi use collab_integrate::CollabKVDB; use flowy_error::{internal_error, ErrorCode, FlowyError, FlowyResult}; use flowy_folder_pub::cloud::{gen_view_id, FolderCloudService, FolderCollabParams}; +use flowy_folder_pub::entities::{ + PublishInfoResponse, PublishViewInfo, PublishViewMeta, PublishViewMetaData, PublishViewPayload, +}; use flowy_folder_pub::folder_builder::ParentChildViews; use flowy_search_pub::entities::FolderIndexManager; use flowy_sqlite::kv::KVStorePreferences; +use futures::future; use parking_lot::RwLock; use std::fmt::{Display, Formatter}; use std::ops::Deref; @@ -992,6 +997,201 @@ impl FolderManager { Ok(()) } + /// Publish the view with the given view id. + /// [publish_name] is one part of the URL of the published view. if it is None, the default publish name will be used. The default publish name is generated by the view id and view name. + #[tracing::instrument(level = "debug", skip(self), err)] + pub async fn publish_view(&self, view_id: &str, publish_name: Option<String>) -> FlowyResult<()> { + let view = self + .with_folder(|| None, |folder| folder.views.get_view(view_id)) + .ok_or_else(|| FlowyError::record_not_found().with_context("Can't find the view"))?; + + let layout = view.layout.clone(); + + if layout != ViewLayout::Document { + return Err(FlowyError::new( + ErrorCode::NotSupportYet, + "Only document view can be published".to_string(), + )); + } + + // Get the view payload and its child views recursively + let payload = self + .get_batch_publish_payload(view_id, publish_name, Some(false)) + .await?; + + let workspace_id = self.user.workspace_id()?; + self + .cloud_service + .publish_view(workspace_id.as_str(), payload) + .await?; + Ok(()) + } + + /// Unpublish the view with the given view id. + #[tracing::instrument(level = "debug", skip(self), err)] + pub async fn unpublish_views(&self, view_ids: Vec<String>) -> FlowyResult<()> { + let workspace_id = self.user.workspace_id()?; + self + .cloud_service + .unpublish_views(workspace_id.as_str(), view_ids) + .await?; + Ok(()) + } + + /// Get the publish info of the view with the given view id. + /// The publish info contains the namespace and publish_name of the view. + #[tracing::instrument(level = "debug", skip(self), err)] + pub async fn get_publish_info(&self, view_id: &str) -> FlowyResult<PublishInfoResponse> { + let publish_info = self.cloud_service.get_publish_info(view_id).await?; + Ok(publish_info) + } + + /// Get the namespace of the current workspace. + /// The namespace is used to generate the URL of the published view. + #[tracing::instrument(level = "debug", skip(self), err)] + pub async fn set_publish_namespace(&self, namespace: String) -> FlowyResult<()> { + let workspace_id = self.user.workspace_id()?; + self + .cloud_service + .set_publish_namespace(workspace_id.as_str(), namespace.as_str()) + .await?; + Ok(()) + } + + /// Get the namespace of the current workspace. + #[tracing::instrument(level = "debug", skip(self), err)] + pub async fn get_publish_namespace(&self) -> FlowyResult<String> { + let workspace_id = self.user.workspace_id()?; + let namespace = self + .cloud_service + .get_publish_namespace(workspace_id.as_str()) + .await?; + Ok(namespace) + } + + /// Get the publishing payload of the view with the given view id. + /// The publishing payload contains the view data and its child views(not recursively). + pub async fn get_batch_publish_payload( + &self, + view_id: &str, + publish_name: Option<String>, + include_children: Option<bool>, + ) -> FlowyResult<Vec<PublishViewPayload>> { + let mut stack = vec![view_id.to_string()]; + let mut payloads = Vec::new(); + + while let Some(current_view_id) = stack.pop() { + let view = match self.get_view_pb(¤t_view_id).await { + Ok(view) => view, + Err(_) => continue, + }; + + // Only document view can be published + let layout = if view.layout == ViewLayoutPB::Document { + ViewLayout::Document + } else { + continue; + }; + + // Only support set the publish_name for the current view, not for the child views + let publish_name = if current_view_id == view_id { + publish_name.clone() + } else { + None + }; + + let payload = self + .get_publish_payload(¤t_view_id, publish_name, layout) + .await; + + if let Ok(payload) = payload { + payloads.push(payload); + } + + if include_children.unwrap_or(false) { + // Add the child views to the stack + for child in &view.child_views { + stack.push(child.id.clone()); + } + } + } + + Ok(payloads) + } + + async fn build_publish_views(&self, view_id: &str) -> Option<PublishViewInfo> { + let view_pb = self.get_view_pb(view_id).await.ok()?; + + let mut child_views_futures = vec![]; + + for child in &view_pb.child_views { + let future = self.build_publish_views(&child.id); + child_views_futures.push(future); + } + + let child_views = future::join_all(child_views_futures) + .await + .into_iter() + .flatten() + .collect::<Vec<PublishViewInfo>>(); + + let view_child_views = if child_views.is_empty() { + None + } else { + Some(child_views) + }; + + let view = view_pb_to_publish_view(&view_pb); + + let view = PublishViewInfo { + child_views: view_child_views, + ..view + }; + + Some(view) + } + async fn get_publish_payload( + &self, + view_id: &str, + publish_name: Option<String>, + layout: ViewLayout, + ) -> FlowyResult<PublishViewPayload> { + let handler = self.get_handler(&layout)?; + let encoded_collab = handler.encoded_collab_v1(view_id, layout).await?; + let view = self + .with_folder(|| None, |folder| folder.views.get_view(view_id)) + .ok_or_else(|| FlowyError::record_not_found().with_context("Can't find the view"))?; + let publish_name = publish_name.unwrap_or_else(|| generate_publish_name(&view.id, &view.name)); + + let child_views = self + .build_publish_views(view_id) + .await + .map(|v| v.child_views.map_or(vec![], |c| c)) + .map_or(vec![], |c| c); + + let ancestor_views = self + .get_view_ancestors_pb(view_id) + .await? + .iter() + .map(view_pb_to_publish_view) + .collect::<Vec<PublishViewInfo>>(); + + let view_pb = self.get_view_pb(view_id).await?; + let metadata = PublishViewMetaData { + view: view_pb_to_publish_view(&view_pb), + child_views, + ancestor_views, + }; + let meta = PublishViewMeta { + view_id: view.id.clone(), + publish_name, + metadata, + }; + + let data = Vec::from(encoded_collab.doc_state); + Ok(PublishViewPayload { meta, data }) + } + // Used by toggle_favorites to send notification to frontend, after the favorite status of view has been changed.It sends two distinct notifications: one to correctly update the concerned view's is_favorite status, and another to update the list of favorites that is to be displayed. async fn send_toggle_favorite_notification(&self, view_id: &str) { if let Ok(view) = self.get_view_pb(view_id).await { diff --git a/frontend/rust-lib/flowy-folder/src/publish_util.rs b/frontend/rust-lib/flowy-folder/src/publish_util.rs new file mode 100644 index 0000000000..735614ffa4 --- /dev/null +++ b/frontend/rust-lib/flowy-folder/src/publish_util.rs @@ -0,0 +1,37 @@ +use crate::entities::ViewPB; +use flowy_folder_pub::entities::PublishViewInfo; +use regex::Regex; +use tracing::trace; + +fn replace_invalid_url_chars(input: &str) -> String { + let regex = Regex::new(r"[^\w-]").unwrap(); + regex.replace_all(input, "-").to_string() +} + +pub fn generate_publish_name(id: &str, name: &str) -> String { + let id_len = id.len(); + let name = replace_invalid_url_chars(name); + let name_len = name.len(); + // The backend limits the publish name to a maximum of 50 characters. + // If the combined length of the ID and the name exceeds 50 characters, + // we will truncate the name to ensure the final result is within the limit. + // The name should only contain alphanumeric characters and hyphens. + let result = format!("{}-{}", &name[..std::cmp::min(49 - id_len, name_len)], id); + trace!("generate_publish_name: {}", result); + result +} + +pub fn view_pb_to_publish_view(view: &ViewPB) -> PublishViewInfo { + PublishViewInfo { + view_id: view.id.clone(), + name: view.name.clone(), + layout: view.layout.clone().into(), + icon: view.icon.clone().map(|icon| icon.into()), + child_views: None, + extra: view.extra.clone(), + created_by: view.created_by, + last_edited_by: view.last_edited_by, + last_edited_time: view.last_edited, + created_at: view.create_time, + } +} diff --git a/frontend/rust-lib/flowy-folder/src/view_operation.rs b/frontend/rust-lib/flowy-folder/src/view_operation.rs index 86b3f4894f..57edf1ba15 100644 --- a/frontend/rust-lib/flowy-folder/src/view_operation.rs +++ b/frontend/rust-lib/flowy-folder/src/view_operation.rs @@ -2,8 +2,7 @@ use std::collections::HashMap; use std::sync::Arc; use bytes::Bytes; - -use collab_entity::EncodedCollab; +use collab::entity::EncodedCollab; pub use collab_folder::View; use collab_folder::ViewLayout; use tokio::sync::RwLock; @@ -46,6 +45,12 @@ pub trait FolderOperationHandler { /// Returns the [ViewData] that can be used to create the same view. fn duplicate_view(&self, view_id: &str) -> FutureResult<ViewData, FlowyError>; + fn encoded_collab_v1( + &self, + view_id: &str, + layout: ViewLayout, + ) -> FutureResult<EncodedCollab, FlowyError>; + /// Create a view with the data. /// /// # Arguments diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/folder.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/folder.rs index fe58f3fc16..7468c163fd 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/folder.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/folder.rs @@ -1,6 +1,7 @@ use anyhow::Error; use client_api::entity::{ - workspace_dto::CreateWorkspaceParam, CollabParams, QueryCollab, QueryCollabParams, + workspace_dto::CreateWorkspaceParam, CollabParams, PublishCollabItem, PublishCollabMetadata, + QueryCollab, QueryCollabParams, }; use collab::core::collab::DataSource; use collab::core::origin::CollabOrigin; @@ -8,12 +9,14 @@ use collab_entity::CollabType; use collab_folder::RepeatedViewIdentifier; use std::sync::Arc; use tracing::instrument; +use uuid::Uuid; -use flowy_error::FlowyError; +use flowy_error::{ErrorCode, FlowyError}; use flowy_folder_pub::cloud::{ Folder, FolderCloudService, FolderCollabParams, FolderData, FolderSnapshot, Workspace, WorkspaceRecord, }; +use flowy_folder_pub::entities::{PublishInfoResponse, PublishViewPayload}; use lib_infra::future::FutureResult; use crate::af_cloud::define::ServerUser; @@ -180,4 +183,95 @@ where fn service_name(&self) -> String { "AppFlowy Cloud".to_string() } + + fn publish_view( + &self, + workspace_id: &str, + payload: Vec<PublishViewPayload>, + ) -> FutureResult<(), Error> { + let workspace_id = workspace_id.to_string(); + let try_get_client = self.inner.try_get_client(); + FutureResult::new(async move { + let params = payload + .into_iter() + .map(|object| PublishCollabItem { + meta: PublishCollabMetadata { + view_id: Uuid::parse_str(object.meta.view_id.as_str()).unwrap_or(Uuid::nil()), + publish_name: object.meta.publish_name, + metadata: object.meta.metadata, + }, + data: object.data, + }) + .collect::<Vec<_>>(); + try_get_client? + .publish_collabs(&workspace_id, params) + .await + .map_err(FlowyError::from)?; + Ok(()) + }) + } + + fn unpublish_views(&self, workspace_id: &str, view_ids: Vec<String>) -> FutureResult<(), Error> { + let workspace_id = workspace_id.to_string(); + let try_get_client = self.inner.try_get_client(); + let view_uuids = view_ids + .iter() + .map(|id| Uuid::parse_str(id).unwrap_or(Uuid::nil())) + .collect::<Vec<_>>(); + FutureResult::new(async move { + try_get_client? + .unpublish_collabs(&workspace_id, &view_uuids) + .await + .map_err(FlowyError::from)?; + Ok(()) + }) + } + + fn get_publish_info(&self, view_id: &str) -> FutureResult<PublishInfoResponse, Error> { + let try_get_client = self.inner.try_get_client(); + let view_id = Uuid::parse_str(view_id) + .map_err(|_| FlowyError::new(ErrorCode::InvalidParams, "Invalid view id")); + + FutureResult::new(async move { + let view_id = view_id?; + let info = try_get_client? + .get_published_collab_info(&view_id) + .await + .map_err(FlowyError::from)?; + Ok(PublishInfoResponse { + view_id: info.view_id.to_string(), + publish_name: info.publish_name, + namespace: info.namespace, + }) + }) + } + + fn set_publish_namespace( + &self, + workspace_id: &str, + new_namespace: &str, + ) -> FutureResult<(), Error> { + let workspace_id = workspace_id.to_string(); + let namespace = new_namespace.to_string(); + let try_get_client = self.inner.try_get_client(); + FutureResult::new(async move { + try_get_client? + .set_workspace_publish_namespace(&workspace_id, &namespace) + .await + .map_err(FlowyError::from)?; + Ok(()) + }) + } + + fn get_publish_namespace(&self, workspace_id: &str) -> FutureResult<String, Error> { + let workspace_id = workspace_id.to_string(); + let try_get_client = self.inner.try_get_client(); + FutureResult::new(async move { + let namespace = try_get_client? + .get_workspace_publish_namespace(&workspace_id) + .await + .map_err(FlowyError::from)?; + Ok(namespace) + }) + } } diff --git a/frontend/rust-lib/flowy-server/src/local_server/impls/folder.rs b/frontend/rust-lib/flowy-server/src/local_server/impls/folder.rs index ea0ee027b9..5c3a5464ed 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/impls/folder.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/impls/folder.rs @@ -7,6 +7,7 @@ use flowy_folder_pub::cloud::{ gen_workspace_id, FolderCloudService, FolderCollabParams, FolderData, FolderSnapshot, Workspace, WorkspaceRecord, }; +use flowy_folder_pub::entities::{PublishInfoResponse, PublishViewPayload}; use lib_infra::future::FutureResult; use crate::local_server::LocalServerDB; @@ -77,4 +78,48 @@ impl FolderCloudService for LocalServerFolderCloudServiceImpl { fn service_name(&self) -> String { "Local".to_string() } + + fn publish_view( + &self, + _workspace_id: &str, + _payload: Vec<PublishViewPayload>, + ) -> FutureResult<(), Error> { + FutureResult::new(async { Err(anyhow!("Local server doesn't support publish view")) }) + } + + fn unpublish_views( + &self, + _workspace_id: &str, + _view_ids: Vec<String>, + ) -> FutureResult<(), Error> { + FutureResult::new(async { Err(anyhow!("Local server doesn't support unpublish views")) }) + } + + fn get_publish_info(&self, _view_id: &str) -> FutureResult<PublishInfoResponse, Error> { + FutureResult::new(async move { + Err(anyhow!( + "Local server doesn't support get publish info from remote" + )) + }) + } + + fn set_publish_namespace( + &self, + _workspace_id: &str, + _new_namespace: &str, + ) -> FutureResult<(), Error> { + FutureResult::new(async { + Err(anyhow!( + "Local server doesn't support set publish namespace" + )) + }) + } + + fn get_publish_namespace(&self, _workspace_id: &str) -> FutureResult<String, Error> { + FutureResult::new(async { + Err(anyhow!( + "Local server doesn't support get publish namespace" + )) + }) + } } diff --git a/frontend/rust-lib/flowy-server/src/supabase/api/folder.rs b/frontend/rust-lib/flowy-server/src/supabase/api/folder.rs index ca0957c375..828c0fedbb 100644 --- a/frontend/rust-lib/flowy-server/src/supabase/api/folder.rs +++ b/frontend/rust-lib/flowy-server/src/supabase/api/folder.rs @@ -13,6 +13,7 @@ use flowy_folder_pub::cloud::{ gen_workspace_id, Folder, FolderCloudService, FolderCollabParams, FolderData, FolderSnapshot, Workspace, WorkspaceRecord, }; +use flowy_folder_pub::entities::{PublishInfoResponse, PublishViewPayload}; use lib_dispatch::prelude::af_spawn; use lib_infra::future::FutureResult; use lib_infra::util::timestamp; @@ -174,6 +175,46 @@ where fn service_name(&self) -> String { "Supabase".to_string() } + + fn publish_view( + &self, + _workspace_id: &str, + _payload: Vec<PublishViewPayload>, + ) -> FutureResult<(), Error> { + FutureResult::new(async { Err(anyhow!("supabase server doesn't support publish view")) }) + } + + fn unpublish_views( + &self, + _workspace_id: &str, + _view_ids: Vec<String>, + ) -> FutureResult<(), Error> { + FutureResult::new(async { Err(anyhow!("supabase server doesn't support unpublish views")) }) + } + + fn get_publish_info(&self, _view_id: &str) -> FutureResult<PublishInfoResponse, Error> { + FutureResult::new(async { Err(anyhow!("supabase server doesn't support publish info")) }) + } + + fn set_publish_namespace( + &self, + _workspace_id: &str, + _new_namespace: &str, + ) -> FutureResult<(), Error> { + FutureResult::new(async { + Err(anyhow!( + "supabase server doesn't support set publish namespace" + )) + }) + } + + fn get_publish_namespace(&self, _workspace_id: &str) -> FutureResult<String, Error> { + FutureResult::new(async { + Err(anyhow!( + "supabase server doesn't support get publish namespace" + )) + }) + } } fn workspace_from_json_value(value: Value) -> Result<Workspace, Error> { diff --git a/frontend/scripts/code_generation/flowy_icons/generate_flowy_icons.sh b/frontend/scripts/code_generation/flowy_icons/generate_flowy_icons.sh index 30538def96..f4da25fb58 100755 --- a/frontend/scripts/code_generation/flowy_icons/generate_flowy_icons.sh +++ b/frontend/scripts/code_generation/flowy_icons/generate_flowy_icons.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash echo "Generating flowy icon files" diff --git a/frontend/scripts/code_generation/freezed/generate_freezed.sh b/frontend/scripts/code_generation/freezed/generate_freezed.sh index 24c90650d2..01692fe7ee 100755 --- a/frontend/scripts/code_generation/freezed/generate_freezed.sh +++ b/frontend/scripts/code_generation/freezed/generate_freezed.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # Store the current working directory original_dir=$(pwd) diff --git a/frontend/scripts/code_generation/generate.sh b/frontend/scripts/code_generation/generate.sh index f71ceba2df..cfe7a12b60 100755 --- a/frontend/scripts/code_generation/generate.sh +++ b/frontend/scripts/code_generation/generate.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # Store the current working directory original_dir=$(pwd) diff --git a/frontend/scripts/code_generation/language_files/generate_language_files.sh b/frontend/scripts/code_generation/language_files/generate_language_files.sh index 8aa403d1f2..ec5a14836e 100755 --- a/frontend/scripts/code_generation/language_files/generate_language_files.sh +++ b/frontend/scripts/code_generation/language_files/generate_language_files.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash echo "Generating language files" diff --git a/frontend/scripts/docker-buildfiles/Dockerfile b/frontend/scripts/docker-buildfiles/Dockerfile index 8ebe5fc8ef..a2e15cfd1b 100644 --- a/frontend/scripts/docker-buildfiles/Dockerfile +++ b/frontend/scripts/docker-buildfiles/Dockerfile @@ -32,8 +32,8 @@ RUN yay -S --noconfirm curl base-devel openssl clang cmake ninja pkg-config xdg- RUN xdg-user-dirs-update RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y RUN source ~/.cargo/env && \ - rustup toolchain install 1.75 && \ - rustup default 1.75 + rustup toolchain install 1.77.2 && \ + rustup default 1.77.2 # Install Flutter RUN sudo pacman -S --noconfirm git tar gtk3 diff --git a/frontend/scripts/flatpack-buildfiles/launcher.sh b/frontend/scripts/flatpack-buildfiles/launcher.sh index c7e7b9ee4a..24b4fdbea4 100644 --- a/frontend/scripts/flatpack-buildfiles/launcher.sh +++ b/frontend/scripts/flatpack-buildfiles/launcher.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash gdbus call --session --dest io.appflowy.AppFlowy \ --object-path /io/appflowy/AppFlowy/Object \ --method io.appflowy.AppFlowy.Open "['$1']" {} diff --git a/frontend/scripts/install_dev_env/install_ios.sh b/frontend/scripts/install_dev_env/install_ios.sh index 653eb8f1b3..5f27fff913 100644 --- a/frontend/scripts/install_dev_env/install_ios.sh +++ b/frontend/scripts/install_dev_env/install_ios.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash YELLOW="\e[93m" GREEN="\e[32m" diff --git a/frontend/scripts/install_dev_env/install_linux.sh b/frontend/scripts/install_dev_env/install_linux.sh index b02b31d62c..7ec91019ec 100755 --- a/frontend/scripts/install_dev_env/install_linux.sh +++ b/frontend/scripts/install_dev_env/install_linux.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash YELLOW="\e[93m" GREEN="\e[32m" diff --git a/frontend/scripts/install_dev_env/install_macos.sh b/frontend/scripts/install_dev_env/install_macos.sh index 8613b904c6..5bb83c35ad 100755 --- a/frontend/scripts/install_dev_env/install_macos.sh +++ b/frontend/scripts/install_dev_env/install_macos.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash YELLOW="\e[93m" GREEN="\e[32m" @@ -90,4 +90,4 @@ cargo install --force duckscript_cli # Check prerequisites printMessage "Checking prerequisites." -cargo make appflowy-flutter-deps-tools \ No newline at end of file +cargo make appflowy-flutter-deps-tools diff --git a/frontend/scripts/install_dev_env/install_windows.sh b/frontend/scripts/install_dev_env/install_windows.sh index 1d68a677ae..45182df3c0 100644 --- a/frontend/scripts/install_dev_env/install_windows.sh +++ b/frontend/scripts/install_dev_env/install_windows.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash YELLOW="\e[93m" GREEN="\e[32m" diff --git a/frontend/scripts/linux_distribution/appimage/build_appimage.sh b/frontend/scripts/linux_distribution/appimage/build_appimage.sh index a7e4d1b11b..73deb45edd 100644 --- a/frontend/scripts/linux_distribution/appimage/build_appimage.sh +++ b/frontend/scripts/linux_distribution/appimage/build_appimage.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash VERSION=$1 diff --git a/frontend/scripts/linux_distribution/deb/DEBIAN/postinst b/frontend/scripts/linux_distribution/deb/DEBIAN/postinst index 56186649d4..bf2f79fa97 100755 --- a/frontend/scripts/linux_distribution/deb/DEBIAN/postinst +++ b/frontend/scripts/linux_distribution/deb/DEBIAN/postinst @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash if [ -e /usr/bin/AppFlowy ]; then echo "Symlink already exists, skipping." else diff --git a/frontend/scripts/linux_distribution/deb/DEBIAN/postrm b/frontend/scripts/linux_distribution/deb/DEBIAN/postrm index f815d1bb5c..59a680e767 100755 --- a/frontend/scripts/linux_distribution/deb/DEBIAN/postrm +++ b/frontend/scripts/linux_distribution/deb/DEBIAN/postrm @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash if [ -e /usr/bin/AppFlowy ]; then rm /usr/bin/AppFlowy rm /usr/bin/AppFlowyLauncher.sh diff --git a/frontend/scripts/linux_distribution/deb/build_deb.sh b/frontend/scripts/linux_distribution/deb/build_deb.sh index 35fe9dbbaf..42fbf7346d 100644 --- a/frontend/scripts/linux_distribution/deb/build_deb.sh +++ b/frontend/scripts/linux_distribution/deb/build_deb.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash LINUX_RELEASE_PRODUCTION=$1 VERSION=$2 diff --git a/frontend/scripts/linux_distribution/packaging/launcher.sh b/frontend/scripts/linux_distribution/packaging/launcher.sh index c7e7b9ee4a..24b4fdbea4 100644 --- a/frontend/scripts/linux_distribution/packaging/launcher.sh +++ b/frontend/scripts/linux_distribution/packaging/launcher.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash gdbus call --session --dest io.appflowy.AppFlowy \ --object-path /io/appflowy/AppFlowy/Object \ --method io.appflowy.AppFlowy.Open "['$1']" {} diff --git a/frontend/scripts/linux_installer/postinst b/frontend/scripts/linux_installer/postinst index 4f495f86a2..83e1a1043e 100644 --- a/frontend/scripts/linux_installer/postinst +++ b/frontend/scripts/linux_installer/postinst @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash if [ -e /usr/local/bin/AppFlowy ]; then echo "Symlink already exists, skipping." else diff --git a/frontend/scripts/linux_installer/postrm b/frontend/scripts/linux_installer/postrm index 53304b1b48..7927bc56e5 100644 --- a/frontend/scripts/linux_installer/postrm +++ b/frontend/scripts/linux_installer/postrm @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash if [ -e /usr/local/bin/appflowy ]; then rm /usr/local/bin/appflowy -fi \ No newline at end of file +fi diff --git a/frontend/scripts/makefile/web.toml b/frontend/scripts/makefile/web.toml index 1d40d9da17..d00dbaf8d1 100644 --- a/frontend/scripts/makefile/web.toml +++ b/frontend/scripts/makefile/web.toml @@ -2,7 +2,7 @@ script_runner = "bash" script = [ """ - #!/bin/bash + #!/usr/bin/env bash BASE_DIR=$(pwd) crates=("lib-dispatch" "flowy-encrypt" "lib-infra" "flowy-notification" "flowy-date" "flowy-error" "collab-integrate" "flowy-document") @@ -56,4 +56,4 @@ script = [""" end end """] -script_runner = "@duckscript" \ No newline at end of file +script_runner = "@duckscript" diff --git a/frontend/scripts/tool/update_collab_rev.sh b/frontend/scripts/tool/update_collab_rev.sh index fabc71160a..469974cdb2 100755 --- a/frontend/scripts/tool/update_collab_rev.sh +++ b/frontend/scripts/tool/update_collab_rev.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # Ensure a new revision ID is provided if [ "$#" -ne 1 ]; then diff --git a/frontend/scripts/tool/update_collab_source.sh b/frontend/scripts/tool/update_collab_source.sh index 094e5caf14..fcf8ae9744 100755 --- a/frontend/scripts/tool/update_collab_source.sh +++ b/frontend/scripts/tool/update_collab_source.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # Paths to your Cargo.toml files REPO_PATH="./AppFlowy-Collab" @@ -38,4 +38,4 @@ fi # Switch dependencies in both Cargo.toml files switch_deps "$CARGO_TOML_1" "$REPO_RELATIVE_PATH_1" -switch_deps "$CARGO_TOML_2" "$REPO_RELATIVE_PATH_2" \ No newline at end of file +switch_deps "$CARGO_TOML_2" "$REPO_RELATIVE_PATH_2"