diff --git a/frontend/.vscode/tasks.json b/frontend/.vscode/tasks.json index cec5290496..f4eb56186a 100644 --- a/frontend/.vscode/tasks.json +++ b/frontend/.vscode/tasks.json @@ -118,15 +118,14 @@ { "label": "AF: Generate Freezed Files", "type": "shell", - "command": "dart run build_runner build -d", + "command": "sh ./scripts/code_generation/freezed/generate_freezed.sh", "options": { - "cwd": "${workspaceFolder}/appflowy_flutter" - } - }, - { - "label": "AF: Generate Language Files", - "type": "shell", - "command": "sh ./scripts/generate_language_files.sh", + "cwd": "${workspaceFolder}" + }, + "group": { + "kind": "build", + "isDefault": true + }, "windows": { "options": { "shell": { @@ -134,7 +133,24 @@ "args": [ "/d", "/c", - ".\\scripts\\generate_language_files.cmd" + ".\\scripts\\code_generation\\freezed\\generate_freezed.cmd" + ] + } + } + }, + }, + { + "label": "AF: Generate Language Files", + "type": "shell", + "command": "sh ./scripts/code_generation/language_files/generate_language_files.sh", + "windows": { + "options": { + "shell": { + "executable": "cmd.exe", + "args": [ + "/d", + "/c", + ".\\scripts\\code_generation\\language_files\\generate_language_files.cmd" ] } } diff --git a/frontend/appflowy_flutter/assets/translations/en.json b/frontend/appflowy_flutter/assets/translations/en.json index f2be271214..4097f67c37 100644 --- a/frontend/appflowy_flutter/assets/translations/en.json +++ b/frontend/appflowy_flutter/assets/translations/en.json @@ -192,7 +192,19 @@ "dark": "Dark Mode", "system": "Adapt to System" }, - "theme": "Theme" + "themeUpload": { + "button": "Upload", + "description": "Upload your own AppFlowy theme using the button below.", + "failure": "The theme that was uploaded had an invalid format.", + "loading": "Please wait while we validate and upload your theme...", + "uploadSuccess": "Your theme was uploaded successfully", + "deletionFailure": "Failed to delete the theme. Try to delete it manually.", + "filePickerDialogTitle": "Choose a .flowy_plugin file", + "urlUploadFailure": "Failed to open url: {}" + }, + "theme": "Theme", + "builtInsLabel": "Built-in Themes", + "pluginsLabel": "Plugins" }, "files": { "copy": "Copy", diff --git a/frontend/appflowy_flutter/integration_test/util/mock/mock_file_picker.dart b/frontend/appflowy_flutter/integration_test/util/mock/mock_file_picker.dart index caaefea2d4..5617f86edd 100644 --- a/frontend/appflowy_flutter/integration_test/util/mock/mock_file_picker.dart +++ b/frontend/appflowy_flutter/integration_test/util/mock/mock_file_picker.dart @@ -1,6 +1,5 @@ import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/util/file_picker/file_picker_service.dart'; -import 'package:file_picker/file_picker.dart' as fp; +import 'package:flowy_infra/file_picker/file_picker_service.dart'; class MockFilePicker implements FilePickerService { MockFilePicker({ @@ -21,7 +20,7 @@ class MockFilePicker implements FilePickerService { String? dialogTitle, String? fileName, String? initialDirectory, - fp.FileType type = fp.FileType.any, + FileType type = FileType.any, List? allowedExtensions, bool lockParentWindow = false, }) { @@ -32,18 +31,17 @@ class MockFilePicker implements FilePickerService { Future pickFiles({ String? dialogTitle, String? initialDirectory, - fp.FileType type = fp.FileType.any, + FileType type = FileType.any, List? allowedExtensions, - Function(fp.FilePickerStatus p1)? onFileLoading, + Function(FilePickerStatus p1)? onFileLoading, bool allowCompression = true, bool allowMultiple = false, bool withData = false, bool withReadStream = false, bool lockParentWindow = false, }) { - final platformFiles = mockPaths - .map((e) => fp.PlatformFile(path: e, name: '', size: 0)) - .toList(); + final platformFiles = + mockPaths.map((e) => PlatformFile(path: e, name: '', size: 0)).toList(); return Future.value( FilePickerResult( platformFiles, diff --git a/frontend/appflowy_flutter/lib/plugins/document/document_page.dart b/frontend/appflowy_flutter/lib/plugins/document/document_page.dart index 19672d22eb..c35955b7f6 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/document_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/document_page.dart @@ -10,12 +10,12 @@ import 'package:appflowy/plugins/document/presentation/editor_style.dart'; import 'package:appflowy/plugins/document/presentation/export_page_widget.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/util/base64_string.dart'; -import 'package:appflowy/util/file_picker/file_picker_service.dart'; import 'package:appflowy_backend/protobuf/flowy-document2/protobuf.dart' hide DocumentEvent; import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; import 'package:appflowy_editor/appflowy_editor.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/error_page.dart'; import 'package:flutter/material.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/link_to_page_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/link_to_page_widget.dart index 944b76e1d7..381476ccb8 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/link_to_page_widget.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/link_to_page_widget.dart @@ -46,7 +46,7 @@ void showLinkToPageMenu( linkToPageMenuEntry.remove(); } on FlowyError catch (e) { Dialogs.show( - FlowyErrorPage.message( + child: FlowyErrorPage.message( e.msg, howToFix: LocaleKeys.errorDialog_howToFixFallback.tr(), ), diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/custom_cover_picker_bloc.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/custom_cover_picker_bloc.dart index ecd45f3af7..6ff4b0a460 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/custom_cover_picker_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/custom_cover_picker_bloc.dart @@ -1,12 +1,11 @@ import 'dart:io'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/util/file_picker/file_picker_service.dart'; import 'package:appflowy/workspace/application/settings/prelude.dart'; import 'package:easy_localization/easy_localization.dart'; -import 'package:file_picker/file_picker.dart' as fp; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; +import 'package:flowy_infra/file_picker/file_picker_service.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:dartz/dartz.dart'; @@ -121,7 +120,7 @@ class CoverImagePickerBloc final result = await getIt().pickFiles( dialogTitle: LocaleKeys.document_plugins_cover_addLocalImage.tr(), allowMultiple: false, - type: fp.FileType.image, + type: FileType.image, allowedExtensions: allowedExtensions, ); if (result != null && result.files.isNotEmpty) { 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 90c3694db5..d7a8be47a1 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,13 +1,13 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/plugins/document/application/share_bloc.dart'; -import 'package:appflowy/util/file_picker/file_picker_service.dart'; import 'package:appflowy/workspace/application/view/view_listener.dart'; import 'package:appflowy/workspace/presentation/home/toast.dart'; import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart'; import 'package:appflowy_backend/protobuf/flowy-document2/entities.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/widget/rounded_button.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; diff --git a/frontend/appflowy_flutter/lib/startup/deps_resolver.dart b/frontend/appflowy_flutter/lib/startup/deps_resolver.dart index aac9e258f1..b99546022c 100644 --- a/frontend/appflowy_flutter/lib/startup/deps_resolver.dart +++ b/frontend/appflowy_flutter/lib/startup/deps_resolver.dart @@ -11,8 +11,8 @@ import 'package:appflowy/user/application/auth/auth_service.dart'; import 'package:appflowy/user/application/auth/supabase_auth_service.dart'; import 'package:appflowy/user/application/user_listener.dart'; import 'package:appflowy/user/application/user_service.dart'; -import 'package:appflowy/util/file_picker/file_picker_impl.dart'; -import 'package:appflowy/util/file_picker/file_picker_service.dart'; +import 'package:flowy_infra/file_picker/file_picker_impl.dart'; +import 'package:flowy_infra/file_picker/file_picker_service.dart'; import 'package:appflowy/plugins/document/application/prelude.dart'; import 'package:appflowy/workspace/application/user/prelude.dart'; import 'package:appflowy/workspace/application/workspace/prelude.dart'; diff --git a/frontend/appflowy_flutter/lib/user/presentation/folder/folder_widget.dart b/frontend/appflowy_flutter/lib/user/presentation/folder/folder_widget.dart index 8ffde1c971..7902d1d49f 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/folder/folder_widget.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/folder/folder_widget.dart @@ -1,8 +1,8 @@ import 'dart:io'; -import 'package:appflowy/util/file_picker/file_picker_service.dart'; import 'package:appflowy/workspace/application/settings/prelude.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/file_picker/file_picker_service.dart'; import 'package:flowy_infra/image.dart'; import 'package:flowy_infra/size.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; diff --git a/frontend/appflowy_flutter/lib/workspace/application/appearance.dart b/frontend/appflowy_flutter/lib/workspace/application/appearance.dart index e669e31add..a37d3e18a0 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/appearance.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/appearance.dart @@ -36,10 +36,10 @@ class AppearanceSettingsCubit extends Cubit { /// Update selected theme in the user's settings and emit an updated state /// with the AppTheme named [themeName]. - void setTheme(String themeName) { + Future setTheme(String themeName) async { _setting.theme = themeName; _saveAppearanceSettings(); - emit(state.copyWith(appTheme: AppTheme.fromName(themeName))); + emit(state.copyWith(appTheme: await AppTheme.fromName(themeName))); } /// Update the theme mode in the user's settings and emit an updated state. @@ -182,7 +182,7 @@ class AppearanceSettingsState with _$AppearanceSettingsState { double menuOffset, ) { return AppearanceSettingsState( - appTheme: AppTheme.fromName(themeName), + appTheme: AppTheme.fallback, font: font, monospaceFont: monospaceFont, themeMode: _themeModeFromPB(themeModePB), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/app/header/import/import_panel.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/app/header/import/import_panel.dart index 68c2ff4ede..3bb11d291f 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/app/header/import/import_panel.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/app/header/import/import_panel.dart @@ -5,12 +5,11 @@ import 'dart:typed_data'; import 'package:appflowy/plugins/document/application/document_data_pb_extension.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/migration/editor_migration.dart'; import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/util/file_picker/file_picker_service.dart'; import 'package:appflowy/workspace/application/settings/share/import_service.dart'; import 'package:appflowy/workspace/presentation/home/menu/app/header/import/import_type.dart'; import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart'; +import 'package:flowy_infra/file_picker/file_picker_service.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:file_picker/file_picker.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/style_widget/container.dart'; import 'package:flutter/material.dart'; diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_appearance_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_appearance_view.dart index 57c2d2a789..7aeac6de22 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_appearance_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_appearance_view.dart @@ -1,8 +1,13 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/workspace/application/appearance.dart'; +import 'package:appflowy/workspace/presentation/settings/widgets/theme_upload/theme_upload_view.dart'; +import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/image.dart'; +import 'package:flowy_infra/plugins/bloc/dynamic_plugin_bloc.dart'; +import 'package:flowy_infra/plugins/bloc/dynamic_plugin_event.dart'; +import 'package:flowy_infra/plugins/bloc/dynamic_plugin_state.dart'; import 'package:flowy_infra/theme.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; @@ -14,28 +19,37 @@ class SettingsAppearanceView extends StatelessWidget { @override Widget build(BuildContext context) { return SingleChildScrollView( - child: BlocBuilder( - builder: (context, state) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ThemeModeSetting(currentThemeMode: state.themeMode), - ThemeSetting(currentTheme: state.appTheme.themeName), - ], - ); - }, + child: BlocProvider( + create: (_) => DynamicPluginBloc(), + child: BlocBuilder( + builder: (context, state) { + return Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + BrightnessSetting(currentThemeMode: state.themeMode), + ColorSchemeSetting( + currentTheme: state.appTheme.themeName, + bloc: context.read(), + ), + ], + ); + }, + ), ), ); } } -class ThemeSetting extends StatelessWidget { - final String currentTheme; - const ThemeSetting({ +class ColorSchemeSetting extends StatelessWidget { + const ColorSchemeSetting({ super.key, required this.currentTheme, + required this.bloc, }); + final String currentTheme; + final DynamicPluginBloc bloc; + @override Widget build(BuildContext context) { return Row( @@ -46,52 +60,152 @@ class ThemeSetting extends StatelessWidget { overflow: TextOverflow.ellipsis, ), ), - AppFlowyPopover( - direction: PopoverDirection.bottomWithRightAligned, - child: FlowyTextButton( - currentTheme, - fontColor: Theme.of(context).colorScheme.onBackground, - fillColor: Colors.transparent, - onPressed: () {}, - ), - popupBuilder: (BuildContext context) { - return IntrinsicWidth( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - _themeItemButton(context, BuiltInTheme.defaultTheme), - _themeItemButton(context, BuiltInTheme.dandelion), - _themeItemButton(context, BuiltInTheme.lavender), - ], - ), - ); - }, - ), + ThemeUploadOverlayButton(bloc: bloc), + const SizedBox(width: 4), + ThemeSelectionPopover(currentTheme: currentTheme, bloc: bloc), ], ); } +} - Widget _themeItemButton(BuildContext context, String theme) { +class ThemeUploadOverlayButton extends StatelessWidget { + const ThemeUploadOverlayButton({super.key, required this.bloc}); + + final DynamicPluginBloc bloc; + + @override + Widget build(BuildContext context) { + return FlowyIconButton( + width: 24, + icon: const FlowySvg(name: 'folder'), + iconColorOnHover: Theme.of(context).colorScheme.onPrimary, + onPressed: () => Dialogs.show( + context, + child: BlocProvider.value( + value: bloc, + child: const FlowyDialog( + constraints: BoxConstraints(maxHeight: 300), + child: ThemeUploadWidget(), + ), + ), + ).then((value) { + if (value == null) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: FlowyText.medium( + color: Theme.of(context).colorScheme.onPrimary, + LocaleKeys.settings_appearance_themeUpload_uploadSuccess.tr(), + ), + ), + ); + }), + ); + } +} + +class ThemeSelectionPopover extends StatelessWidget { + const ThemeSelectionPopover({ + super.key, + required this.currentTheme, + required this.bloc, + }); + + final String currentTheme; + final DynamicPluginBloc bloc; + + @override + Widget build(BuildContext context) { + return AppFlowyPopover( + direction: PopoverDirection.bottomWithRightAligned, + child: FlowyTextButton( + currentTheme, + fontColor: Theme.of(context).colorScheme.onBackground, + fillColor: Colors.transparent, + onPressed: () {}, + ), + popupBuilder: (BuildContext context) { + return IntrinsicWidth( + child: BlocBuilder( + bloc: bloc..add(DynamicPluginEvent.load()), + buildWhen: (previous, current) => current is Ready, + builder: (context, state) { + return state.when( + uninitialized: () => const SizedBox.shrink(), + processing: () => const SizedBox.shrink(), + compilationFailure: (message) => const SizedBox.shrink(), + deletionFailure: (message) => const SizedBox.shrink(), + deletionSuccess: () => const SizedBox.shrink(), + compilationSuccess: () => const SizedBox.shrink(), + ready: (plugins) => Column( + mainAxisSize: MainAxisSize.min, + children: [ + ...AppTheme.builtins + .map( + (theme) => _themeItemButton(context, theme.themeName), + ) + .toList(), + if (plugins.isNotEmpty) ...[ + const Divider(), + ...plugins + .map((plugin) => plugin.theme) + .whereType() + .map( + (theme) => _themeItemButton( + context, + theme.themeName, + false, + ), + ) + .toList() + ], + ], + ), + ); + }, + ), + ); + }, + ); + } + + Widget _themeItemButton( + BuildContext context, + String theme, [ + bool isBuiltin = true, + ]) { return SizedBox( height: 32, - child: FlowyButton( - text: FlowyText.medium(theme), - rightIcon: currentTheme == theme - ? const FlowySvg(name: 'grid/checkmark') - : null, - onTap: () { - if (currentTheme != theme) { - context.read().setTheme(theme); - } - }, + child: Row( + children: [ + Expanded( + child: FlowyButton( + text: FlowyText.medium(theme), + rightIcon: currentTheme == theme + ? const FlowySvg(name: 'grid/checkmark') + : null, + onTap: () { + if (currentTheme != theme) { + context.read().setTheme(theme); + } + }, + ), + ), + if (!isBuiltin) + FlowyIconButton( + icon: const FlowySvg(name: 'home/close'), + width: 20, + onPressed: () => + bloc.add(DynamicPluginEvent.removePlugin(name: theme)), + ) + ], ), ); } } -class ThemeModeSetting extends StatelessWidget { +class BrightnessSetting extends StatelessWidget { final ThemeMode currentThemeMode; - const ThemeModeSetting({required this.currentThemeMode, super.key}); + const BrightnessSetting({required this.currentThemeMode, super.key}); @override Widget build(BuildContext context) { diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_file_customize_location_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_file_customize_location_view.dart index 097d3a4631..5451cd5632 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_file_customize_location_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_file_customize_location_view.dart @@ -1,7 +1,7 @@ import 'dart:io'; import 'package:appflowy/startup/entry_point.dart'; -import 'package:appflowy/util/file_picker/file_picker_service.dart'; +import 'package:flowy_infra/file_picker/file_picker_service.dart'; import 'package:appflowy/workspace/application/settings/settings_location_cubit.dart'; import 'package:flowy_infra/image.dart'; import 'package:flowy_infra_ui/style_widget/hover.dart'; diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_file_exporter_widget.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_file_exporter_widget.dart index f673921816..3c69358aa2 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_file_exporter_widget.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_file_exporter_widget.dart @@ -1,8 +1,9 @@ import 'dart:io'; import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/util/file_picker/file_picker_service.dart'; import 'package:appflowy/workspace/application/export/document_exporter.dart'; +import 'package:appflowy_backend/dispatch/dispatch.dart'; +import 'package:flowy_infra/file_picker/file_picker_service.dart'; import 'package:appflowy/workspace/application/settings/settings_file_exporter_cubit.dart'; import 'package:appflowy/workspace/application/settings/share/export_service.dart'; import 'package:appflowy_backend/log.dart'; @@ -10,7 +11,6 @@ import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; import 'package:dartz/dartz.dart' as dartz; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart' hide WidgetBuilder; -import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pbserver.dart'; import 'package:appflowy_backend/protobuf/flowy-folder2/workspace.pb.dart'; import 'package:flutter/material.dart'; diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_confirm_delete_dialog.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_confirm_delete_dialog.dart new file mode 100644 index 0000000000..e7cfd14327 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_confirm_delete_dialog.dart @@ -0,0 +1,59 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/theme.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; + +import 'theme_upload_view.dart'; + +class ThemeConfirmDeleteDialog extends StatelessWidget { + const ThemeConfirmDeleteDialog({ + super.key, + required this.theme, + }); + + final AppTheme theme; + + void onConfirm(BuildContext context) => Navigator.of(context).pop(true); + void onCancel(BuildContext context) => Navigator.of(context).pop(false); + + @override + Widget build(BuildContext context) { + return FlowyDialog( + padding: EdgeInsets.zero, + constraints: const BoxConstraints.tightFor( + width: 300, + height: 100, + ), + title: FlowyText.regular( + LocaleKeys.document_plugins_cover_alertDialogConfirmation.tr(), + textAlign: TextAlign.center, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + SizedBox( + width: ThemeUploadWidget.buttonSize.width, + child: FlowyButton( + text: FlowyText.semibold( + LocaleKeys.button_OK.tr(), + fontSize: ThemeUploadWidget.buttonFontSize, + ), + onTap: () => onConfirm(context), + ), + ), + SizedBox( + width: ThemeUploadWidget.buttonSize.width, + child: FlowyButton( + text: FlowyText.semibold( + LocaleKeys.button_Cancel.tr(), + fontSize: ThemeUploadWidget.buttonFontSize, + ), + onTap: () => onCancel(context), + ), + ), + ], + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_upload_button.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_upload_button.dart new file mode 100644 index 0000000000..73fb23b806 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_upload_button.dart @@ -0,0 +1,41 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/plugins/bloc/dynamic_plugin_bloc.dart'; +import 'package:flowy_infra/plugins/bloc/dynamic_plugin_event.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'theme_upload_view.dart'; + +class ThemeUploadButton extends StatelessWidget { + const ThemeUploadButton({super.key, this.color}); + + final Color? color; + + @override + Widget build(BuildContext context) { + return SizedBox.fromSize( + size: ThemeUploadWidget.buttonSize, + child: FlowyButton( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + color: color ?? Theme.of(context).colorScheme.primary, + ), + hoverColor: color, + text: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + FlowyText.medium( + fontSize: ThemeUploadWidget.buttonFontSize, + color: Theme.of(context).colorScheme.onPrimary, + LocaleKeys.settings_appearance_themeUpload_button.tr(), + ), + ], + ), + onTap: () => BlocProvider.of(context) + .add(DynamicPluginEvent.addPlugin()), + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_upload_decoration.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_upload_decoration.dart new file mode 100644 index 0000000000..87060c349b --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_upload_decoration.dart @@ -0,0 +1,40 @@ +import 'package:dotted_border/dotted_border.dart'; +import 'package:flutter/material.dart'; + +import 'theme_upload_view.dart'; + +class ThemeUploadDecoration extends StatelessWidget { + const ThemeUploadDecoration({super.key, required this.child}); + + final Widget child; + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(ThemeUploadWidget.borderRadius), + color: Theme.of(context).colorScheme.surface, + border: Border.all( + color: Theme.of(context).colorScheme.onBackground.withOpacity( + ThemeUploadWidget.fadeOpacity, + ), + ), + ), + padding: ThemeUploadWidget.padding, + child: DottedBorder( + borderType: BorderType.RRect, + strokeWidth: 1, + dashPattern: const [6, 6], + color: Theme.of(context) + .colorScheme + .onBackground + .withOpacity(ThemeUploadWidget.fadeOpacity), + radius: const Radius.circular(ThemeUploadWidget.borderRadius), + child: ClipRRect( + borderRadius: BorderRadius.circular(ThemeUploadWidget.borderRadius), + child: child, + ), + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_upload_failure_widget.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_upload_failure_widget.dart new file mode 100644 index 0000000000..77f8fcdc68 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_upload_failure_widget.dart @@ -0,0 +1,41 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/image.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:flutter/material.dart'; + +import 'theme_upload_button.dart'; +import 'theme_upload_view.dart'; + +class ThemeUploadFailureWidget extends StatelessWidget { + const ThemeUploadFailureWidget({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + color: Theme.of(context) + .colorScheme + .error + .withOpacity(ThemeUploadWidget.fadeOpacity), + constraints: const BoxConstraints.expand(), + padding: ThemeUploadWidget.padding, + child: Column( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + svgWidget( + 'home/close', + size: ThemeUploadWidget.iconSize, + color: Theme.of(context).colorScheme.onBackground, + ), + FlowyText.medium( + LocaleKeys.settings_appearance_themeUpload_failure.tr(), + overflow: TextOverflow.ellipsis, + ), + ThemeUploadWidget.elementSpacer, + ThemeUploadButton(color: Theme.of(context).colorScheme.error), + ], + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_upload_loading_widget.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_upload_loading_widget.dart new file mode 100644 index 0000000000..28b4115e51 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_upload_loading_widget.dart @@ -0,0 +1,34 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/workspace/presentation/settings/widgets/theme_upload/theme_upload_view.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; + +class ThemeUploadLoadingWidget extends StatelessWidget { + const ThemeUploadLoadingWidget({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + padding: ThemeUploadWidget.padding, + color: Theme.of(context) + .colorScheme + .background + .withOpacity(ThemeUploadWidget.fadeOpacity), + constraints: const BoxConstraints.expand(), + child: Column( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CircularProgressIndicator( + color: Theme.of(context).colorScheme.primary, + ), + ThemeUploadWidget.elementSpacer, + FlowyText.regular( + LocaleKeys.settings_appearance_themeUpload_loading.tr(), + ), + ], + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_upload_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_upload_view.dart new file mode 100644 index 0000000000..c36ca25312 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_upload_view.dart @@ -0,0 +1,79 @@ +import 'package:flowy_infra/plugins/bloc/dynamic_plugin_bloc.dart'; +import 'package:flowy_infra/plugins/bloc/dynamic_plugin_state.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'theme_upload_decoration.dart'; +import 'theme_upload_failure_widget.dart'; +import 'theme_upload_loading_widget.dart'; +import 'upload_new_theme_widget.dart'; + +class ThemeUploadWidget extends StatefulWidget { + const ThemeUploadWidget({super.key}); + + static const double borderRadius = 8; + static const double buttonFontSize = 14; + static const Size buttonSize = Size(72, 28); + static const EdgeInsets padding = EdgeInsets.all(12.0); + static const Size iconSize = Size.square(48); + static const Widget elementSpacer = SizedBox(height: 12); + static const double fadeOpacity = 0.5; + static const Duration fadeDuration = Duration(milliseconds: 750); + + @override + State createState() => _ThemeUploadWidgetState(); +} + +class _ThemeUploadWidgetState extends State { + void listen(BuildContext context, DynamicPluginState state) { + setState(() { + state.when( + uninitialized: () => null, + ready: (plugins) { + child = + const UploadNewThemeWidget(key: Key('upload_new_theme_widget')); + }, + deletionSuccess: () { + child = + const UploadNewThemeWidget(key: Key('upload_new_theme_widget')); + }, + processing: () { + child = const ThemeUploadLoadingWidget( + key: Key('upload_theme_loading_widget'), + ); + }, + compilationFailure: (path) { + child = const ThemeUploadFailureWidget( + key: Key('upload_theme_failure_widget'), + ); + }, + compilationSuccess: () { + if (Navigator.of(context).canPop()) { + Navigator.of(context) + .pop(const DynamicPluginState.compilationSuccess()); + } + }, + deletionFailure: (path) {}, + ); + }); + } + + Widget child = + const UploadNewThemeWidget(key: Key('upload_new_theme_widget')); + + @override + Widget build(BuildContext context) { + return BlocListener( + listener: listen, + child: ThemeUploadDecoration( + child: Center( + child: AnimatedSwitcher( + duration: ThemeUploadWidget.fadeDuration, + switchInCurve: Curves.easeInOutCubicEmphasized, + child: child, + ), + ), + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/upload_new_theme_widget.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/upload_new_theme_widget.dart new file mode 100644 index 0000000000..8e05c66a8a --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/upload_new_theme_widget.dart @@ -0,0 +1,90 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/workspace/presentation/settings/widgets/theme_upload/theme_upload_button.dart'; +import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/image.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flowy_infra_ui/widget/error_page.dart'; +import 'package:flutter/material.dart'; +import 'package:url_launcher/url_launcher.dart'; + +import 'theme_upload_view.dart'; + +class UploadNewThemeWidget extends StatelessWidget { + const UploadNewThemeWidget({super.key}); + + static const learnMoreRedirect = + 'https://appflowy.gitbook.io/docs/essential-documentation/themes'; + + @override + Widget build(BuildContext context) { + return Container( + color: Theme.of(context) + .colorScheme + .background + .withOpacity(ThemeUploadWidget.fadeOpacity), + padding: ThemeUploadWidget.padding, + child: Column( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + svgWidget( + 'folder', + size: ThemeUploadWidget.iconSize, + color: Theme.of(context).colorScheme.onBackground, + ), + FlowyText.medium( + LocaleKeys.settings_appearance_themeUpload_description.tr(), + overflow: TextOverflow.ellipsis, + ), + ThemeUploadWidget.elementSpacer, + SizedBox( + height: ThemeUploadWidget.buttonSize.height, + child: IntrinsicWidth( + child: FlowyButton( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + color: Theme.of(context).colorScheme.onBackground, + ), + hoverColor: Theme.of(context).colorScheme.onBackground, + text: FlowyText.medium( + fontSize: ThemeUploadWidget.buttonFontSize, + color: Theme.of(context).colorScheme.onPrimary, + LocaleKeys.document_plugins_autoGeneratorLearnMore.tr(), + ), + onTap: () async { + final uri = Uri.parse(learnMoreRedirect); + if (await canLaunchUrl(uri)) { + await launchUrl(uri); + } else { + if (context.mounted) { + Dialogs.show( + context, + child: FlowyDialog( + child: FlowyErrorPage.message( + LocaleKeys + .settings_appearance_themeUpload_urlUploadFailure + .tr() + .replaceAll( + '{}', + uri.toString(), + ), + howToFix: + LocaleKeys.errorDialog_howToFixFallback.tr(), + ), + ), + ); + } + } + }, + ), + ), + ), + const Divider(), + ThemeUploadWidget.elementSpacer, + const ThemeUploadButton(), + ], + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/utils/form_factor.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/utils/form_factor.dart new file mode 100644 index 0000000000..83a96075f5 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/utils/form_factor.dart @@ -0,0 +1,19 @@ +enum FormFactor { + mobile._(600), + tablet._(840), + desktop._(1280); + + const FormFactor._(this.width); + + final double width; + + factory FormFactor.fromWidth(double width) { + if (width < FormFactor.mobile.width) { + return FormFactor.mobile; + } else if (width < FormFactor.tablet.width) { + return FormFactor.tablet; + } else { + return FormFactor.desktop; + } + } +} diff --git a/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/colorscheme.dart b/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/colorscheme.dart index 22bf6a93ca..9f80fb694a 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/colorscheme.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/colorscheme.dart @@ -1,10 +1,14 @@ +import 'package:flowy_infra/utils/color_converter.dart'; import 'package:flutter/material.dart'; import 'package:flowy_infra/theme.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; import 'default_colorscheme.dart'; import 'dandelion.dart'; import 'lavender.dart'; +part 'colorscheme.g.dart'; + /// A map of all the built-in themes. /// /// The key is the theme name, and the value is a list of two color schemes: @@ -25,8 +29,12 @@ const Map> themeMap = { ], }; -@immutable -abstract class FlowyColorScheme { +@JsonSerializable( + converters: [ + ColorConverter(), + ], +) +class FlowyColorScheme { final Color surface; final Color hover; final Color selector; @@ -127,12 +135,8 @@ abstract class FlowyColorScheme { required this.toggleButtonBGColor, }); - factory FlowyColorScheme.builtIn(String themeName, Brightness brightness) { - switch (brightness) { - case Brightness.light: - return themeMap[themeName]?[0] ?? const DefaultColorScheme.light(); - case Brightness.dark: - return themeMap[themeName]?[1] ?? const DefaultColorScheme.dark(); - } - } + factory FlowyColorScheme.fromJson(Map json) => + _$FlowyColorSchemeFromJson(json); + + Map toJson() => _$FlowyColorSchemeToJson(this); } diff --git a/frontend/appflowy_flutter/lib/util/file_picker/file_picker_impl.dart b/frontend/appflowy_flutter/packages/flowy_infra/lib/file_picker/file_picker_impl.dart similarity index 88% rename from frontend/appflowy_flutter/lib/util/file_picker/file_picker_impl.dart rename to frontend/appflowy_flutter/packages/flowy_infra/lib/file_picker/file_picker_impl.dart index b6f16d6d4f..2e4d082761 100644 --- a/frontend/appflowy_flutter/lib/util/file_picker/file_picker_impl.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra/lib/file_picker/file_picker_impl.dart @@ -1,4 +1,4 @@ -import 'package:appflowy/util/file_picker/file_picker_service.dart'; +import 'package:flowy_infra/file_picker/file_picker_service.dart'; import 'package:file_picker/file_picker.dart' as fp; class FilePicker implements FilePickerService { @@ -40,11 +40,11 @@ class FilePicker implements FilePickerService { String? dialogTitle, String? fileName, String? initialDirectory, - fp.FileType type = fp.FileType.any, + FileType type = FileType.any, List? allowedExtensions, bool lockParentWindow = false, - }) { - return fp.FilePicker.platform.saveFile( + }) async { + final result = await fp.FilePicker.platform.saveFile( dialogTitle: dialogTitle, fileName: fileName, initialDirectory: initialDirectory, @@ -52,5 +52,7 @@ class FilePicker implements FilePickerService { allowedExtensions: allowedExtensions, lockParentWindow: lockParentWindow, ); + + return result; } } diff --git a/frontend/appflowy_flutter/lib/util/file_picker/file_picker_service.dart b/frontend/appflowy_flutter/packages/flowy_infra/lib/file_picker/file_picker_service.dart similarity index 92% rename from frontend/appflowy_flutter/lib/util/file_picker/file_picker_service.dart rename to frontend/appflowy_flutter/packages/flowy_infra/lib/file_picker/file_picker_service.dart index 66dec35d3b..8039ea1e25 100644 --- a/frontend/appflowy_flutter/lib/util/file_picker/file_picker_service.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra/lib/file_picker/file_picker_service.dart @@ -1,3 +1,6 @@ +export 'package:file_picker/file_picker.dart' + show FileType, FilePickerStatus, PlatformFile; + import 'package:file_picker/file_picker.dart'; class FilePickerResult { diff --git a/frontend/appflowy_flutter/packages/flowy_infra/lib/plugins/bloc/dynamic_plugin_bloc.dart b/frontend/appflowy_flutter/packages/flowy_infra/lib/plugins/bloc/dynamic_plugin_bloc.dart new file mode 100644 index 0000000000..df37379027 --- /dev/null +++ b/frontend/appflowy_flutter/packages/flowy_infra/lib/plugins/bloc/dynamic_plugin_bloc.dart @@ -0,0 +1,71 @@ +import 'package:bloc/bloc.dart'; +import 'package:flowy_infra/plugins/service/models/exceptions.dart'; +import 'package:flowy_infra/plugins/service/plugin_service.dart'; + +import '../../file_picker/file_picker_impl.dart'; +import 'dynamic_plugin_event.dart'; +import 'dynamic_plugin_state.dart'; + +class DynamicPluginBloc extends Bloc { + DynamicPluginBloc({FilePicker? filePicker}) + : super(const DynamicPluginState.uninitialized()) { + on(dispatch); + add(DynamicPluginEvent.load()); + } + + Future dispatch( + DynamicPluginEvent event, Emitter emit) async { + await event.when( + addPlugin: () => addPlugin(emit), + removePlugin: (name) => removePlugin(emit, name), + load: () => onLoadRequested(emit), + ); + } + + Future onLoadRequested(Emitter emit) async { + emit(DynamicPluginState.ready( + plugins: await FlowyPluginService.instance.plugins)); + } + + Future addPlugin(Emitter emit) async { + emit(const DynamicPluginState.processing()); + try { + final plugin = await FlowyPluginService.pick(); + if (plugin == null) { + emit(DynamicPluginState.ready( + plugins: await FlowyPluginService.instance.plugins)); + return; + } + await FlowyPluginService.instance.addPlugin(plugin); + } on PluginCompilationException { + // TODO(a-wallen): Remove path from compilation failure + emit(const DynamicPluginState.compilationFailure(path: '')); + return; + } + + emit(const DynamicPluginState.compilationSuccess()); + emit(DynamicPluginState.ready( + plugins: await FlowyPluginService.instance.plugins)); + } + + Future removePlugin( + Emitter emit, String name) async { + emit(const DynamicPluginState.processing()); + + final plugin = await FlowyPluginService.instance.lookup(name: name); + + if (plugin == null) { + emit(DynamicPluginState.ready( + plugins: await FlowyPluginService.instance.plugins)); + return; + } + + await FlowyPluginService.removePlugin(plugin); + + emit(const DynamicPluginState.deletionSuccess()); + emit( + DynamicPluginState.ready( + plugins: await FlowyPluginService.instance.plugins), + ); + } +} diff --git a/frontend/appflowy_flutter/packages/flowy_infra/lib/plugins/bloc/dynamic_plugin_event.dart b/frontend/appflowy_flutter/packages/flowy_infra/lib/plugins/bloc/dynamic_plugin_event.dart new file mode 100644 index 0000000000..71c1fce9c0 --- /dev/null +++ b/frontend/appflowy_flutter/packages/flowy_infra/lib/plugins/bloc/dynamic_plugin_event.dart @@ -0,0 +1,11 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'dynamic_plugin_event.freezed.dart'; + +@freezed +class DynamicPluginEvent with _$DynamicPluginEvent { + factory DynamicPluginEvent.addPlugin() = _AddPlugin; + factory DynamicPluginEvent.removePlugin({required String name}) = + _RemovePlugin; + factory DynamicPluginEvent.load() = _Load; +} diff --git a/frontend/appflowy_flutter/packages/flowy_infra/lib/plugins/bloc/dynamic_plugin_state.dart b/frontend/appflowy_flutter/packages/flowy_infra/lib/plugins/bloc/dynamic_plugin_state.dart new file mode 100644 index 0000000000..566ce567e6 --- /dev/null +++ b/frontend/appflowy_flutter/packages/flowy_infra/lib/plugins/bloc/dynamic_plugin_state.dart @@ -0,0 +1,22 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +import '../service/models/flowy_dynamic_plugin.dart'; + +part 'dynamic_plugin_state.freezed.dart'; + +@freezed +class DynamicPluginState with _$DynamicPluginState { + const factory DynamicPluginState.uninitialized() = _Uninitialized; + const factory DynamicPluginState.ready({ + required Iterable plugins, + }) = Ready; + const factory DynamicPluginState.processing() = _Processing; + const factory DynamicPluginState.compilationFailure({ + required String path, + }) = _CompilationFailure; + const factory DynamicPluginState.deletionFailure({ + required String path, + }) = _DeletionFailure; + const factory DynamicPluginState.deletionSuccess() = _DeletionSuccess; + const factory DynamicPluginState.compilationSuccess() = _CompilationSuccess; +} diff --git a/frontend/appflowy_flutter/packages/flowy_infra/lib/plugins/service/location_service.dart b/frontend/appflowy_flutter/packages/flowy_infra/lib/plugins/service/location_service.dart new file mode 100644 index 0000000000..64c6d2b586 --- /dev/null +++ b/frontend/appflowy_flutter/packages/flowy_infra/lib/plugins/service/location_service.dart @@ -0,0 +1,13 @@ +import 'dart:io'; + +class PluginLocationService { + const PluginLocationService({ + required Future fallback, + }) : _fallback = fallback; + + final Future _fallback; + + Future get fallback async => _fallback; + + Future get location async => fallback; +} diff --git a/frontend/appflowy_flutter/packages/flowy_infra/lib/plugins/service/models/exceptions.dart b/frontend/appflowy_flutter/packages/flowy_infra/lib/plugins/service/models/exceptions.dart new file mode 100644 index 0000000000..6f3c1e34b9 --- /dev/null +++ b/frontend/appflowy_flutter/packages/flowy_infra/lib/plugins/service/models/exceptions.dart @@ -0,0 +1,5 @@ +class PluginCompilationException implements Exception { + final String message; + + PluginCompilationException(this.message); +} diff --git a/frontend/appflowy_flutter/packages/flowy_infra/lib/plugins/service/models/flowy_dynamic_plugin.dart b/frontend/appflowy_flutter/packages/flowy_infra/lib/plugins/service/models/flowy_dynamic_plugin.dart new file mode 100644 index 0000000000..3b6ed43cba --- /dev/null +++ b/frontend/appflowy_flutter/packages/flowy_infra/lib/plugins/service/models/flowy_dynamic_plugin.dart @@ -0,0 +1,137 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:file/memory.dart'; +import 'package:flowy_infra/colorscheme/colorscheme.dart'; +import 'package:flowy_infra/plugins/service/models/exceptions.dart'; +import 'package:flowy_infra/theme.dart'; +import 'package:path/path.dart' as p; + +import 'plugin_type.dart'; + +typedef DynamicPluginLibrary = Iterable; + +/// A class that encapsulates dynamically loaded plugins for AppFlowy. +/// +/// This class can be modified to support loading node widget builders and other +/// plugins that are dynamically loaded at runtime for the editor. For now, +/// it only supports loading app themes. +class FlowyDynamicPlugin { + FlowyDynamicPlugin._({ + required String name, + required String path, + this.theme, + }) : _name = name, + _path = path; + + /// The plugins should be loaded into a folder with the extension `.flowy_plugin`. + static bool isPlugin(FileSystemEntity entity) => + entity is Directory && p.extension(entity.path).contains(ext); + + /// The extension for the plugin folder. + static const String ext = 'flowy_plugin'; + static String get lightExtension => ['light', 'json'].join('.'); + static String get darkExtension => ['dark', 'json'].join('.'); + + String get name => _name; + late final String _name; + + String get _fsPluginName => [name, ext].join('.'); + + final AppTheme? theme; + final String _path; + + Directory get source { + return Directory(_path); + } + + /// Loads and "compiles" loaded plugins. + /// + /// If the plugin loaded does not contain the `.flowy_plugin` extension, this + /// this method will throw an error. Likewise, if the plugin does not follow + /// the expected format, this method will throw an error. + static Future decode({required Directory src}) async { + // throw an error if the plugin does not follow the proper format. + if (!isPlugin(src)) { + throw PluginCompilationException( + 'The plugin directory must have the extension `.flowy_plugin`.', + ); + } + + // throws an error if the plugin does not follow the proper format. + final type = PluginType.from(src: src); + + switch (type) { + case PluginType.theme: + return _theme(src: src); + } + } + + /// Encodes the plugin in memory. The Directory given is not the actual + /// directory on the file system, but rather a virtual directory in memory. + /// + /// Instances of this class should always have a path on disk, otherwise a + /// compilation error will be thrown during the construction of this object. + Future encode() async { + final fs = MemoryFileSystem(); + final result = fs.directory(_fsPluginName)..createSync(); + + final lightPath = p.join(_fsPluginName, '$name.$lightExtension'); + result.childFile(lightPath).createSync(); + result + .childFile(lightPath) + .writeAsStringSync(jsonEncode(theme!.lightTheme.toJson())); + + final darkPath = p.join(_fsPluginName, '$name.$darkExtension'); + result.childFile(darkPath).createSync(); + result + .childFile(p.join(_fsPluginName, '$name.$darkExtension')) + .writeAsStringSync(jsonEncode(theme!.darkTheme.toJson())); + + return result; + } + + /// Theme plugins should have the following format. + /// > directory.flowy_plugin // plugin root + /// > - theme.light.json // the light theme + /// > - theme.dark.json // the dark theme + /// + /// If the theme does not adhere to that format, it is considered an error. + static Future _theme({required Directory src}) async { + late final String name; + try { + name = p.basenameWithoutExtension(src.path).split('.').first; + } catch (e) { + throw PluginCompilationException( + 'The theme plugin does not adhere to the following format: `.flowy_plugin`.', + ); + } + + final light = src + .listSync() + .where((event) => + event is File && p.basename(event.path).contains(lightExtension)) + .first as File; + + final dark = src + .listSync() + .where((event) => + event is File && p.basename(event.path).contains(darkExtension)) + .first as File; + + final theme = AppTheme( + themeName: name, + builtIn: false, + lightTheme: + FlowyColorScheme.fromJson(jsonDecode(await light.readAsString())), + darkTheme: + FlowyColorScheme.fromJson(jsonDecode(await dark.readAsString())), + ); + + return FlowyDynamicPlugin._( + name: theme.themeName, + path: src.path, + theme: theme, + ); + } +} diff --git a/frontend/appflowy_flutter/packages/flowy_infra/lib/plugins/service/models/plugin_type.dart b/frontend/appflowy_flutter/packages/flowy_infra/lib/plugins/service/models/plugin_type.dart new file mode 100644 index 0000000000..1b6f8a8f4a --- /dev/null +++ b/frontend/appflowy_flutter/packages/flowy_infra/lib/plugins/service/models/plugin_type.dart @@ -0,0 +1,31 @@ +import 'dart:io'; + +import 'package:flowy_infra/plugins/service/models/exceptions.dart'; +import 'package:flowy_infra/plugins/service/models/flowy_dynamic_plugin.dart'; +import 'package:path/path.dart' as p; + +enum PluginType { + theme._(); + + const PluginType._(); + + factory PluginType.from({required Directory src}) { + if (_isTheme(src)) { + return PluginType.theme; + } + throw PluginCompilationException( + 'Could not determine the plugin type from source `$src`.'); + } + + static bool _isTheme(Directory plugin) { + final files = plugin.listSync(); + return files.any((entity) => + entity is File && + p + .basename(entity.path) + .endsWith(FlowyDynamicPlugin.lightExtension)) && + files.any((entity) => + entity is File && + p.basename(entity.path).endsWith(FlowyDynamicPlugin.darkExtension)); + } +} diff --git a/frontend/appflowy_flutter/packages/flowy_infra/lib/plugins/service/plugin_service.dart b/frontend/appflowy_flutter/packages/flowy_infra/lib/plugins/service/plugin_service.dart new file mode 100644 index 0000000000..bd81333be8 --- /dev/null +++ b/frontend/appflowy_flutter/packages/flowy_infra/lib/plugins/service/plugin_service.dart @@ -0,0 +1,102 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:flowy_infra/file_picker/file_picker_impl.dart'; + +import 'package:path/path.dart' as p; +import 'package:path_provider/path_provider.dart'; +import 'location_service.dart'; +import 'models/flowy_dynamic_plugin.dart'; + +/// A service to maintain the state of the plugins for AppFlowy. +class FlowyPluginService { + FlowyPluginService._(); + static final FlowyPluginService _instance = FlowyPluginService._(); + static FlowyPluginService get instance => _instance; + + PluginLocationService _locationService = PluginLocationService( + fallback: getApplicationDocumentsDirectory(), + ); + + void setLocation(PluginLocationService locationService) => + _locationService = locationService; + + Future> get _targets async { + final location = await _locationService.location; + final targets = location.listSync().where(FlowyDynamicPlugin.isPlugin); + return targets.map((entity) => entity as Directory).toList(); + } + + /// Searches the [PluginLocationService.location] for plugins and compiles them. + Future get plugins async { + final List compiled = []; + for (final src in await _targets) { + final plugin = await FlowyDynamicPlugin.decode(src: src); + compiled.add(plugin); + } + return compiled; + } + + /// Chooses a plugin from the file system using FilePickerService and tries to compile it. + /// + /// If the operation is cancelled or the plugin is invalid, this method will return null. + static Future pick({FilePicker? service}) async { + service ??= FilePicker(); + + final result = await service.getDirectoryPath(); + + if (result == null) { + return null; + } + + final directory = Directory(result); + return FlowyDynamicPlugin.decode(src: directory); + } + + /// Searches the plugin registry for a plugin with the given name. + Future lookup({required String name}) async { + final library = await plugins; + return library + // cast to nullable type to allow return of null if not found. + .cast() + // null assert is fine here because the original list was non-nullable + .firstWhere((plugin) => plugin!.name == name, orElse: () => null); + } + + /// Adds a plugin to the registry. To construct a [FlowyDynamicPlugin] + /// use [FlowyDynamicPlugin.encode()] + Future addPlugin(FlowyDynamicPlugin plugin) async { + // try to compile the plugin before we add it to the registry. + final source = await plugin.encode(); + // add the plugin to the registry + final destionation = [ + (await _locationService.location).path, + p.basename(source.path), + ].join(Platform.pathSeparator); + + _copyDirectorySync(source, Directory(destionation)); + } + + /// Removes a plugin from the registry. + static Future removePlugin(FlowyDynamicPlugin plugin) async { + final target = plugin.source; + await target.delete(recursive: true); + } + + static void _copyDirectorySync(Directory source, Directory destination) { + if (!destination.existsSync()) { + destination.createSync(recursive: true); + } + + for (final child in source.listSync(recursive: false)) { + final newPath = p.join(destination.path, p.basename(child.path)); + if (child is File) { + File(newPath) + ..createSync(recursive: true) + ..writeAsStringSync(child.readAsStringSync()); + } else if (child is Directory) { + _copyDirectorySync(child, Directory(newPath)); + } + } + } +} diff --git a/frontend/appflowy_flutter/packages/flowy_infra/lib/theme.dart b/frontend/appflowy_flutter/packages/flowy_infra/lib/theme.dart index 1f4f3584e1..8b1b8994b6 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra/lib/theme.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra/lib/theme.dart @@ -1,5 +1,6 @@ import 'package:flowy_infra/colorscheme/colorscheme.dart'; -import 'package:flutter/material.dart'; +import 'package:flowy_infra/colorscheme/default_colorscheme.dart'; +import 'plugins/service/plugin_service.dart'; class BuiltInTheme { static const String defaultTheme = 'Default'; @@ -9,22 +10,58 @@ class BuiltInTheme { class AppTheme { // metadata member + final bool builtIn; final String themeName; final FlowyColorScheme lightTheme; final FlowyColorScheme darkTheme; // static final Map _cachedJsonData = {}; const AppTheme({ + required this.builtIn, required this.themeName, required this.lightTheme, required this.darkTheme, }); - factory AppTheme.fromName(String themeName) { - return AppTheme( - themeName: themeName, - lightTheme: FlowyColorScheme.builtIn(themeName, Brightness.light), - darkTheme: FlowyColorScheme.builtIn(themeName, Brightness.dark), - ); + static const AppTheme fallback = AppTheme( + builtIn: true, + themeName: BuiltInTheme.defaultTheme, + lightTheme: DefaultColorScheme.light(), + darkTheme: DefaultColorScheme.dark(), + ); + + static Future> _plugins(FlowyPluginService service) async { + final plugins = await service.plugins; + return plugins.map((plugin) => plugin.theme).whereType(); + } + + static Iterable get builtins => themeMap.entries + .map( + (entry) => AppTheme( + builtIn: true, + themeName: entry.key, + lightTheme: entry.value[0], + darkTheme: entry.value[1], + ), + ) + .toList(); + + static Future> themes(FlowyPluginService service) async => + [ + ...builtins, + ...(await _plugins(service)), + ]; + + static Future fromName( + String themeName, { + FlowyPluginService? pluginService, + }) async { + pluginService ??= FlowyPluginService.instance; + for (final theme in await themes(pluginService)) { + if (theme.themeName == themeName) { + return theme; + } + } + throw ArgumentError('The theme $themeName does not exist.'); } } diff --git a/frontend/appflowy_flutter/packages/flowy_infra/lib/utils/color_converter.dart b/frontend/appflowy_flutter/packages/flowy_infra/lib/utils/color_converter.dart new file mode 100644 index 0000000000..19ca90d78f --- /dev/null +++ b/frontend/appflowy_flutter/packages/flowy_infra/lib/utils/color_converter.dart @@ -0,0 +1,17 @@ +import 'package:flutter/material.dart'; +import 'package:json_annotation/json_annotation.dart'; + +class ColorConverter implements JsonConverter { + const ColorConverter(); + + static const Color fallback = Colors.transparent; + + @override + Color fromJson(String radixString) { + final int? color = int.tryParse(radixString); + return color == null ? fallback : Color(color); + } + + @override + String toJson(Color color) => "0x${color.value.toRadixString(16)}"; +} diff --git a/frontend/appflowy_flutter/packages/flowy_infra/pubspec.yaml b/frontend/appflowy_flutter/packages/flowy_infra/pubspec.yaml index 8991da7ce4..59de25ceb1 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra/pubspec.yaml +++ b/frontend/appflowy_flutter/packages/flowy_infra/pubspec.yaml @@ -10,14 +10,25 @@ environment: dependencies: flutter: sdk: flutter - time: '>=2.0.0' + flutter_svg: ^2.0.2 + json_annotation: ^4.7.0 + path_provider: ^2.0.15 + path: ^1.8.2 + textstyle_extensions: "2.0.0-nullsafety" + time: ">=2.0.0" uuid: ">=2.2.2" - flutter_svg: ^2.0.6 + bloc: ^8.1.2 + freezed_annotation: ^2.1.0 + file_picker: ^5.3.1 + file: ^6.1.4 dev_dependencies: flutter_test: sdk: flutter + build_runner: ^2.2.0 flutter_lints: ^2.0.1 + freezed: 2.2.0 + json_serializable: ^6.5.4 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/dialog/styled_dialogs.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/dialog/styled_dialogs.dart index f853c65e69..67ff399a14 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/dialog/styled_dialogs.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/dialog/styled_dialogs.dart @@ -9,7 +9,7 @@ extension IntoDialog on Widget { Future show(BuildContext context) async { FocusNode dialogFocusNode = FocusNode(); await Dialogs.show( - RawKeyboardListener( + child: RawKeyboardListener( focusNode: dialogFocusNode, onKey: (value) { if (value.isKeyPressed(LogicalKeyboardKey.escape)) { @@ -88,7 +88,8 @@ class StyledDialog extends StatelessWidget { } class Dialogs { - static Future show(Widget child, BuildContext context) async { + static Future show(BuildContext context, + {required Widget child}) async { return await Navigator.of(context).push( StyledDialogRoute( barrier: DialogBarrier(color: Colors.black.withOpacity(0.4)), diff --git a/frontend/appflowy_flutter/pubspec.lock b/frontend/appflowy_flutter/pubspec.lock index dfa33eb8d1..c92f34d3f1 100644 --- a/frontend/appflowy_flutter/pubspec.lock +++ b/frontend/appflowy_flutter/pubspec.lock @@ -337,6 +337,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.4.1" + dotted_border: + dependency: "direct main" + description: + name: dotted_border + sha256: "07a5c5e8d4e6e992279e190e0352be8faa5b8f96d81c77a78b2d42f060279840" + url: "https://pub.dev" + source: hosted + version: "2.0.0+3" easy_localization: dependency: "direct main" description: @@ -410,7 +418,7 @@ packages: source: hosted version: "6.1.4" file_picker: - dependency: "direct main" + dependency: transitive description: name: file_picker sha256: "9d6e95ec73abbd31ec54d0e0df8a961017e165aba1395e462e5b31ea0c165daf" @@ -539,10 +547,10 @@ packages: dependency: "direct dev" description: name: freezed - sha256: "2edb9ef971d0f803860ecd9084afd48c717d002141ad77b69be3e976bee7190e" + sha256: a9520490532087cf38bf3f7de478ab6ebeb5f68bb1eb2641546d92719b224445 url: "https://pub.dev" source: hosted - version: "2.3.4" + version: "2.3.5" freezed_annotation: dependency: "direct main" description: @@ -905,6 +913,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.8.3" + path_drawing: + dependency: transitive + description: + name: path_drawing + sha256: bbb1934c0cbb03091af082a6389ca2080345291ef07a5fa6d6e078ba8682f977 + url: "https://pub.dev" + source: hosted + version: "1.0.1" path_parsing: dependency: transitive description: @@ -1454,6 +1470,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.2" + textstyle_extensions: + dependency: transitive + description: + name: textstyle_extensions + sha256: b0538352844fb4d1d0eea82f7bc6b96e4dae03a3a071247e4dcc85ec627b2c6c + url: "https://pub.dev" + source: hosted + version: "2.0.0-nullsafety" time: dependency: "direct main" description: diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index d869d7e5af..7b5a7f835d 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -90,7 +90,6 @@ dependencies: bloc: ^8.1.2 shared_preferences: ^2.1.1 google_fonts: ^4.0.5 - file_picker: ^5.3.1 percent_indicator: ^4.2.3 calendar_view: ^1.0.3 window_manager: ^0.3.4 @@ -102,6 +101,7 @@ dependencies: nanoid: ^1.0.0 supabase_flutter: ^1.10.0 envied: ^0.3.0+3 + dotted_border: ^2.0.0+3 dev_dependencies: flutter_lints: ^2.0.1 diff --git a/frontend/appflowy_flutter/test/bloc_test/app_setting_test/appearance_test.dart b/frontend/appflowy_flutter/test/bloc_test/app_setting_test/appearance_test.dart index c0f66d7e20..8488118f84 100644 --- a/frontend/appflowy_flutter/test/bloc_test/app_setting_test/appearance_test.dart +++ b/frontend/appflowy_flutter/test/bloc_test/app_setting_test/appearance_test.dart @@ -2,6 +2,7 @@ import 'package:appflowy/user/application/user_settings_service.dart'; import 'package:appflowy/workspace/application/appearance.dart'; import 'package:bloc_test/bloc_test.dart'; import 'package:appflowy_backend/protobuf/flowy-user/user_setting.pb.dart'; +import 'package:flowy_infra/theme.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -54,5 +55,13 @@ void main() { expect(bloc.getValue("123"), null); }, ); + + blocTest( + 'initial state uses fallback theme', + build: () => AppearanceSettingsCubit(appearanceSetting), + verify: (bloc) { + expect(bloc.state.appTheme.themeName, AppTheme.fallback.themeName); + }, + ); }); } diff --git a/frontend/appflowy_flutter/test/unit_test/theme/theme_test.dart b/frontend/appflowy_flutter/test/unit_test/theme/theme_test.dart new file mode 100644 index 0000000000..b4560360f0 --- /dev/null +++ b/frontend/appflowy_flutter/test/unit_test/theme/theme_test.dart @@ -0,0 +1,80 @@ +import 'package:flowy_infra/colorscheme/colorscheme.dart'; +import 'package:flowy_infra/plugins/service/location_service.dart'; +import 'package:flowy_infra/plugins/service/models/flowy_dynamic_plugin.dart'; +import 'package:flowy_infra/plugins/service/plugin_service.dart'; +import 'package:flowy_infra/theme.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; + +class MockPluginService implements FlowyPluginService { + @override + Future addPlugin(FlowyDynamicPlugin plugin) => + throw UnimplementedError(); + + @override + Future lookup({required String name}) => + throw UnimplementedError(); + + @override + Future get plugins async => const Iterable.empty(); + + @override + void setLocation(PluginLocationService locationService) => + throw UnimplementedError(); +} + +void main() { + WidgetsFlutterBinding.ensureInitialized(); + group('AppTheme', () { + test('fallback theme', () { + const theme = AppTheme.fallback; + + expect(theme.builtIn, true); + expect(theme.themeName, BuiltInTheme.defaultTheme); + expect(theme.lightTheme, isA()); + expect(theme.darkTheme, isA()); + }); + + test('built-in themes', () { + final themes = AppTheme.builtins; + + expect(themes, isNotEmpty); + for (final theme in themes) { + expect(theme.builtIn, true); + expect( + theme.themeName, + anyOf([ + BuiltInTheme.defaultTheme, + BuiltInTheme.dandelion, + BuiltInTheme.lavender, + ]), + ); + expect(theme.lightTheme, isA()); + expect(theme.darkTheme, isA()); + } + }); + + test('fromName returns existing theme', () async { + final theme = await AppTheme.fromName( + BuiltInTheme.defaultTheme, + pluginService: MockPluginService(), + ); + + expect(theme, isNotNull); + expect(theme.builtIn, true); + expect(theme.themeName, BuiltInTheme.defaultTheme); + expect(theme.lightTheme, isA()); + expect(theme.darkTheme, isA()); + }); + + test('fromName throws error for non-existent theme', () async { + expect( + () async => await AppTheme.fromName( + 'bogus', + pluginService: MockPluginService(), + ), + throwsArgumentError, + ); + }); + }); +} diff --git a/frontend/scripts/code_generation/freezed/generate_freezed.cmd b/frontend/scripts/code_generation/freezed/generate_freezed.cmd new file mode 100644 index 0000000000..4b607948fa --- /dev/null +++ b/frontend/scripts/code_generation/freezed/generate_freezed.cmd @@ -0,0 +1,38 @@ +@echo off + +REM Store the current working directory +set "original_dir=%CD%" + +REM Change the current working directory to the script's location +cd /d "%~dp0" + +REM Navigate to the project root +cd ..\..\..\appflowy_flutter + +REM Navigate to the appflowy_flutter directory and generate files +echo Generating files for appflowy_flutter +call flutter clean >nul 2>&1 && call flutter packages pub get >nul 2>&1 && call dart run build_runner clean && call dart run build_runner build -d +echo Done generating files for appflowy_flutter + +echo Generating files for packages +cd packages +for /D %%d in (*) do ( + REM Navigate into the subdirectory + cd "%%d" + + REM Check if the subdirectory contains a pubspec.yaml file + if exist "pubspec.yaml" ( + echo Generating freezed files in %%d... + echo Please wait while we clean the project and fetch the dependencies. + call flutter clean >nul 2>&1 && call flutter packages pub get >nul 2>&1 && call dart run build_runner clean && call dart run build_runner build -d + echo Done running build command in %%d + ) else ( + echo No pubspec.yaml found in %%d, it can't be a Dart project. Skipping. + ) + + REM Navigate back to the packages directory + cd .. +) + +REM Return to the original directory +cd /d "%original_dir%" diff --git a/frontend/scripts/code_generation/freezed/generate_freezed.sh b/frontend/scripts/code_generation/freezed/generate_freezed.sh new file mode 100644 index 0000000000..f4c84d910e --- /dev/null +++ b/frontend/scripts/code_generation/freezed/generate_freezed.sh @@ -0,0 +1,37 @@ +#!/bin/bash + +# Store the current working directory +original_dir=$(pwd) + +cd "$(dirname "$0")" + +# Navigate to the project root +cd ../../../appflowy_flutter + +# Navigate to the appflowy_flutter directory and generate files +echo "Generating files for appflowy_flutter" +flutter clean >/dev/null 2>&1 && flutter packages pub get >/dev/null 2>&1 && dart run build_runner clean && dart run build_runner build -d +echo "Done generating files for appflowy_flutter" + +echo "Generating files for packages" +cd packages +for d in */ ; do + # Navigate into the subdirectory + cd "$d" + + # Check if the subdirectory contains a pubspec.yaml file + if [ -f "pubspec.yaml" ]; then + echo "Generating freezed files in $d..." + echo "Please wait while we clean the project and fetch the dependencies." + flutter clean >/dev/null 2>&1 && flutter packages pub get >/dev/null 2>&1 && dart run build_runner clean && dart run build_runner build -d + echo "Done running build command in $d" + else + echo "No pubspec.yaml found in $d, it can\'t be a Dart project. Skipping." + fi + + # Navigate back to the packages directory + cd .. +done + +# Return to the original directory +cd "$original_dir" \ No newline at end of file diff --git a/frontend/scripts/code_generation/generate.cmd b/frontend/scripts/code_generation/generate.cmd new file mode 100644 index 0000000000..17fcfa420f --- /dev/null +++ b/frontend/scripts/code_generation/generate.cmd @@ -0,0 +1,27 @@ +@echo off + +REM Store the current working directory +set "original_dir=%CD%" + +REM Change the current working directory to the script's location +cd /d "%~dp0" + +REM Call the script in the 'language_files' folder +echo Generating files using easy_localization +cd language_files +REM Allow execution permissions on CI +chmod +x generate_language_files.cmd +call generate_language_files.cmd %* + +REM Return to the main script directory +cd .. + +REM Call the script in the 'freezed' folder +echo Generating files using build_runner +cd freezed +REM Allow execution permissions on CI +chmod +x generate_freezed.cmd +call generate_freezed.cmd %* + +REM Return to the original directory +cd /d "%original_dir%" diff --git a/frontend/scripts/code_generation/generate.sh b/frontend/scripts/code_generation/generate.sh new file mode 100644 index 0000000000..8c3d051be0 --- /dev/null +++ b/frontend/scripts/code_generation/generate.sh @@ -0,0 +1,27 @@ +#!/bin/bash + +# Store the current working directory +original_dir=$(pwd) + +# Change the current working directory to the script's location +cd "$(dirname "$0")" + +# Call the script in the 'language_files' folder +echo "Generating files using easy_localization" +cd language_files +# Allow execution permissions on CI +chmod +x ./generate_language_files.sh +./generate_language_files.sh "$@" + +# Return to the main script directory +cd .. + +# Call the script in the 'freezed' folder +echo "Generating files using build_runner" +cd freezed +# Allow execution permissions on CI +chmod +x ./generate_freezed.sh +./generate_freezed.sh "$@" + +# Return to the original directory +cd "$original_dir" diff --git a/frontend/scripts/code_generation/language_files/generate_language_files.cmd b/frontend/scripts/code_generation/language_files/generate_language_files.cmd new file mode 100644 index 0000000000..984f4f365d --- /dev/null +++ b/frontend/scripts/code_generation/language_files/generate_language_files.cmd @@ -0,0 +1,26 @@ +@echo off + +echo 'Generating language files' + +REM Store the current working directory +set "original_dir=%CD%" + +REM Change the current working directory to the script's location +cd /d "%~dp0" + +cd ..\..\..\appflowy_flutter + +call flutter clean + +call flutter packages pub get + +echo Specifying source directory for AppFlowy Localizations. +call dart run easy_localization:generate -S assets/translations/ + +echo Generating language files for AppFlowy. +call dart run easy_localization:generate -f keys -o locale_keys.g.dart -S assets/translations/ -s en.json + +echo Done generating language files. + +REM Return to the original directory +cd /d "%original_dir%" diff --git a/frontend/scripts/code_generation/language_files/generate_language_files.sh b/frontend/scripts/code_generation/language_files/generate_language_files.sh new file mode 100644 index 0000000000..41e37eb268 --- /dev/null +++ b/frontend/scripts/code_generation/language_files/generate_language_files.sh @@ -0,0 +1,26 @@ +#!/bin/bash + +echo "Generating language files" + +# Store the current working directory +original_dir=$(pwd) + +cd "$(dirname "$0")" + +# Navigate to the project root +cd ../../../appflowy_flutter + +flutter clean + +flutter packages pub get + +echo "Specifying source directory for AppFlowy Localizations." +dart run easy_localization:generate -S assets/translations/ + +echo "Generating language files for AppFlowy." +dart run easy_localization:generate -f keys -o locale_keys.g.dart -S assets/translations/ -s en.json + +echo "Done generating language files." + +# Return to the original directory +cd "$original_dir" diff --git a/frontend/scripts/generate_language_files.cmd b/frontend/scripts/generate_language_files.cmd deleted file mode 100644 index a208ac7160..0000000000 --- a/frontend/scripts/generate_language_files.cmd +++ /dev/null @@ -1,5 +0,0 @@ -echo 'Generating language files' -cd appflowy_flutter - -call dart run easy_localization:generate -S assets/translations/ -call dart run easy_localization:generate -f keys -o locale_keys.g.dart -S assets/translations/ -s en.json \ No newline at end of file diff --git a/frontend/scripts/generate_language_files.sh b/frontend/scripts/generate_language_files.sh deleted file mode 100644 index 88baca3001..0000000000 --- a/frontend/scripts/generate_language_files.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/sh -#!/usr/bin/env fish -echo 'Generating language files' -cd appflowy_flutter -dart run easy_localization:generate -S assets/translations/ -dart run easy_localization:generate -f keys -o locale_keys.g.dart -S assets/translations -s en.json diff --git a/frontend/scripts/makefile/flutter.toml b/frontend/scripts/makefile/flutter.toml index a94859fa39..135fbaa66d 100644 --- a/frontend/scripts/makefile/flutter.toml +++ b/frontend/scripts/makefile/flutter.toml @@ -186,28 +186,20 @@ script_runner = "@duckscript" script_runner = "@shell" script = [ """ - cd appflowy_flutter - flutter clean - flutter pub get - flutter packages pub get - dart run easy_localization:generate -S assets/translations/ -f keys -o locale_keys.g.dart -S assets/translations -s en.json - dart run build_runner build -d + chmod +x scripts/code_generation/generate.sh """, + "scripts/code_generation/generate.sh" ] [tasks.code_generation.windows] script_runner = "@duckscript" script = [ """ - cd ./appflowy_flutter/ - exec cmd.exe /c flutter clean - exec cmd.exe /c flutter pub get - exec cmd.exe /c flutter packages pub get - exec cmd.exe /c dart run easy_localization:generate -S assets/translations/ -f keys -o locale_keys.g.dart -S assets/translations -s en.json - exec cmd.exe /c dart run build_runner build -d + exec "scripts/code_generation/generate.cmd" """, ] + [tasks.dry_code_generation] script_runner = "@shell" script = [