diff --git a/frontend/app_flowy/assets/translations/en.json b/frontend/app_flowy/assets/translations/en.json index cd7932945b..2381d5b288 100644 --- a/frontend/app_flowy/assets/translations/en.json +++ b/frontend/app_flowy/assets/translations/en.json @@ -199,7 +199,8 @@ "delete": "Delete", "textPlaceholder": "Empty", "copyProperty": "Copied property to clipboard", - "count": "Count" + "count": "Count", + "newRow": "New row" }, "selectOption": { "create": "Create", @@ -231,4 +232,4 @@ "create_new_card": "New" } } -} \ No newline at end of file +} diff --git a/frontend/app_flowy/assets/translations/fr-FR.json b/frontend/app_flowy/assets/translations/fr-FR.json index 2a3ad60b7b..2de9e63704 100644 --- a/frontend/app_flowy/assets/translations/fr-FR.json +++ b/frontend/app_flowy/assets/translations/fr-FR.json @@ -94,7 +94,20 @@ }, "tooltip": { "lightMode": "Passer en mode clair", - "darkMode": "Passer en mode sombre" + "darkMode": "Passer en mode sombre", + "openAsPage": "Ouvrir en tant que page", + "addNewRow": "Ajouter une ligne", + "openMenu": "Cliquer pour ouvrir le menu" + }, + "sideBar": { + "closeSidebar": "Fermer le menu latéral", + "openSidebar": "Ouvrir le menu latéral" + }, + "notifications": { + "export": { + "markdown": "Note exportée en Markdown", + "path": "Documents/flowy" + } }, "contactsPage": { "title": "Contacts", @@ -123,7 +136,7 @@ "failedMsg": "Assurez-vous d'avoir terminé le processus de connexion dans votre navigateur." }, "google": { - "title": "CONNEXION GOOGLE", + "title": "CONNEXION VIA GOOGLE", "instruction1": "Pour importer vos contacts Google, vous devez autoriser cette application à l'aide de votre navigateur web.", "instruction2": "Copiez ce code dans votre presse-papiers en cliquant sur l'icône ou en sélectionnant le texte:", "instruction3": "Accédez au lien suivant dans votre navigateur web et saisissez le code ci-dessus:", @@ -135,6 +148,7 @@ "menu": { "appearance": "Apparence", "language": "Langue", + "user": "Utilisateur", "open": "Ouvrir les paramètres" }, "appearance": { @@ -142,15 +156,12 @@ "darkLabel": "Mode sombre" } }, - "sideBar": { - "openSidebar": "Open sidebar", - "closeSidebar": "Close sidebar" - }, "grid": { "settings": { "filter": "Filtrer", - "sortBy": "Trier par", - "Properties": "Propriétés" + "sortBy": "Filtrer par", + "Properties": "Propriétés", + "group": "Groupe" }, "field": { "hide": "Cacher", @@ -179,13 +190,17 @@ "addSelectOption": "Ajouter une option", "optionTitle": "Options", "addOption": "Ajouter une option", - "editProperty": "Modifier la propriété" + "editProperty": "Modifier la propriété", + "newColumn": "Nouvelle colonne", + "deleteFieldPromptMessage": "Vous voulez supprimer cette propriété ?" }, "row": { "duplicate": "Dupliquer", "delete": "Supprimer", "textPlaceholder": "Vide", - "copyProperty": "Copie de la propriété dans le presse-papiers" + "copyProperty": "Copie de la propriété dans le presse-papiers", + "count": "Nombre", + "newRow": "Nouvelle ligne" }, "selectOption": { "create": "Créer", @@ -211,5 +226,10 @@ "timeHintTextInTwelveHour": "01:00 PM", "timeHintTextInTwentyFourHour": "13:00" } + }, + "board": { + "column": { + "create_new_card": "Nouveau" + } } -} \ No newline at end of file +} diff --git a/frontend/app_flowy/assets/translations/zh-CN.json b/frontend/app_flowy/assets/translations/zh-CN.json index f5fee6ab23..6271d138cd 100644 --- a/frontend/app_flowy/assets/translations/zh-CN.json +++ b/frontend/app_flowy/assets/translations/zh-CN.json @@ -94,7 +94,14 @@ }, "tooltip": { "lightMode": "切换到亮色模式", - "darkMode": "切换到暗色模式" + "darkMode": "切换到暗色模式", + "openAsPage": "作为页面打开", + "addNewRow": "增加一行", + "openMenu": "点击打开菜单" + }, + "sideBar": { + "openSidebar": "打开侧边栏", + "closeSidebar": "关闭侧边栏" }, "notifications": { "export": { @@ -149,15 +156,12 @@ "darkLabel": "夜间模式" } }, - "sideBar": { - "openSidebar": "打开侧边栏", - "closeSidebar": "关闭侧边栏" - }, "grid": { "settings": { "filter": "过滤器", "sortBy": "排序", - "Properties": "属性" + "Properties": "属性", + "group": "组" }, "field": { "hide": "隐藏", @@ -186,13 +190,17 @@ "addSelectOption": "添加一个标签", "optionTitle": "标签", "addOption": "添加标签", - "editProperty": "编辑列属性" + "editProperty": "编辑列属性", + "newColumn": "增加一列", + "deleteFieldPromptMessage": "确定要删除这个属性吗? " }, "row": { "duplicate": "复制", "delete": "删除", "textPlaceholder": "空", - "copyProperty": "复制列" + "copyProperty": "复制列", + "count": "数量", + "newRow": "添加一行" }, "selectOption": { "create": "新建", @@ -218,5 +226,11 @@ "timeHintTextInTwelveHour": "01:00 PM", "timeHintTextInTwentyFourHour": "13:00" } + }, + "board": { + "column": { + "create_new_card": "新建" + }, + "menuName": "看板" } } \ No newline at end of file diff --git a/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart b/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart index 673c159c8d..afb04e8f38 100644 --- a/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart +++ b/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart @@ -36,21 +36,21 @@ class BoardBloc extends Bloc { super(BoardState.initial(view.id)) { boardController = AppFlowyBoardController( onMoveGroup: ( - fromColumnId, + fromGroupId, fromIndex, - toColumnId, + toGroupId, toIndex, ) { - _moveGroup(fromColumnId, toColumnId); + _moveGroup(fromGroupId, toGroupId); }, onMoveGroupItem: ( - columnId, + groupId, fromIndex, toIndex, ) { - final fromRow = groupControllers[columnId]?.rowAtIndex(fromIndex); - final toRow = groupControllers[columnId]?.rowAtIndex(toIndex); - _moveRow(fromRow, columnId, toRow); + final fromRow = groupControllers[groupId]?.rowAtIndex(fromIndex); + final toRow = groupControllers[groupId]?.rowAtIndex(toIndex); + _moveRow(fromRow, groupId, toRow); }, onMoveGroupItemToGroup: ( fromGroupId, diff --git a/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart b/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart index b6d92e2781..dd5e619b46 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart @@ -69,7 +69,6 @@ class BoardContent extends StatefulWidget { class _BoardContentState extends State { late AppFlowyBoardScrollController scrollManager; - final Map cardKeysCache = {}; final config = AppFlowyBoardConfig( groupBackgroundColor: HexColor.fromHex('#F7F8FC'), @@ -104,8 +103,8 @@ class _BoardContentState extends State { Widget _buildBoard(BuildContext context) { return ChangeNotifierProvider.value( - value: Provider.of(context, listen: true), - child: Selector( + value: Provider.of(context, listen: true), + child: Selector( selector: (ctx, notifier) => notifier.theme, builder: (ctx, theme, child) => Expanded( child: AppFlowyBoard( @@ -139,7 +138,7 @@ class _BoardContentState extends State { .read() .add(BoardEvent.endEditRow(editingRow.row.id)); } else { - scrollManager.scrollToBottom(editingRow.columnId, () { + scrollManager.scrollToBottom(editingRow.columnId, (boardContext) { context .read() .add(BoardEvent.endEditRow(editingRow.row.id)); @@ -247,15 +246,8 @@ class _BoardContentState extends State { ); final groupItemId = columnItem.id + group.id; - ValueKey? key = cardKeysCache[groupItemId]; - if (key == null) { - final newKey = ValueKey(groupItemId); - cardKeysCache[groupItemId] = newKey; - key = newKey; - } - return AppFlowyGroupCard( - key: key, + key: ValueKey(groupItemId), margin: config.cardPadding, decoration: _makeBoxDecoration(context), child: BoardCard( @@ -331,8 +323,8 @@ class _ToolbarBlocAdaptor extends StatelessWidget { ); return ChangeNotifierProvider.value( - value: Provider.of(context, listen: true), - child: Selector( + value: Provider.of(context, listen: true), + child: Selector( selector: (ctx, notifier) => notifier.theme, builder: (ctx, theme, child) { return BoardToolbar(toolbarContext: toolbarContext); diff --git a/frontend/app_flowy/lib/plugins/doc/document.dart b/frontend/app_flowy/lib/plugins/doc/document.dart index 7403a01a91..d186513acc 100644 --- a/frontend/app_flowy/lib/plugins/doc/document.dart +++ b/frontend/app_flowy/lib/plugins/doc/document.dart @@ -131,8 +131,8 @@ class DocumentShareButton extends StatelessWidget { child: BlocBuilder( builder: (context, state) { return ChangeNotifierProvider.value( - value: Provider.of(context, listen: true), - child: Selector( + value: Provider.of(context, listen: true), + child: Selector( selector: (ctx, notifier) => notifier.locale, builder: (ctx, _, child) => ConstrainedBox( constraints: const BoxConstraints.expand( diff --git a/frontend/app_flowy/lib/plugins/doc/document_page.dart b/frontend/app_flowy/lib/plugins/doc/document_page.dart index 1d745982be..8593766087 100644 --- a/frontend/app_flowy/lib/plugins/doc/document_page.dart +++ b/frontend/app_flowy/lib/plugins/doc/document_page.dart @@ -134,7 +134,7 @@ class _DocumentPageState extends State { Widget _renderToolbar(quill.QuillController controller) { return ChangeNotifierProvider.value( - value: Provider.of(context, listen: true), + value: Provider.of(context, listen: true), child: EditorToolbar.basic( controller: controller, ), diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/date_cell/date_editor.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/date_cell/date_editor.dart index 63917da8f9..4018c0acb6 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/date_cell/date_editor.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/date_cell/date_editor.dart @@ -1,6 +1,8 @@ import 'package:app_flowy/generated/locale_keys.g.dart'; import 'package:app_flowy/plugins/grid/application/cell/date_cal_bloc.dart'; import 'package:app_flowy/plugins/grid/application/field/type_option/type_option_context.dart'; +import 'package:app_flowy/workspace/presentation/widgets/toggle/toggle.dart'; +import 'package:app_flowy/workspace/presentation/widgets/toggle/toggle_style.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:dartz/dartz.dart' show Either; import 'package:easy_localization/easy_localization.dart'; @@ -167,6 +169,18 @@ class _CellCalendarWidgetState extends State<_CellCalendarWidget> { rightChevronMargin: EdgeInsets.zero, rightChevronIcon: svgWidget("home/arrow_right"), ), + daysOfWeekStyle: DaysOfWeekStyle( + dowTextFormatter: (date, locale) => + DateFormat.E(locale).format(date).toUpperCase(), + weekdayStyle: TextStyle( + fontSize: 13, + color: theme.shader3, + ), + weekendStyle: TextStyle( + fontSize: 13, + color: theme.shader3, + ), + ), calendarStyle: CalendarStyle( selectedDecoration: BoxDecoration( color: theme.main1, @@ -230,11 +244,13 @@ class _IncludeTimeButton extends StatelessWidget { FlowyText.medium(LocaleKeys.grid_field_includeTime.tr(), fontSize: 14), const Spacer(), - Switch( + Toggle( value: includeTime, - onChanged: (newValue) => context + onChanged: (value) => context .read() - .add(DateCalEvent.setIncludeTime(newValue)), + .add(DateCalEvent.setIncludeTime(!value)), + style: ToggleStyle.big(theme), + padding: EdgeInsets.zero, ), ], ), @@ -350,7 +366,7 @@ class _DateTypeOptionButton extends StatelessWidget { offset: const Offset(20, 0), constraints: BoxConstraints.loose(const Size(140, 100)), child: FlowyButton( - text: FlowyText.medium(title, fontSize: 12), + text: FlowyText.medium(title, fontSize: 14), hoverColor: theme.hover, margin: kMargin, rightIcon: svgWidget("grid/more", color: theme.iconColor), diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/extension.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/extension.dart index 93b0699462..3f60de4142 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/extension.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/extension.dart @@ -130,14 +130,10 @@ class SelectOptionTagCell extends StatelessWidget { child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Flexible( - fit: FlexFit.loose, - flex: 2, - child: SelectOptionTag.fromOption( - context: context, - option: option, - onSelected: () => onSelected(option), - ), + SelectOptionTag.fromOption( + context: context, + option: option, + onSelected: () => onSelected(option), ), const Spacer(), ...children, diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/select_option_editor.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/select_option_editor.dart index bc80798fea..1b683713a3 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/select_option_editor.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/select_option_editor.dart @@ -248,32 +248,25 @@ class _SelectOptionCellState extends State<_SelectOptionCell> { mutex: widget.popoverMutex, child: SizedBox( height: GridSize.typeOptionItemHeight, - child: Row( + child: SelectOptionTagCell( + option: widget.option, + onSelected: (option) { + context + .read() + .add(SelectOptionEditorEvent.selectOption(option.id)); + }, children: [ - Flexible( - fit: FlexFit.loose, - child: SelectOptionTagCell( - option: widget.option, - onSelected: (option) { - context - .read() - .add(SelectOptionEditorEvent.selectOption(option.id)); - }, - children: [ - if (widget.isSelected) - Padding( - padding: const EdgeInsets.only(right: 6), - child: svgWidget("grid/checkmark"), - ), - ], + if (widget.isSelected) + Padding( + padding: const EdgeInsets.only(right: 6), + child: svgWidget("grid/checkmark"), ), - ), FlowyIconButton( width: 30, onPressed: () => _popoverController.show(), iconPadding: const EdgeInsets.fromLTRB(4, 4, 4, 4), icon: svgWidget("editor/details", color: theme.iconColor), - ) + ), ], ), ), diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/url_cell/url_cell.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/url_cell/url_cell.dart index 5ed184aafb..04d10f9aa2 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/url_cell/url_cell.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/url_cell/url_cell.dart @@ -97,7 +97,6 @@ class GridURLCell extends GridCellWidget { class _GridURLCellState extends GridCellState { final _popoverController = PopoverController(); - GridURLCellController? _cellContext; late URLCellBloc _cellBloc; @override @@ -132,6 +131,7 @@ class _GridURLCellState extends GridCellState { controller: _popoverController, constraints: BoxConstraints.loose(const Size(300, 160)), direction: PopoverDirection.bottomWithLeftAligned, + triggerActions: PopoverTriggerFlags.none, offset: const Offset(0, 20), child: SizedBox.expand( child: GestureDetector( @@ -144,7 +144,8 @@ class _GridURLCellState extends GridCellState { ), popupBuilder: (BuildContext popoverContext) { return URLEditorPopover( - cellController: _cellContext!, + cellController: widget.cellControllerBuilder.build() + as GridURLCellController, ); }, onClose: () { @@ -166,17 +167,13 @@ class _GridURLCellState extends GridCellState { final uri = Uri.parse(url); if (url.isNotEmpty && await canLaunchUrl(uri)) { await launchUrl(uri); - } else { - _cellContext = - widget.cellControllerBuilder.build() as GridURLCellController; - widget.onCellEditing.value = true; - _popoverController.show(); } } @override void requestBeginFocus() { - _openUrlOrEdit(_cellBloc.state.url); + widget.onCellEditing.value = true; + _popoverController.show(); } @override diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/footer/grid_footer.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/footer/grid_footer.dart index 27d6a59cd6..338ada57fe 100755 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/footer/grid_footer.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/footer/grid_footer.dart @@ -1,4 +1,6 @@ +import 'package:app_flowy/generated/locale_keys.g.dart'; import 'package:app_flowy/plugins/grid/application/grid_bloc.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/image.dart'; import 'package:flowy_infra/theme.dart'; import 'package:flowy_infra_ui/style_widget/button.dart'; @@ -13,7 +15,7 @@ class GridAddRowButton extends StatelessWidget { Widget build(BuildContext context) { final theme = context.watch(); return FlowyButton( - text: const FlowyText.medium('New row', fontSize: 12), + text: FlowyText.medium(LocaleKeys.grid_row_newRow.tr(), fontSize: 12), hoverColor: theme.shader6, onTap: () => context.read().add(const GridEvent.createRow()), leftIcon: svgWidget("home/add", color: theme.iconColor), diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/date.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/date.dart index 1cebba9911..954c933511 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/date.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/date.dart @@ -1,5 +1,7 @@ import 'package:app_flowy/plugins/grid/application/field/type_option/date_bloc.dart'; import 'package:app_flowy/plugins/grid/application/field/type_option/type_option_context.dart'; +import 'package:app_flowy/workspace/presentation/widgets/toggle/toggle.dart'; +import 'package:app_flowy/workspace/presentation/widgets/toggle/toggle_style.dart'; import 'package:easy_localization/easy_localization.dart' hide DateFormat; import 'package:app_flowy/generated/locale_keys.g.dart'; import 'package:flowy_infra/image.dart'; @@ -161,6 +163,7 @@ class _IncludeTimeButton extends StatelessWidget { @override Widget build(BuildContext context) { + final theme = context.watch(); return BlocSelector( selector: (state) => state.typeOption.includeTime, builder: (context, includeTime) { @@ -173,13 +176,15 @@ class _IncludeTimeButton extends StatelessWidget { FlowyText.medium(LocaleKeys.grid_field_includeTime.tr(), fontSize: 12), const Spacer(), - Switch( + Toggle( value: includeTime, - onChanged: (newValue) { + onChanged: (value) { context .read() - .add(DateTypeOptionEvent.includeTime(newValue)); + .add(DateTypeOptionEvent.includeTime(!value)); }, + style: ToggleStyle.big(theme), + padding: EdgeInsets.zero, ), ], ), diff --git a/frontend/app_flowy/lib/plugins/trash/menu.dart b/frontend/app_flowy/lib/plugins/trash/menu.dart index 15cfc398a1..73fff65ba3 100644 --- a/frontend/app_flowy/lib/plugins/trash/menu.dart +++ b/frontend/app_flowy/lib/plugins/trash/menu.dart @@ -33,8 +33,8 @@ class MenuTrash extends StatelessWidget { Widget _render(BuildContext context) { return Row(children: [ ChangeNotifierProvider.value( - value: Provider.of(context, listen: true), - child: Selector( + value: Provider.of(context, listen: true), + child: Selector( selector: (ctx, notifier) => notifier.theme, builder: (ctx, theme, child) => SizedBox( width: 16, @@ -44,8 +44,8 @@ class MenuTrash extends StatelessWidget { ), const HSpace(6), ChangeNotifierProvider.value( - value: Provider.of(context, listen: true), - child: Selector( + value: Provider.of(context, listen: true), + child: Selector( selector: (ctx, notifier) => notifier.locale, builder: (ctx, _, child) => FlowyText.medium(LocaleKeys.trash_text.tr(), fontSize: 12), diff --git a/frontend/app_flowy/lib/startup/tasks/app_widget.dart b/frontend/app_flowy/lib/startup/tasks/app_widget.dart index 9ca28e2f36..7ff53efeff 100644 --- a/frontend/app_flowy/lib/startup/tasks/app_widget.dart +++ b/frontend/app_flowy/lib/startup/tasks/app_widget.dart @@ -17,8 +17,8 @@ class InitAppWidgetTask extends LaunchTask { @override Future initialize(LaunchContext context) async { final widget = context.getIt().create(); - final setting = await UserSettingsService().getAppearanceSettings(); - final settingModel = AppearanceSettingModel(setting); + final setting = await SettingsFFIService().getAppearanceSetting(); + final settingModel = AppearanceSetting(setting); final app = ApplicationWidget( settingModel: settingModel, child: widget, @@ -58,7 +58,7 @@ class InitAppWidgetTask extends LaunchTask { class ApplicationWidget extends StatelessWidget { final Widget child; - final AppearanceSettingModel settingModel; + final AppearanceSetting settingModel; const ApplicationWidget({ Key? key, @@ -75,10 +75,10 @@ class ApplicationWidget extends StatelessWidget { const minWidth = 600.0; setWindowMinSize(const Size(minWidth, minWidth / ratio)); settingModel.readLocaleWhenAppLaunch(context); - AppTheme theme = context.select( + AppTheme theme = context.select( (value) => value.theme, ); - Locale locale = context.select( + Locale locale = context.select( (value) => value.locale, ); diff --git a/frontend/app_flowy/lib/user/application/user_settings_service.dart b/frontend/app_flowy/lib/user/application/user_settings_service.dart index 28309d202c..b420af505c 100644 --- a/frontend/app_flowy/lib/user/application/user_settings_service.dart +++ b/frontend/app_flowy/lib/user/application/user_settings_service.dart @@ -4,8 +4,8 @@ import 'package:flowy_sdk/flowy_sdk.dart'; import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-user/user_setting.pb.dart'; -class UserSettingsService { - Future getAppearanceSettings() async { +class SettingsFFIService { + Future getAppearanceSetting() async { final result = await UserEventGetAppearanceSetting().send(); return result.fold( @@ -18,7 +18,8 @@ class UserSettingsService { ); } - Future> setAppearanceSettings(AppearanceSettingsPB settings) { - return UserEventSetAppearanceSetting(settings).send(); + Future> setAppearanceSetting( + AppearanceSettingsPB setting) { + return UserEventSetAppearanceSetting(setting).send(); } } diff --git a/frontend/app_flowy/lib/workspace/application/appearance.dart b/frontend/app_flowy/lib/workspace/application/appearance.dart index a9b498fb00..1d3a13c7af 100644 --- a/frontend/app_flowy/lib/workspace/application/appearance.dart +++ b/frontend/app_flowy/lib/workspace/application/appearance.dart @@ -8,72 +8,114 @@ import 'package:flowy_sdk/protobuf/flowy-user/user_setting.pb.dart'; import 'package:flutter/material.dart'; import 'package:easy_localization/easy_localization.dart'; -class AppearanceSettingModel extends ChangeNotifier with EquatableMixin { - AppearanceSettingsPB setting; +/// [AppearanceSetting] is used to modify the appear setting of AppFlowy application. Including the [Locale], [AppTheme], etc. +class AppearanceSetting extends ChangeNotifier with EquatableMixin { + final AppearanceSettingsPB _setting; AppTheme _theme; Locale _locale; - Timer? _saveOperation; + Timer? _debounceSaveOperation; - AppearanceSettingModel(this.setting) - : _theme = AppTheme.fromName(name: setting.theme), - _locale = - Locale(setting.locale.languageCode, setting.locale.countryCode); + AppearanceSetting(AppearanceSettingsPB setting) + : _setting = setting, + _theme = AppTheme.fromName(name: setting.theme), + _locale = Locale( + setting.locale.languageCode, + setting.locale.countryCode, + ); + /// Returns the current [AppTheme] AppTheme get theme => _theme; + + /// Returns the current [Locale] Locale get locale => _locale; - Future save() async { - _saveOperation?.cancel(); - _saveOperation = Timer(const Duration(seconds: 2), () async { - await UserSettingsService().setAppearanceSettings(setting); - }); - } - - @override - List get props { - return [setting.hashCode]; - } - - void swapTheme() { - final themeType = - (_theme.ty == ThemeType.light ? ThemeType.dark : ThemeType.light); - - if (_theme.ty != themeType) { - _theme = AppTheme.fromType(themeType); - setting.theme = themeTypeToString(themeType); - notifyListeners(); - save(); + /// Updates the current theme and notify the listeners the theme was changed. + /// Do nothing if the passed in themeType equal to the current theme type. + /// + void setTheme(ThemeType themeType) { + if (_theme.ty == themeType) { + return; } + + _theme = AppTheme.fromType(themeType); + _setting.theme = themeTypeToString(themeType); + _saveAppearSetting(); + + notifyListeners(); } + /// Updates the current locale and notify the listeners the locale was changed + /// Fallback to [en] locale If the newLocale is not supported. + /// void setLocale(BuildContext context, Locale newLocale) { if (!context.supportedLocales.contains(newLocale)) { - Log.warn("Unsupported locale: $newLocale"); + Log.warn("Unsupported locale: $newLocale, Fallback to locale: en"); newLocale = const Locale('en'); - Log.debug("Fallback to locale: $newLocale"); } context.setLocale(newLocale); if (_locale != newLocale) { _locale = newLocale; - setting.locale.languageCode = _locale.languageCode; - setting.locale.countryCode = _locale.countryCode ?? ""; + _setting.locale.languageCode = _locale.languageCode; + _setting.locale.countryCode = _locale.countryCode ?? ""; + _saveAppearSetting(); + notifyListeners(); - save(); } } - void readLocaleWhenAppLaunch(BuildContext context) { - if (setting.resetAsDefault) { - setting.resetAsDefault = false; - save(); + /// Saves key/value setting to disk. + /// Removes the key if the passed in value is null + void setKeyValue(String key, String? value) { + if (key.isEmpty) { + Log.warn("The key should not be empty"); + return; + } + if (value == null) { + _setting.settingKeyValue.remove(key); + } + + if (_setting.settingKeyValue[key] != value) { + if (value == null) { + _setting.settingKeyValue.remove(key); + } else { + _setting.settingKeyValue[key] = value; + } + + _saveAppearSetting(); + notifyListeners(); + } + } + + /// Called when the application launch. + /// Uses the device locale when open the application for the first time + void readLocaleWhenAppLaunch(BuildContext context) { + if (_setting.resetToDefault) { + _setting.resetToDefault = false; + _saveAppearSetting(); setLocale(context, context.deviceLocale); return; } - // when opening app the first time setLocale(context, _locale); } + + Future _saveAppearSetting() async { + _debounceSaveOperation?.cancel(); + _debounceSaveOperation = Timer( + const Duration(seconds: 1), + () { + SettingsFFIService().setAppearanceSetting(_setting).then((result) { + result.fold((l) => null, (error) => Log.error(error)); + }); + }, + ); + } + + @override + List get props { + return [_setting.hashCode]; + } } diff --git a/frontend/app_flowy/lib/workspace/presentation/home/menu/app/header/header.dart b/frontend/app_flowy/lib/workspace/presentation/home/menu/app/header/header.dart index 727b18762b..5724262aa5 100644 --- a/frontend/app_flowy/lib/workspace/presentation/home/menu/app/header/header.dart +++ b/frontend/app_flowy/lib/workspace/presentation/home/menu/app/header/header.dart @@ -99,6 +99,7 @@ class MenuAppHeader extends StatelessWidget { app.name, fontSize: 12, color: theme.textColor, + overflow: TextOverflow.ellipsis, ), ), ), diff --git a/frontend/app_flowy/lib/workspace/presentation/home/menu/app/menu_app.dart b/frontend/app_flowy/lib/workspace/presentation/home/menu/app/menu_app.dart index 7c2e2e4d87..c7ee59d03e 100644 --- a/frontend/app_flowy/lib/workspace/presentation/home/menu/app/menu_app.dart +++ b/frontend/app_flowy/lib/workspace/presentation/home/menu/app/menu_app.dart @@ -89,8 +89,7 @@ class _MenuAppState extends State { hasIcon: false, ), header: ChangeNotifierProvider.value( - value: - Provider.of(context, listen: true), + value: Provider.of(context, listen: true), child: MenuAppHeader(widget.app), ), expanded: ViewSection(appViewData: viewDataContext), diff --git a/frontend/app_flowy/lib/workspace/presentation/home/menu/app/section/item.dart b/frontend/app_flowy/lib/workspace/presentation/home/menu/app/section/item.dart index 083f7b55db..195c82adcd 100644 --- a/frontend/app_flowy/lib/workspace/presentation/home/menu/app/section/item.dart +++ b/frontend/app_flowy/lib/workspace/presentation/home/menu/app/section/item.dart @@ -82,7 +82,7 @@ class ViewSectionItem extends StatelessWidget { child: FlowyText.regular( state.view.name, fontSize: 12, - overflow: TextOverflow.clip, + overflow: TextOverflow.ellipsis, ), ), ]; diff --git a/frontend/app_flowy/lib/workspace/presentation/home/menu/menu_user.dart b/frontend/app_flowy/lib/workspace/presentation/home/menu/menu_user.dart index 399bbd1f89..12a3114f42 100644 --- a/frontend/app_flowy/lib/workspace/presentation/home/menu/menu_user.dart +++ b/frontend/app_flowy/lib/workspace/presentation/home/menu/menu_user.dart @@ -28,8 +28,9 @@ class MenuUser extends StatelessWidget { children: [ _renderAvatar(context), const HSpace(10), - _renderUserName(context), - const Spacer(), + Expanded( + child: _renderUserName(context), + ), _renderSettingsButton(context), //ToDo: when the user is allowed to create another workspace, //we get the below block back @@ -63,7 +64,7 @@ class MenuUser extends StatelessWidget { if (name.isEmpty) { name = context.read().state.userProfile.email; } - return FlowyText(name, fontSize: 12); + return FlowyText(name, fontSize: 12, overflow: TextOverflow.ellipsis); } Widget _renderSettingsButton(BuildContext context) { diff --git a/frontend/app_flowy/lib/workspace/presentation/settings/settings_dialog.dart b/frontend/app_flowy/lib/workspace/presentation/settings/settings_dialog.dart index 6de69029a5..42a51b7da1 100644 --- a/frontend/app_flowy/lib/workspace/presentation/settings/settings_dialog.dart +++ b/frontend/app_flowy/lib/workspace/presentation/settings/settings_dialog.dart @@ -33,8 +33,7 @@ class SettingsDialog extends StatelessWidget { ..add(const SettingsDialogEvent.initial()), child: BlocBuilder( builder: (context, state) => ChangeNotifierProvider.value( - value: Provider.of(context, - listen: true), + value: Provider.of(context, listen: true), child: FlowyDialog( title: Text( LocaleKeys.settings_title.tr(), diff --git a/frontend/app_flowy/lib/workspace/presentation/settings/widgets/settings_appearance_view.dart b/frontend/app_flowy/lib/workspace/presentation/settings/widgets/settings_appearance_view.dart index 85b78ae3b0..54077135b0 100644 --- a/frontend/app_flowy/lib/workspace/presentation/settings/widgets/settings_appearance_view.dart +++ b/frontend/app_flowy/lib/workspace/presentation/settings/widgets/settings_appearance_view.dart @@ -30,9 +30,7 @@ class SettingsAppearanceView extends StatelessWidget { ), Toggle( value: theme.isDark, - onChanged: (val) { - context.read().swapTheme(); - }, + onChanged: (_) => setTheme(context), style: ToggleStyle.big(theme), ), Text( @@ -48,4 +46,13 @@ class SettingsAppearanceView extends StatelessWidget { ), ); } + + void setTheme(BuildContext context) { + final theme = context.read(); + if (theme.isDark) { + context.read().setTheme(ThemeType.light); + } else { + context.read().setTheme(ThemeType.dark); + } + } } diff --git a/frontend/app_flowy/lib/workspace/presentation/settings/widgets/settings_language_view.dart b/frontend/app_flowy/lib/workspace/presentation/settings/widgets/settings_language_view.dart index bb1b419da0..457d3b2f59 100644 --- a/frontend/app_flowy/lib/workspace/presentation/settings/widgets/settings_language_view.dart +++ b/frontend/app_flowy/lib/workspace/presentation/settings/widgets/settings_language_view.dart @@ -13,7 +13,7 @@ class SettingsLanguageView extends StatelessWidget { Widget build(BuildContext context) { context.watch(); return ChangeNotifierProvider.value( - value: Provider.of(context, listen: true), + value: Provider.of(context, listen: true), child: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -43,7 +43,8 @@ class LanguageSelectorDropdown extends StatefulWidget { }) : super(key: key); @override - State createState() => _LanguageSelectorDropdownState(); + State createState() => + _LanguageSelectorDropdownState(); } class _LanguageSelectorDropdownState extends State { @@ -77,10 +78,10 @@ class _LanguageSelectorDropdownState extends State { ), child: DropdownButtonHideUnderline( child: DropdownButton( - value: context.read().locale, + value: context.read().locale, onChanged: (val) { setState(() { - context.read().setLocale(context, val!); + context.read().setLocale(context, val!); }); }, icon: const Visibility( diff --git a/frontend/app_flowy/packages/appflowy_board/CHANGELOG.md b/frontend/app_flowy/packages/appflowy_board/CHANGELOG.md index 56115c698b..2a29982421 100644 --- a/frontend/app_flowy/packages/appflowy_board/CHANGELOG.md +++ b/frontend/app_flowy/packages/appflowy_board/CHANGELOG.md @@ -1,3 +1,5 @@ +# 0.0.8 +* Enable drag and drop group # 0.0.7 * Rename some classes * Add documentation @@ -7,7 +9,7 @@ # 0.0.5 * Optimize insert card animation -* Enable insert card at the end of the column +* Enable insert card at the end of the group * Fix some bugs # 0.0.4 @@ -24,6 +26,5 @@ # 0.0.1 -* Support drag and drop column -* Support drag and drop column items from one to another +* Support drag and drop group items from one to another diff --git a/frontend/app_flowy/packages/appflowy_board/example/lib/main.dart b/frontend/app_flowy/packages/appflowy_board/example/lib/main.dart index c881370e03..9e42611e7e 100644 --- a/frontend/app_flowy/packages/appflowy_board/example/lib/main.dart +++ b/frontend/app_flowy/packages/appflowy_board/example/lib/main.dart @@ -34,7 +34,7 @@ class _MyAppState extends State { appBar: AppBar( title: const Text('AppFlowy Board'), ), - body: _examples[_currentIndex], + body: Container(color: Colors.white, child: _examples[_currentIndex]), bottomNavigationBar: BottomNavigationBar( fixedColor: _bottomNavigationColor, showSelectedLabels: true, diff --git a/frontend/app_flowy/packages/appflowy_board/example/lib/multi_board_list_example.dart b/frontend/app_flowy/packages/appflowy_board/example/lib/multi_board_list_example.dart index 8bd7ae98e3..2a256a51c4 100644 --- a/frontend/app_flowy/packages/appflowy_board/example/lib/multi_board_list_example.dart +++ b/frontend/app_flowy/packages/appflowy_board/example/lib/multi_board_list_example.dart @@ -21,8 +21,11 @@ class _MultiBoardListExampleState extends State { }, ); + late AppFlowyBoardScrollController boardController; + @override void initState() { + boardController = AppFlowyBoardScrollController(); final group1 = AppFlowyGroupData(id: "To Do", name: "To Do", items: [ TextItem("Card 1"), TextItem("Card 2"), @@ -67,12 +70,16 @@ class _MultiBoardListExampleState extends State { child: _buildCard(groupItem), ); }, + boardScrollController: boardController, footerBuilder: (context, columnData) { return AppFlowyGroupFooter( icon: const Icon(Icons.add, size: 20), title: const Text('New'), height: 50, margin: config.groupItemPadding, + onAddButtonClick: () { + boardController.scrollToBottom(columnData.id, (p0) {}); + }, ); }, headerBuilder: (context, columnData) { diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board.dart index 4a71595ea6..8aaf94e6d6 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board.dart @@ -13,10 +13,8 @@ import '../rendering/board_overlay.dart'; class AppFlowyBoardScrollController { AppFlowyBoardState? _groupState; - void scrollToBottom(String groupId, VoidCallback? completed) { - _groupState - ?.getReorderFlexState(groupId: groupId) - ?.scrollToBottom(completed); + void scrollToBottom(String groupId, void Function(BuildContext)? completed) { + _groupState?.reorderFlexActionMap[groupId]?.scrollToBottom(completed); } } @@ -133,7 +131,7 @@ class AppFlowyBoard extends StatelessWidget { dataController: controller, scrollController: scrollController, scrollManager: boardScrollController, - columnsState: _groupState, + groupState: _groupState, background: background, delegate: _phantomController, groupConstraints: groupConstraints, @@ -158,7 +156,7 @@ class _AppFlowyBoardContent extends StatefulWidget { final ReorderFlexConfig reorderFlexConfig; final BoxConstraints groupConstraints; final AppFlowyBoardScrollController? scrollManager; - final AppFlowyBoardState columnsState; + final AppFlowyBoardState groupState; final AppFlowyBoardCardBuilder cardBuilder; final AppFlowyBoardHeaderBuilder? headerBuilder; final AppFlowyBoardFooterBuilder? footerBuilder; @@ -171,7 +169,7 @@ class _AppFlowyBoardContent extends StatefulWidget { required this.delegate, required this.dataController, required this.scrollManager, - required this.columnsState, + required this.groupState, this.scrollController, this.background, required this.groupConstraints, @@ -192,8 +190,6 @@ class _AppFlowyBoardContentState extends State<_AppFlowyBoardContent> { GlobalKey(debugLabel: '$_AppFlowyBoardContent overlay key'); late BoardOverlayEntry _overlayEntry; - final Map _reorderFlexKeys = {}; - @override void initState() { _overlayEntry = BoardOverlayEntry( @@ -202,7 +198,7 @@ class _AppFlowyBoardContentState extends State<_AppFlowyBoardContent> { reorderFlexId: widget.dataController.identifier, acceptedReorderFlexId: widget.dataController.groupIds, delegate: widget.delegate, - columnsState: widget.columnsState, + columnsState: widget.groupState, ); final reorderFlex = ReorderFlex( @@ -212,7 +208,7 @@ class _AppFlowyBoardContentState extends State<_AppFlowyBoardContent> { dataSource: widget.dataController, direction: Axis.horizontal, interceptor: interceptor, - reorderable: false, + reorderable: true, children: _buildColumns(), ); @@ -257,18 +253,16 @@ class _AppFlowyBoardContentState extends State<_AppFlowyBoardContent> { dataController: widget.dataController, ); - if (_reorderFlexKeys[columnData.id] == null) { - _reorderFlexKeys[columnData.id] = GlobalObjectKey(columnData.id); - } + final reorderFlexAction = ReorderFlexActionImpl(); + widget.groupState.reorderFlexActionMap[columnData.id] = + reorderFlexAction; - GlobalObjectKey reorderFlexKey = _reorderFlexKeys[columnData.id]!; return ChangeNotifierProvider.value( key: ValueKey(columnData.id), value: widget.dataController.getGroupController(columnData.id), child: Consumer( builder: (context, value, child) { final boardColumn = AppFlowyBoardGroup( - reorderFlexKey: reorderFlexKey, // key: PageStorageKey(columnData.id), margin: _marginFromIndex(columnIndex), itemMargin: widget.config.groupItemPadding, @@ -281,11 +275,11 @@ class _AppFlowyBoardContentState extends State<_AppFlowyBoardContent> { onReorder: widget.dataController.moveGroupItem, cornerRadius: widget.config.cornerRadius, backgroundColor: widget.config.groupBackgroundColor, - dragStateStorage: widget.columnsState, - dragTargetIndexKeyStorage: widget.columnsState, + dragStateStorage: widget.groupState, + dragTargetKeys: widget.groupState, + reorderFlexAction: reorderFlexAction, ); - widget.columnsState.addGroup(columnData.id, boardColumn); return ConstrainedBox( constraints: widget.groupConstraints, child: boardColumn, @@ -356,71 +350,61 @@ class AppFlowyGroupContext { } class AppFlowyBoardState extends DraggingStateStorage - with ReorderDragTargetIndexKeyStorage { + with ReorderDragTargeKeys { + final Map groupDragStates = {}; + final Map> groupDragTargetKeys = {}; + /// Quick access to the [AppFlowyBoardGroup], the [GlobalKey] is bind to the /// AppFlowyBoardGroup's [ReorderFlex] widget. - final Map groupReorderFlexKeys = {}; - final Map groupDragStates = {}; - final Map> groupDragDragTargets = {}; - - void addGroup(String groupId, AppFlowyBoardGroup groupWidget) { - groupReorderFlexKeys[groupId] = groupWidget.reorderFlexKey; - } - - ReorderFlexState? getReorderFlexState({required String groupId}) { - final flexGlobalKey = groupReorderFlexKeys[groupId]; - if (flexGlobalKey == null) return null; - if (flexGlobalKey.currentState is! ReorderFlexState) return null; - final state = flexGlobalKey.currentState as ReorderFlexState; - return state; - } - - ReorderFlex? getReorderFlex({required String groupId}) { - final flexGlobalKey = groupReorderFlexKeys[groupId]; - if (flexGlobalKey == null) return null; - if (flexGlobalKey.currentWidget is! ReorderFlex) return null; - final widget = flexGlobalKey.currentWidget as ReorderFlex; - return widget; - } + final Map reorderFlexActionMap = {}; @override - DraggingState? read(String reorderFlexId) { + DraggingState? readState(String reorderFlexId) { return groupDragStates[reorderFlexId]; } @override - void write(String reorderFlexId, DraggingState state) { + void insertState(String reorderFlexId, DraggingState state) { Log.trace('$reorderFlexId Write dragging state: $state'); groupDragStates[reorderFlexId] = state; } @override - void remove(String reorderFlexId) { + void removeState(String reorderFlexId) { groupDragStates.remove(reorderFlexId); } @override - void addKey( + void insertDragTarget( String reorderFlexId, String key, GlobalObjectKey> value, ) { - Map? group = groupDragDragTargets[reorderFlexId]; + Map? group = groupDragTargetKeys[reorderFlexId]; if (group == null) { group = {}; - groupDragDragTargets[reorderFlexId] = group; + groupDragTargetKeys[reorderFlexId] = group; } group[key] = value; } @override - GlobalObjectKey>? readKey( - String reorderFlexId, String key) { - Map? group = groupDragDragTargets[reorderFlexId]; + GlobalObjectKey>? getDragTarget( + String reorderFlexId, + String key, + ) { + Map? group = groupDragTargetKeys[reorderFlexId]; if (group != null) { return group[key]; } else { return null; } } + + @override + void removeDragTarget(String reorderFlexId) { + groupDragTargetKeys.remove(reorderFlexId); + } } + +class ReorderFlexActionImpl extends ReorderFlexAction {} diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_group/group.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_group/group.dart index 8c27def35d..880a81f666 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_group/group.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_group/group.dart @@ -90,21 +90,21 @@ class AppFlowyBoardGroup extends StatefulWidget { final DraggingStateStorage? dragStateStorage; - final ReorderDragTargetIndexKeyStorage? dragTargetIndexKeyStorage; + final ReorderDragTargeKeys? dragTargetKeys; - final GlobalObjectKey reorderFlexKey; + final ReorderFlexAction? reorderFlexAction; const AppFlowyBoardGroup({ Key? key, - required this.reorderFlexKey, this.headerBuilder, this.footerBuilder, required this.cardBuilder, required this.onReorder, required this.dataSource, required this.phantomController, + this.reorderFlexAction, this.dragStateStorage, - this.dragTargetIndexKeyStorage, + this.dragTargetKeys, this.scrollController, this.onDragStarted, this.onDragEnded, @@ -146,9 +146,9 @@ class _AppFlowyBoardGroupState extends State { ); Widget reorderFlex = ReorderFlex( - key: widget.reorderFlexKey, + key: ValueKey(widget.groupId), dragStateStorage: widget.dragStateStorage, - dragTargetIndexKeyStorage: widget.dragTargetIndexKeyStorage, + dragTargetKeys: widget.dragTargetKeys, scrollController: widget.scrollController, config: widget.config, onDragStarted: (index) { @@ -168,6 +168,7 @@ class _AppFlowyBoardGroupState extends State { }, dataSource: widget.dataSource, interceptor: interceptor, + reorderFlexAction: widget.reorderFlexAction, children: children, ); diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_group/group_data.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_group/group_data.dart index 0dba483715..5223e10e90 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_group/group_data.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_group/group_data.dart @@ -41,7 +41,7 @@ class AppFlowyGroupController extends ChangeNotifier with EquatableMixin { void updateGroupName(String newName) { if (groupData.headerData.groupName != newName) { groupData.headerData.groupName = newName; - notifyListeners(); + _notify(); } } @@ -56,7 +56,7 @@ class AppFlowyGroupController extends ChangeNotifier with EquatableMixin { Log.debug('[$AppFlowyGroupController] $groupData remove item at $index'); final item = groupData._items.removeAt(index); if (notify) { - notifyListeners(); + _notify(); } return item; } @@ -81,7 +81,7 @@ class AppFlowyGroupController extends ChangeNotifier with EquatableMixin { '[$AppFlowyGroupController] $groupData move item from $fromIndex to $toIndex'); final item = groupData._items.removeAt(fromIndex); groupData._items.insert(toIndex, item); - notifyListeners(); + _notify(); return true; } @@ -102,7 +102,7 @@ class AppFlowyGroupController extends ChangeNotifier with EquatableMixin { groupData._items.add(item); } - if (notify) notifyListeners(); + if (notify) _notify(); return true; } } @@ -112,7 +112,7 @@ class AppFlowyGroupController extends ChangeNotifier with EquatableMixin { return false; } else { groupData._items.add(item); - if (notify) notifyListeners(); + if (notify) _notify(); return true; } } @@ -135,7 +135,7 @@ class AppFlowyGroupController extends ChangeNotifier with EquatableMixin { '[$AppFlowyGroupController] $groupData replace $removedItem with $newItem at $index'); } - notifyListeners(); + _notify(); } void replaceOrInsertItem(AppFlowyGroupItem newItem) { @@ -143,10 +143,10 @@ class AppFlowyGroupController extends ChangeNotifier with EquatableMixin { if (index != -1) { groupData._items.removeAt(index); groupData._items.insert(index, newItem); - notifyListeners(); + _notify(); } else { groupData._items.add(newItem); - notifyListeners(); + _notify(); } } @@ -154,6 +154,10 @@ class AppFlowyGroupController extends ChangeNotifier with EquatableMixin { return groupData._items.indexWhere((element) => element.id == item.id) != -1; } + + void _notify() { + notifyListeners(); + } } /// [AppFlowyGroupData] represents the data of each group of the Board. diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_state.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_state.dart index 9d528bf9f1..8e1a61be1a 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_state.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_state.dart @@ -69,9 +69,9 @@ class FlexDragTargetData extends DragTargetData { } abstract class DraggingStateStorage { - void write(String reorderFlexId, DraggingState state); - void remove(String reorderFlexId); - DraggingState? read(String reorderFlexId); + void insertState(String reorderFlexId, DraggingState state); + void removeState(String reorderFlexId); + DraggingState? readState(String reorderFlexId); } class DraggingState { diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_target.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_target.dart index 4199e07ec8..fde9c3470a 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_target.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_target.dart @@ -73,11 +73,15 @@ class ReorderDragTarget extends StatefulWidget { final ReorderFlexDraggableTargetBuilder? draggableTargetBuilder; final AnimationController insertAnimationController; + final AnimationController deleteAnimationController; final bool useMoveAnimation; + final bool draggable; + final double draggingOpacity; + const ReorderDragTarget({ Key? key, required this.child, @@ -94,6 +98,7 @@ class ReorderDragTarget extends StatefulWidget { this.onAccept, this.onLeave, this.draggableTargetBuilder, + this.draggingOpacity = 0.3, }) : super(key: key); @override @@ -164,6 +169,7 @@ class _ReorderDragTargetState feedback: feedbackBuilder, childWhenDragging: IgnorePointerWidget( useIntrinsicSize: !widget.useMoveAnimation, + opacity: widget.draggingOpacity, child: widget.child, ), onDragStarted: () { @@ -195,7 +201,10 @@ class _ReorderDragTargetState } Widget _buildDraggableFeedback( - BuildContext context, BoxConstraints constraints, Widget child) { + BuildContext context, + BoxConstraints constraints, + Widget child, + ) { return Transform( transform: Matrix4.rotationZ(0), alignment: FractionalOffset.topLeft, @@ -205,7 +214,7 @@ class _ReorderDragTargetState clipBehavior: Clip.hardEdge, child: ConstrainedBox( constraints: constraints, - child: Opacity(opacity: 0.3, child: child), + child: Opacity(opacity: widget.draggingOpacity, child: child), ), ), ); @@ -274,8 +283,11 @@ class DragTargetAnimation { class IgnorePointerWidget extends StatelessWidget { final Widget? child; final bool useIntrinsicSize; + final double opacity; + const IgnorePointerWidget({ required this.child, + required this.opacity, this.useIntrinsicSize = false, Key? key, }) : super(key: key); @@ -286,11 +298,10 @@ class IgnorePointerWidget extends StatelessWidget { ? child : SizedBox(width: 0.0, height: 0.0, child: child); - final opacity = useIntrinsicSize ? 0.3 : 0.0; return IgnorePointer( ignoring: true, child: Opacity( - opacity: opacity, + opacity: useIntrinsicSize ? opacity : 0.0, child: sizedChild, ), ); @@ -300,8 +311,10 @@ class IgnorePointerWidget extends StatelessWidget { class AbsorbPointerWidget extends StatelessWidget { final Widget? child; final bool useIntrinsicSize; + final double opacity; const AbsorbPointerWidget({ required this.child, + required this.opacity, this.useIntrinsicSize = false, Key? key, }) : super(key: key); @@ -312,10 +325,9 @@ class AbsorbPointerWidget extends StatelessWidget { ? child : SizedBox(width: 0.0, height: 0.0, child: child); - final opacity = useIntrinsicSize ? 0.3 : 0.0; return AbsorbPointer( child: Opacity( - opacity: opacity, + opacity: useIntrinsicSize ? opacity : 0.0, child: sizedChild, ), ); @@ -494,6 +506,7 @@ class _FakeDragTargetState sizeFactor: widget.deleteAnimationController, axis: Axis.vertical, child: AbsorbPointerWidget( + opacity: 0.3, child: widget.child, ), ); @@ -503,6 +516,7 @@ class _FakeDragTargetState axis: Axis.vertical, child: AbsorbPointerWidget( useIntrinsicSize: true, + opacity: 0.3, child: widget.child, ), ); diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_target_interceptor.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_target_interceptor.dart index ecf91d6934..21a67a3c7d 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_target_interceptor.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_target_interceptor.dart @@ -81,7 +81,7 @@ class OverlappingDragTargetInterceptor extends DragTargetInterceptor { delegate.cancel(); } else { // Ignore the event if the dragTarget overlaps with the other column's dragTargets. - final columnKeys = columnsState.groupDragDragTargets[dragTargetId]; + final columnKeys = columnsState.groupDragTargetKeys[dragTargetId]; if (columnKeys != null) { final keys = columnKeys.values.toList(); if (dragTargetData.isOverlapWithWidgets(keys)) { @@ -102,8 +102,7 @@ class OverlappingDragTargetInterceptor extends DragTargetInterceptor { delegate.dragTargetDidMoveToReorderFlex( dragTargetId, dragTargetData, index); - columnsState - .getReorderFlexState(groupId: dragTargetId) + columnsState.reorderFlexActionMap[dragTargetId] ?.resetDragTargetIndex(index); } }); diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/reorder_flex.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/reorder_flex.dart index 1317ad9ab8..69d763804d 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/reorder_flex.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/reorder_flex.dart @@ -31,9 +31,32 @@ abstract class ReoderFlexItem { String get id; } -abstract class ReorderDragTargetIndexKeyStorage { - void addKey(String reorderFlexId, String key, GlobalObjectKey value); - GlobalObjectKey? readKey(String reorderFlexId, String key); +/// Cache each dragTarget's key. +/// For the moment, the key is used to locate the render object that will +/// be passed in the [ScrollPosition]'s [ensureVisible] function. +/// +abstract class ReorderDragTargeKeys { + void insertDragTarget( + String reorderFlexId, + String key, + GlobalObjectKey value, + ); + + GlobalObjectKey? getDragTarget( + String reorderFlexId, + String key, + ); + + void removeDragTarget(String reorderFlexId); +} + +abstract class ReorderFlexAction { + void Function(void Function(BuildContext)?)? _scrollToBottom; + void Function(void Function(BuildContext)?) get scrollToBottom => + _scrollToBottom!; + + void Function(int)? _resetDragTargetIndex; + void Function(int) get resetDragTargetIndex => _resetDragTargetIndex!; } class ReorderFlexConfig { @@ -78,9 +101,12 @@ class ReorderFlex extends StatefulWidget { final DragTargetInterceptor? interceptor; + /// Save the [DraggingState] if the current [ReorderFlex] get reinitialize. final DraggingStateStorage? dragStateStorage; - final ReorderDragTargetIndexKeyStorage? dragTargetIndexKeyStorage; + final ReorderDragTargeKeys? dragTargetKeys; + + final ReorderFlexAction? reorderFlexAction; final bool reorderable; @@ -93,10 +119,11 @@ class ReorderFlex extends StatefulWidget { required this.onReorder, this.reorderable = true, this.dragStateStorage, - this.dragTargetIndexKeyStorage, + this.dragTargetKeys, this.onDragStarted, this.onDragEnded, this.interceptor, + this.reorderFlexAction, this.direction = Axis.vertical, }) : assert(children.every((Widget w) => w.key != null), 'All child must have a key.'), @@ -109,7 +136,7 @@ class ReorderFlex extends StatefulWidget { } class ReorderFlexState extends State - with ReorderFlexMinxi, TickerProviderStateMixin { + with ReorderFlexMixin, TickerProviderStateMixin { /// Controls scrolls and measures scroll progress. late ScrollController _scrollController; @@ -131,11 +158,11 @@ class ReorderFlexState extends State void initState() { _notifier = ReorderFlexNotifier(); final flexId = widget.reorderFlexId; - dragState = widget.dragStateStorage?.read(flexId) ?? + dragState = widget.dragStateStorage?.readState(flexId) ?? DraggingState(widget.reorderFlexId); Log.trace('[DragTarget] init dragState: $dragState'); - widget.dragStateStorage?.remove(flexId); + widget.dragStateStorage?.removeState(flexId); _animation = DragTargetAnimation( reorderAnimationDuration: widget.config.reorderAnimationDuration, @@ -148,6 +175,14 @@ class ReorderFlexState extends State vsync: this, ); + widget.reorderFlexAction?._scrollToBottom = (fn) { + scrollToBottom(fn); + }; + + widget.reorderFlexAction?._resetDragTargetIndex = (index) { + resetDragTargetIndex(index); + }; + super.initState(); } @@ -184,7 +219,7 @@ class ReorderFlexState extends State final indexKey = GlobalObjectKey(child.key!); // Save the index key for quick access - widget.dragTargetIndexKeyStorage?.addKey( + widget.dragTargetKeys?.insertDragTarget( widget.reorderFlexId, item.id, indexKey, @@ -236,8 +271,12 @@ class ReorderFlexState extends State /// [childIndex]: the index of the child in a list Widget _wrap(Widget child, int childIndex, GlobalObjectKey indexKey) { return Builder(builder: (context) { - final ReorderDragTarget dragTarget = - _buildDragTarget(context, child, childIndex, indexKey); + final ReorderDragTarget dragTarget = _buildDragTarget( + context, + child, + childIndex, + indexKey, + ); int shiftedIndex = childIndex; if (dragState.isOverlapWithPhantom()) { @@ -342,6 +381,15 @@ class ReorderFlexState extends State }); } + static ReorderFlexState of(BuildContext context) { + if (context is StatefulElement && context.state is ReorderFlexState) { + return context.state as ReorderFlexState; + } + final ReorderFlexState? result = + context.findAncestorStateOfType(); + return result!; + } + ReorderDragTarget _buildDragTarget( BuildContext builderContext, Widget child, @@ -364,7 +412,7 @@ class ReorderFlexState extends State "[DragTarget] Group:[${widget.dataSource.identifier}] start dragging item at $draggingIndex"); _startDragging(draggingWidget, draggingIndex, size); widget.onDragStarted?.call(draggingIndex); - widget.dragStateStorage?.remove(widget.reorderFlexId); + widget.dragStateStorage?.removeState(widget.reorderFlexId); }, onDragMoved: (dragTargetData, offset) { dragTargetData.dragTargetOffset = offset; @@ -435,6 +483,7 @@ class ReorderFlexState extends State draggableTargetBuilder: widget.interceptor?.draggableTargetBuilder, useMoveAnimation: widget.config.useMoveAnimation, draggable: widget.reorderable, + draggingOpacity: widget.config.draggingWidgetOpacity, child: child, ); } @@ -487,7 +536,7 @@ class ReorderFlexState extends State } dragState.setStartDraggingIndex(dragTargetIndex); - widget.dragStateStorage?.write( + widget.dragStateStorage?.insertState( widget.reorderFlexId, dragState, ); @@ -581,46 +630,46 @@ class ReorderFlexState extends State } } - void scrollToBottom(VoidCallback? completed) { + void scrollToBottom(void Function(BuildContext)? completed) { if (_scrolling) { - completed?.call(); + completed?.call(context); return; } if (widget.dataSource.items.isNotEmpty) { final item = widget.dataSource.items.last; - final indexKey = widget.dragTargetIndexKeyStorage?.readKey( + final dragTargetKey = widget.dragTargetKeys?.getDragTarget( widget.reorderFlexId, item.id, ); - if (indexKey == null) { - completed?.call(); + if (dragTargetKey == null) { + completed?.call(context); return; } - final indexContext = indexKey.currentContext; - if (indexContext == null || _scrollController.hasClients == false) { - completed?.call(); + final dragTargetContext = dragTargetKey.currentContext; + if (dragTargetContext == null || _scrollController.hasClients == false) { + completed?.call(context); return; } - final renderObject = indexContext.findRenderObject(); - if (renderObject != null) { + final dragTargetRenderObject = dragTargetContext.findRenderObject(); + if (dragTargetRenderObject != null) { _scrolling = true; _scrollController.position .ensureVisible( - renderObject, + dragTargetRenderObject, alignment: 0.5, duration: const Duration(milliseconds: 120), ) .then((value) { setState(() { _scrolling = false; - completed?.call(); + completed?.call(context); }); }); } else { - completed?.call(); + completed?.call(context); } } } diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/reorder_mixin.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/reorder_mixin.dart index accdaa866b..742d5dd7a6 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/reorder_mixin.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/reorder_mixin.dart @@ -3,7 +3,7 @@ import 'package:flutter/widgets.dart'; import '../transitions.dart'; import 'drag_target.dart'; -mixin ReorderFlexMinxi { +mixin ReorderFlexMixin { @protected Widget makeAppearingWidget( Widget child, diff --git a/frontend/app_flowy/packages/appflowy_board/pubspec.yaml b/frontend/app_flowy/packages/appflowy_board/pubspec.yaml index 90907edca5..35c7aa2b67 100644 --- a/frontend/app_flowy/packages/appflowy_board/pubspec.yaml +++ b/frontend/app_flowy/packages/appflowy_board/pubspec.yaml @@ -1,6 +1,6 @@ name: appflowy_board description: AppFlowyBoard is a board-style widget that consists of multi-groups. It supports drag and drop between different groups. -version: 0.0.7 +version: 0.0.8 homepage: https://github.com/AppFlowy-IO/AppFlowy repository: https://github.com/AppFlowy-IO/AppFlowy/tree/main/frontend/app_flowy/packages/appflowy_board diff --git a/frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/code_block_node_widget.dart b/frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/code_block_node_widget.dart index 645e4c0c75..990edf9298 100644 --- a/frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/code_block_node_widget.dart +++ b/frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/code_block_node_widget.dart @@ -45,7 +45,7 @@ ShortcutEventHandler _ignorekHandler = (editorState, event) { }; SelectionMenuItem codeBlockMenuItem = SelectionMenuItem( - name: 'Code Block', + name: () => 'Code Block', icon: const Icon(Icons.abc), keywords: ['code block'], handler: (editorState, _, __) { diff --git a/frontend/app_flowy/packages/appflowy_editor/example/macos/Podfile.lock b/frontend/app_flowy/packages/appflowy_editor/example/macos/Podfile.lock index 49a5879fa6..79143cb186 100644 --- a/frontend/app_flowy/packages/appflowy_editor/example/macos/Podfile.lock +++ b/frontend/app_flowy/packages/appflowy_editor/example/macos/Podfile.lock @@ -24,7 +24,7 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos SPEC CHECKSUMS: - FlutterMacOS: 57701585bf7de1b3fc2bb61f6378d73bbdea8424 + FlutterMacOS: ae6af50a8ea7d6103d888583d46bd8328a7e9811 path_provider_macos: 3c0c3b4b0d4a76d2bf989a913c2de869c5641a19 rich_clipboard_macos: 43364b66b9dc69d203eb8dd6d758e2d12e02723c url_launcher_macos: 597e05b8e514239626bcf4a850fcf9ef5c856ec3 diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/l10n/intl_cs_CZ.arb b/frontend/app_flowy/packages/appflowy_editor/lib/l10n/intl_cs_CZ.arb new file mode 100644 index 0000000000..2b1ea7b0bf --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/lib/l10n/intl_cs_CZ.arb @@ -0,0 +1,35 @@ +{ + "@@locale": "cs-CZ", + "bold": "Tučně", + "@bold": {}, + "bulletedList": "Odrážkový seznam", + "@bulletedList": {}, + "checkbox": "Zaškrtávací políčko", + "@checkbox": {}, + "embedCode": "Vložit kód", + "@embedCode": {}, + "heading1": "Nadpis 1", + "@heading1": {}, + "heading2": "Nadpis 2", + "@heading2": {}, + "heading3": "Nadpis 3", + "@heading3": {}, + "highlight": "Zvýraznění", + "@highlight": {}, + "image": "Obrázek", + "@image": {}, + "italic": "Kurzíva", + "@italic": {}, + "link": "Odkaz", + "@link": {}, + "numberedList": "Číslovaný seznam", + "@numberedList": {}, + "quote": "Citace", + "@quote": {}, + "strikethrough": "Přeškrtnutí", + "@strikethrough": {}, + "text": "Text", + "@text": {}, + "underline": "Podtržení", + "@underline": {} +} \ No newline at end of file diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/l10n/intl_fr_CA.arb b/frontend/app_flowy/packages/appflowy_editor/lib/l10n/intl_fr_CA.arb index 6875d45d72..1ff4ab2f75 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/l10n/intl_fr_CA.arb +++ b/frontend/app_flowy/packages/appflowy_editor/lib/l10n/intl_fr_CA.arb @@ -1,35 +1,35 @@ { "@@locale": "fr-CA", - "bold": "", + "bold": "gras", "@bold": {}, - "bulletedList": "", + "bulletedList": "liste à puces", "@bulletedList": {}, - "checkbox": "", + "checkbox": "case à cocher", "@checkbox": {}, - "embedCode": "", + "embedCode": "incorporer Code", "@embedCode": {}, - "heading1": "", + "heading1": "en-tête1", "@heading1": {}, - "heading2": "", + "heading2": "en-tête2", "@heading2": {}, - "heading3": "", + "heading3": "en-tête3", "@heading3": {}, - "highlight": "", + "highlight": "mettre en évidence", "@highlight": {}, - "image": "", + "image": "l’image", "@image": {}, - "italic": "", + "italic": "italique", "@italic": {}, - "link": "", + "link": "lien", "@link": {}, - "numberedList": "", + "numberedList": "liste numérotée", "@numberedList": {}, - "quote": "", + "quote": "citation", "@quote": {}, - "strikethrough": "", + "strikethrough": "barré", "@strikethrough": {}, - "text": "", + "text": "texte", "@text": {}, - "underline": "", + "underline": "souligner", "@underline": {} } \ No newline at end of file diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/l10n/intl_fr_FR.arb b/frontend/app_flowy/packages/appflowy_editor/lib/l10n/intl_fr_FR.arb index 051676b02c..0e63189506 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/l10n/intl_fr_FR.arb +++ b/frontend/app_flowy/packages/appflowy_editor/lib/l10n/intl_fr_FR.arb @@ -1,35 +1,35 @@ { "@@locale": "fr-FR", - "bold": "", + "bold": "Gras", "@bold": {}, - "bulletedList": "", + "bulletedList": "List à puces", "@bulletedList": {}, - "checkbox": "", + "checkbox": "Case à cocher", "@checkbox": {}, - "embedCode": "", + "embedCode": "Incorporer code", "@embedCode": {}, - "heading1": "", + "heading1": "Titre 1", "@heading1": {}, - "heading2": "", + "heading2": "Titre 2", "@heading2": {}, - "heading3": "", + "heading3": "Titre 3", "@heading3": {}, - "highlight": "", + "highlight": "Surligné", "@highlight": {}, - "image": "", + "image": "Image", "@image": {}, - "italic": "", + "italic": "Italique", "@italic": {}, - "link": "", + "link": "Lien", "@link": {}, - "numberedList": "", + "numberedList": "Liste numérotée", "@numberedList": {}, - "quote": "", + "quote": "Citation", "@quote": {}, - "strikethrough": "", + "strikethrough": "Barré", "@strikethrough": {}, - "text": "", + "text": "Texte", "@text": {}, - "underline": "", + "underline": "Souligné", "@underline": {} } \ No newline at end of file diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/l10n/intl_hu_HU.arb b/frontend/app_flowy/packages/appflowy_editor/lib/l10n/intl_hu_HU.arb index 8b55ed3096..f96b3b0ec3 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/l10n/intl_hu_HU.arb +++ b/frontend/app_flowy/packages/appflowy_editor/lib/l10n/intl_hu_HU.arb @@ -1,35 +1,35 @@ { "@@locale": "hu-HU", - "bold": "", + "bold": "bátor", "@bold": {}, - "bulletedList": "", + "bulletedList": "pontozott lista", "@bulletedList": {}, - "checkbox": "", + "checkbox": "jelölőnégyzetet", "@checkbox": {}, - "embedCode": "", + "embedCode": "Beágyazás", "@embedCode": {}, - "heading1": "", + "heading1": "címsor1", "@heading1": {}, - "heading2": "", + "heading2": "címsor2", "@heading2": {}, - "heading3": "", + "heading3": "címsor3", "@heading3": {}, - "highlight": "", + "highlight": "Kiemel", "@highlight": {}, - "image": "", + "image": "kép", "@image": {}, - "italic": "", + "italic": "dőlt", "@italic": {}, - "link": "", + "link": "link", "@link": {}, - "numberedList": "", + "numberedList": "számozottLista", "@numberedList": {}, - "quote": "", + "quote": "idézet", "@quote": {}, - "strikethrough": "", + "strikethrough": "áthúzott", "@strikethrough": {}, - "text": "", + "text": "szöveg", "@text": {}, - "underline": "", + "underline": "aláhúzás", "@underline": {} } \ No newline at end of file diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/l10n/intl_id_ID.arb b/frontend/app_flowy/packages/appflowy_editor/lib/l10n/intl_id_ID.arb index cadebb36aa..6b3f6a0a6d 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/l10n/intl_id_ID.arb +++ b/frontend/app_flowy/packages/appflowy_editor/lib/l10n/intl_id_ID.arb @@ -1,35 +1,35 @@ { "@@locale": "id-ID", - "bold": "", + "bold": "berani", "@bold": {}, - "bulletedList": "", + "bulletedList": "daftar berpoin", "@bulletedList": {}, - "checkbox": "", + "checkbox": "kotak centang", "@checkbox": {}, - "embedCode": "", + "embedCode": "menyematkan Kode", "@embedCode": {}, - "heading1": "", + "heading1": "pos1", "@heading1": {}, - "heading2": "", + "heading2": "pos2", "@heading2": {}, - "heading3": "", + "heading3": "pos3", "@heading3": {}, - "highlight": "", + "highlight": "menyorot", "@highlight": {}, - "image": "", + "image": "gambar", "@image": {}, - "italic": "", + "italic": "miring", "@italic": {}, - "link": "", + "link": "tautan", "@link": {}, - "numberedList": "", + "numberedList": "daftar bernomor", "@numberedList": {}, - "quote": "", + "quote": "mengutip", "@quote": {}, - "strikethrough": "", + "strikethrough": "coret", "@strikethrough": {}, - "text": "", + "text": "teks", "@text": {}, - "underline": "", + "underline": "menggarisbawahi", "@underline": {} } \ No newline at end of file diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/l10n/intl_it_IT.arb b/frontend/app_flowy/packages/appflowy_editor/lib/l10n/intl_it_IT.arb index 908b80f275..e645959f7b 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/l10n/intl_it_IT.arb +++ b/frontend/app_flowy/packages/appflowy_editor/lib/l10n/intl_it_IT.arb @@ -1,35 +1,35 @@ { "@@locale": "it-IT", - "bold": "", + "bold": "Grassetto", "@bold": {}, - "bulletedList": "", + "bulletedList": "Elenco puntato", "@bulletedList": {}, - "checkbox": "", + "checkbox": "Casella di spunta", "@checkbox": {}, - "embedCode": "", + "embedCode": "Incorpora codice", "@embedCode": {}, - "heading1": "", + "heading1": "H1", "@heading1": {}, - "heading2": "", + "heading2": "H2", "@heading2": {}, - "heading3": "", + "heading3": "H3", "@heading3": {}, - "highlight": "", + "highlight": "Evidenzia", "@highlight": {}, - "image": "", + "image": "Immagine", "@image": {}, - "italic": "", + "italic": "Corsivo", "@italic": {}, - "link": "", + "link": "Collegamento", "@link": {}, - "numberedList": "", + "numberedList": "Elenco numerato", "@numberedList": {}, - "quote": "", + "quote": "Cita", "@quote": {}, - "strikethrough": "", + "strikethrough": "Barrato", "@strikethrough": {}, - "text": "", + "text": "Testo", "@text": {}, - "underline": "", + "underline": "Sottolineato", "@underline": {} } \ No newline at end of file diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/l10n/intl_nl_NL.arb b/frontend/app_flowy/packages/appflowy_editor/lib/l10n/intl_nl_NL.arb new file mode 100644 index 0000000000..814e22655d --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/lib/l10n/intl_nl_NL.arb @@ -0,0 +1,35 @@ +{ + "@@locale": "nl-NL", + "bold": "Vet", + "@bold": {}, + "bulletedList": "Opsommingstekens", + "@bulletedList": {}, + "checkbox": "Selectievakje", + "@checkbox": {}, + "embedCode": "Invoegcode", + "@embedCode": {}, + "heading1": "H1", + "@heading1": {}, + "heading2": "H2", + "@heading2": {}, + "heading3": "H3", + "@heading3": {}, + "highlight": "Highlight", + "@highlight": {}, + "image": "Afbeelding", + "@image": {}, + "italic": "Cursief", + "@italic": {}, + "link": "", + "@link": {}, + "numberedList": "Nummering", + "@numberedList": {}, + "quote": "Quote", + "@quote": {}, + "strikethrough": "Doorhalen", + "@strikethrough": {}, + "text": "Tekst", + "@text": {}, + "underline": "Onderstrepen", + "@underline": {} +} \ No newline at end of file diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/l10n/intl_pt_BR.arb b/frontend/app_flowy/packages/appflowy_editor/lib/l10n/intl_pt_BR.arb index 8e9c87e0cf..8f9186b7d8 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/l10n/intl_pt_BR.arb +++ b/frontend/app_flowy/packages/appflowy_editor/lib/l10n/intl_pt_BR.arb @@ -1,35 +1,35 @@ { "@@locale": "pt-BR", - "bold": "", + "bold": "Negrito", "@bold": {}, - "bulletedList": "", + "bulletedList": "Lista de marcadores", "@bulletedList": {}, - "checkbox": "", + "checkbox": "Caixa de seleção", "@checkbox": {}, - "embedCode": "", + "embedCode": "Código incorporado", "@embedCode": {}, - "heading1": "", + "heading1": "H1", "@heading1": {}, - "heading2": "", + "heading2": "H2", "@heading2": {}, - "heading3": "", + "heading3": "H3", "@heading3": {}, - "highlight": "", + "highlight": "Destacar", "@highlight": {}, - "image": "", + "image": "Imagem", "@image": {}, - "italic": "", + "italic": "Itálico", "@italic": {}, - "link": "", + "link": "Link", "@link": {}, - "numberedList": "", + "numberedList": "Lista numerada", "@numberedList": {}, - "quote": "", + "quote": "Citar", "@quote": {}, - "strikethrough": "", + "strikethrough": "Rasurar", "@strikethrough": {}, - "text": "", + "text": "Texto", "@text": {}, - "underline": "", + "underline": "Sublinhar", "@underline": {} } \ No newline at end of file diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/l10n/intl_pt_PT.arb b/frontend/app_flowy/packages/appflowy_editor/lib/l10n/intl_pt_PT.arb index 4e9187f282..9b7386fd46 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/l10n/intl_pt_PT.arb +++ b/frontend/app_flowy/packages/appflowy_editor/lib/l10n/intl_pt_PT.arb @@ -1,35 +1,35 @@ { "@@locale": "pt-PT", - "bold": "", + "bold": "negrito", "@bold": {}, - "bulletedList": "", + "bulletedList": "lista com marcadores", "@bulletedList": {}, - "checkbox": "", + "checkbox": "caixa de seleção", "@checkbox": {}, - "embedCode": "", + "embedCode": "Código embutido", "@embedCode": {}, - "heading1": "", + "heading1": "Cabeçallho 1", "@heading1": {}, - "heading2": "", + "heading2": "Cabeçallho 2", "@heading2": {}, - "heading3": "", + "heading3": "Cabeçallho 3", "@heading3": {}, - "highlight": "", + "highlight": "realçar", "@highlight": {}, - "image": "", + "image": "imagem", "@image": {}, - "italic": "", + "italic": "itálico", "@italic": {}, - "link": "", + "link": "link", "@link": {}, - "numberedList": "", + "numberedList": "lista numerada", "@numberedList": {}, - "quote": "", + "quote": "citar", "@quote": {}, - "strikethrough": "", + "strikethrough": "tachado", "@strikethrough": {}, - "text": "", + "text": "texto", "@text": {}, - "underline": "", + "underline": "sublinhado", "@underline": {} } \ No newline at end of file diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/commands/edit_text.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/commands/edit_text.dart new file mode 100644 index 0000000000..2e1310ca2c --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/commands/edit_text.dart @@ -0,0 +1,34 @@ +import 'dart:async'; + +import 'package:appflowy_editor/src/commands/text_command_infra.dart'; +import 'package:appflowy_editor/src/document/node.dart'; +import 'package:appflowy_editor/src/document/path.dart'; +import 'package:appflowy_editor/src/editor_state.dart'; +import 'package:appflowy_editor/src/operation/transaction_builder.dart'; +import 'package:flutter/widgets.dart'; + +Future insertContextInText( + EditorState editorState, + int index, + String content, { + Path? path, + TextNode? textNode, +}) async { + final result = getTextNodeToBeFormatted( + editorState, + path: path, + textNode: textNode, + ); + + final completer = Completer(); + + TransactionBuilder(editorState) + ..insertText(result, index, content) + ..commit(); + + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + completer.complete(); + }); + + return completer.future; +} diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/commands/format_built_in_text.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/commands/format_built_in_text.dart new file mode 100644 index 0000000000..dcce054351 --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/commands/format_built_in_text.dart @@ -0,0 +1,83 @@ +import 'package:appflowy_editor/src/commands/format_text.dart'; +import 'package:appflowy_editor/src/commands/text_command_infra.dart'; +import 'package:appflowy_editor/src/document/attributes.dart'; +import 'package:appflowy_editor/src/document/built_in_attribute_keys.dart'; +import 'package:appflowy_editor/src/document/node.dart'; +import 'package:appflowy_editor/src/document/path.dart'; +import 'package:appflowy_editor/src/document/selection.dart'; +import 'package:appflowy_editor/src/editor_state.dart'; + +Future formatBuiltInTextAttributes( + EditorState editorState, + String key, + Attributes attributes, { + Selection? selection, + Path? path, + TextNode? textNode, +}) async { + final result = getTextNodeToBeFormatted( + editorState, + path: path, + textNode: textNode, + ); + if (BuiltInAttributeKey.globalStyleKeys.contains(key)) { + // remove all the existing style + final newAttributes = result.attributes + ..removeWhere((key, value) { + if (BuiltInAttributeKey.globalStyleKeys.contains(key)) { + return true; + } + return false; + }) + ..addAll(attributes) + ..addAll({ + BuiltInAttributeKey.subtype: key, + }); + return updateTextNodeAttributes( + editorState, + newAttributes, + textNode: textNode, + ); + } else if (BuiltInAttributeKey.partialStyleKeys.contains(key)) { + return updateTextNodeDeltaAttributes( + editorState, + selection, + attributes, + textNode: textNode, + ); + } +} + +Future formatTextToCheckbox( + EditorState editorState, + bool check, { + Path? path, + TextNode? textNode, +}) async { + return formatBuiltInTextAttributes( + editorState, + BuiltInAttributeKey.checkbox, + { + BuiltInAttributeKey.checkbox: check, + }, + path: path, + textNode: textNode, + ); +} + +Future formatLinkInText( + EditorState editorState, + String? link, { + Path? path, + TextNode? textNode, +}) async { + return formatBuiltInTextAttributes( + editorState, + BuiltInAttributeKey.href, + { + BuiltInAttributeKey.href: link, + }, + path: path, + textNode: textNode, + ); +} diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/commands/format_text.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/commands/format_text.dart new file mode 100644 index 0000000000..0ec9e7b61a --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/commands/format_text.dart @@ -0,0 +1,67 @@ +import 'dart:async'; + +import 'package:appflowy_editor/src/commands/text_command_infra.dart'; +import 'package:appflowy_editor/src/document/attributes.dart'; +import 'package:appflowy_editor/src/document/node.dart'; +import 'package:appflowy_editor/src/document/path.dart'; +import 'package:appflowy_editor/src/document/selection.dart'; +import 'package:appflowy_editor/src/editor_state.dart'; +import 'package:appflowy_editor/src/operation/transaction_builder.dart'; +import 'package:flutter/widgets.dart'; + +Future updateTextNodeAttributes( + EditorState editorState, + Attributes attributes, { + Path? path, + TextNode? textNode, +}) async { + final result = getTextNodeToBeFormatted( + editorState, + path: path, + textNode: textNode, + ); + + final completer = Completer(); + + TransactionBuilder(editorState) + ..updateNode(result, attributes) + ..commit(); + + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + completer.complete(); + }); + + return completer.future; +} + +Future updateTextNodeDeltaAttributes( + EditorState editorState, + Selection? selection, + Attributes attributes, { + Path? path, + TextNode? textNode, +}) { + final result = getTextNodeToBeFormatted( + editorState, + path: path, + textNode: textNode, + ); + final newSelection = getSelection(editorState, selection: selection); + + final completer = Completer(); + + TransactionBuilder(editorState) + ..formatText( + result, + newSelection.startIndex, + newSelection.length, + attributes, + ) + ..commit(); + + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + completer.complete(); + }); + + return completer.future; +} diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/commands/text_command_infra.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/commands/text_command_infra.dart new file mode 100644 index 0000000000..d54a84a3e0 --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/commands/text_command_infra.dart @@ -0,0 +1,43 @@ +import 'package:appflowy_editor/src/document/node.dart'; +import 'package:appflowy_editor/src/document/path.dart'; +import 'package:appflowy_editor/src/document/selection.dart'; +import 'package:appflowy_editor/src/editor_state.dart'; + +// get formatted [TextNode] +TextNode getTextNodeToBeFormatted( + EditorState editorState, { + Path? path, + TextNode? textNode, +}) { + final currentSelection = + editorState.service.selectionService.currentSelection.value; + TextNode result; + if (textNode != null) { + result = textNode; + } else if (path != null) { + result = editorState.document.nodeAtPath(path) as TextNode; + } else if (currentSelection != null && currentSelection.isCollapsed) { + result = editorState.document.nodeAtPath(currentSelection.start.path) + as TextNode; + } else { + throw Exception('path and textNode cannot be null at the same time'); + } + return result; +} + +Selection getSelection( + EditorState editorState, { + Selection? selection, +}) { + final currentSelection = + editorState.service.selectionService.currentSelection.value; + Selection result; + if (selection != null) { + result = selection; + } else if (currentSelection != null) { + result = currentSelection; + } else { + throw Exception('path and textNode cannot be null at the same time'); + } + return result; +} diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/document/selection.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/document/selection.dart index ea451b46dd..99248dc167 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/document/selection.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/document/selection.dart @@ -53,6 +53,10 @@ class Selection { Selection get reversed => copyWith(start: end, end: start); + int get startIndex => normalize.start.offset; + int get endIndex => normalize.end.offset; + int get length => endIndex - startIndex; + Selection collapse({bool atStart = false}) { if (atStart) { return Selection(start: start, end: start); diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/editor_state.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/editor_state.dart index 4f2ca39a84..4aeb7ab599 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/editor_state.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/editor_state.dart @@ -72,6 +72,8 @@ class EditorState { // TODO: only for testing. bool disableSealTimer = false; + bool editable = true; + Selection? get cursorSelection { return _cursorSelection; } @@ -112,6 +114,9 @@ class EditorState { /// should record the transaction in undo/redo stack. apply(Transaction transaction, [ApplyOptions options = const ApplyOptions()]) { + if (!editable) { + return; + } // TODO: validate the transation. for (final op in transaction.operations) { _applyOperation(op); diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/text_node_extensions.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/text_node_extensions.dart index 352cb69f97..d4d7857286 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/text_node_extensions.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/text_node_extensions.dart @@ -54,6 +54,11 @@ extension TextNodeExtension on TextNode { return value == true; }); + bool allSatisfyCodeInSelection(Selection selection) => + allSatisfyInSelection(selection, BuiltInAttributeKey.code, (value) { + return value == true; + }); + bool allSatisfyInSelection( Selection selection, String styleKey, diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_all.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_all.dart index 1dc2273626..02e3d46041 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_all.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_all.dart @@ -16,6 +16,7 @@ import 'package:intl/message_lookup_by_library.dart'; import 'package:intl/src/intl_helpers.dart'; import 'messages_ca.dart' as messages_ca; +import 'messages_cs-CZ.dart' as messages_cs_cz; import 'messages_de-DE.dart' as messages_de_de; import 'messages_en.dart' as messages_en; import 'messages_es-VE.dart' as messages_es_ve; @@ -25,6 +26,7 @@ import 'messages_hu-HU.dart' as messages_hu_hu; import 'messages_id-ID.dart' as messages_id_id; import 'messages_it-IT.dart' as messages_it_it; import 'messages_ja-JP.dart' as messages_ja_jp; +import 'messages_nl-NL.dart' as messages_nl_nl; import 'messages_pl-PL.dart' as messages_pl_pl; import 'messages_pt-BR.dart' as messages_pt_br; import 'messages_pt-PT.dart' as messages_pt_pt; @@ -36,6 +38,7 @@ import 'messages_zh-TW.dart' as messages_zh_tw; typedef Future LibraryLoader(); Map _deferredLibraries = { 'ca': () => new Future.value(null), + 'cs_CZ': () => new Future.value(null), 'de_DE': () => new Future.value(null), 'en': () => new Future.value(null), 'es_VE': () => new Future.value(null), @@ -45,6 +48,7 @@ Map _deferredLibraries = { 'id_ID': () => new Future.value(null), 'it_IT': () => new Future.value(null), 'ja_JP': () => new Future.value(null), + 'nl_NL': () => new Future.value(null), 'pl_PL': () => new Future.value(null), 'pt_BR': () => new Future.value(null), 'pt_PT': () => new Future.value(null), @@ -58,6 +62,8 @@ MessageLookupByLibrary? _findExact(String localeName) { switch (localeName) { case 'ca': return messages_ca.messages; + case 'cs_CZ': + return messages_cs_cz.messages; case 'de_DE': return messages_de_de.messages; case 'en': @@ -76,6 +82,8 @@ MessageLookupByLibrary? _findExact(String localeName) { return messages_it_it.messages; case 'ja_JP': return messages_ja_jp.messages; + case 'nl_NL': + return messages_nl_nl.messages; case 'pl_PL': return messages_pl_pl.messages; case 'pt_BR': diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_cs-CZ.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_cs-CZ.dart new file mode 100644 index 0000000000..810fe3888f --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_cs-CZ.dart @@ -0,0 +1,44 @@ +// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart +// This is a library that provides messages for a cs_CZ locale. All the +// messages from the main program should be duplicated here with the same +// function name. + +// Ignore issues from commonly used lints in this file. +// ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new +// ignore_for_file:prefer_single_quotes,comment_references, directives_ordering +// ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases +// ignore_for_file:unused_import, file_names, avoid_escaping_inner_quotes +// ignore_for_file:unnecessary_string_interpolations, unnecessary_string_escapes + +import 'package:intl/intl.dart'; +import 'package:intl/message_lookup_by_library.dart'; + +final messages = new MessageLookup(); + +typedef String MessageIfAbsent(String messageStr, List args); + +class MessageLookup extends MessageLookupByLibrary { + String get localeName => 'cs_CZ'; + + final messages = _notInlinedMessages(_notInlinedMessages); + static Map _notInlinedMessages(_) => { + "bold": MessageLookupByLibrary.simpleMessage("Tučně"), + "bulletedList": + MessageLookupByLibrary.simpleMessage("Odrážkový seznam"), + "checkbox": MessageLookupByLibrary.simpleMessage("Zaškrtávací políčko"), + "embedCode": MessageLookupByLibrary.simpleMessage("Vložit kód"), + "heading1": MessageLookupByLibrary.simpleMessage("Nadpis 1"), + "heading2": MessageLookupByLibrary.simpleMessage("Nadpis 2"), + "heading3": MessageLookupByLibrary.simpleMessage("Nadpis 3"), + "highlight": MessageLookupByLibrary.simpleMessage("Zvýraznění"), + "image": MessageLookupByLibrary.simpleMessage("Obrázek"), + "italic": MessageLookupByLibrary.simpleMessage("Kurzíva"), + "link": MessageLookupByLibrary.simpleMessage("Odkaz"), + "numberedList": + MessageLookupByLibrary.simpleMessage("Číslovaný seznam"), + "quote": MessageLookupByLibrary.simpleMessage("Citace"), + "strikethrough": MessageLookupByLibrary.simpleMessage("Přeškrtnutí"), + "text": MessageLookupByLibrary.simpleMessage("Text"), + "underline": MessageLookupByLibrary.simpleMessage("Podtržení") + }; +} diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_fr-CA.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_fr-CA.dart index 7dd3517ac5..a7239232ed 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_fr-CA.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_fr-CA.dart @@ -22,21 +22,21 @@ class MessageLookup extends MessageLookupByLibrary { final messages = _notInlinedMessages(_notInlinedMessages); static Map _notInlinedMessages(_) => { - "bold": MessageLookupByLibrary.simpleMessage(""), - "bulletedList": MessageLookupByLibrary.simpleMessage(""), - "checkbox": MessageLookupByLibrary.simpleMessage(""), - "embedCode": MessageLookupByLibrary.simpleMessage(""), - "heading1": MessageLookupByLibrary.simpleMessage(""), - "heading2": MessageLookupByLibrary.simpleMessage(""), - "heading3": MessageLookupByLibrary.simpleMessage(""), - "highlight": MessageLookupByLibrary.simpleMessage(""), - "image": MessageLookupByLibrary.simpleMessage(""), - "italic": MessageLookupByLibrary.simpleMessage(""), - "link": MessageLookupByLibrary.simpleMessage(""), - "numberedList": MessageLookupByLibrary.simpleMessage(""), - "quote": MessageLookupByLibrary.simpleMessage(""), - "strikethrough": MessageLookupByLibrary.simpleMessage(""), - "text": MessageLookupByLibrary.simpleMessage(""), - "underline": MessageLookupByLibrary.simpleMessage("") + "bold": MessageLookupByLibrary.simpleMessage("gras"), + "bulletedList": MessageLookupByLibrary.simpleMessage("liste à puces"), + "checkbox": MessageLookupByLibrary.simpleMessage("case à cocher"), + "embedCode": MessageLookupByLibrary.simpleMessage("incorporer Code"), + "heading1": MessageLookupByLibrary.simpleMessage("en-tête1"), + "heading2": MessageLookupByLibrary.simpleMessage("en-tête2"), + "heading3": MessageLookupByLibrary.simpleMessage("en-tête3"), + "highlight": MessageLookupByLibrary.simpleMessage("mettre en évidence"), + "image": MessageLookupByLibrary.simpleMessage("l’image"), + "italic": MessageLookupByLibrary.simpleMessage("italique"), + "link": MessageLookupByLibrary.simpleMessage("lien"), + "numberedList": MessageLookupByLibrary.simpleMessage("liste numérotée"), + "quote": MessageLookupByLibrary.simpleMessage("citation"), + "strikethrough": MessageLookupByLibrary.simpleMessage("barré"), + "text": MessageLookupByLibrary.simpleMessage("texte"), + "underline": MessageLookupByLibrary.simpleMessage("souligner") }; } diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_fr-FR.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_fr-FR.dart index dbe21e0e02..07e7302033 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_fr-FR.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_fr-FR.dart @@ -22,21 +22,21 @@ class MessageLookup extends MessageLookupByLibrary { final messages = _notInlinedMessages(_notInlinedMessages); static Map _notInlinedMessages(_) => { - "bold": MessageLookupByLibrary.simpleMessage(""), - "bulletedList": MessageLookupByLibrary.simpleMessage(""), - "checkbox": MessageLookupByLibrary.simpleMessage(""), - "embedCode": MessageLookupByLibrary.simpleMessage(""), - "heading1": MessageLookupByLibrary.simpleMessage(""), - "heading2": MessageLookupByLibrary.simpleMessage(""), - "heading3": MessageLookupByLibrary.simpleMessage(""), - "highlight": MessageLookupByLibrary.simpleMessage(""), - "image": MessageLookupByLibrary.simpleMessage(""), - "italic": MessageLookupByLibrary.simpleMessage(""), - "link": MessageLookupByLibrary.simpleMessage(""), - "numberedList": MessageLookupByLibrary.simpleMessage(""), - "quote": MessageLookupByLibrary.simpleMessage(""), - "strikethrough": MessageLookupByLibrary.simpleMessage(""), - "text": MessageLookupByLibrary.simpleMessage(""), - "underline": MessageLookupByLibrary.simpleMessage("") + "bold": MessageLookupByLibrary.simpleMessage("Gras"), + "bulletedList": MessageLookupByLibrary.simpleMessage("List à puces"), + "checkbox": MessageLookupByLibrary.simpleMessage("Case à cocher"), + "embedCode": MessageLookupByLibrary.simpleMessage("Incorporer code"), + "heading1": MessageLookupByLibrary.simpleMessage("Titre 1"), + "heading2": MessageLookupByLibrary.simpleMessage("Titre 2"), + "heading3": MessageLookupByLibrary.simpleMessage("Titre 3"), + "highlight": MessageLookupByLibrary.simpleMessage("Surligné"), + "image": MessageLookupByLibrary.simpleMessage("Image"), + "italic": MessageLookupByLibrary.simpleMessage("Italique"), + "link": MessageLookupByLibrary.simpleMessage("Lien"), + "numberedList": MessageLookupByLibrary.simpleMessage("Liste numérotée"), + "quote": MessageLookupByLibrary.simpleMessage("Citation"), + "strikethrough": MessageLookupByLibrary.simpleMessage("Barré"), + "text": MessageLookupByLibrary.simpleMessage("Texte"), + "underline": MessageLookupByLibrary.simpleMessage("Souligné") }; } diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_hu-HU.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_hu-HU.dart index ac9acad543..44a54b5478 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_hu-HU.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_hu-HU.dart @@ -22,21 +22,21 @@ class MessageLookup extends MessageLookupByLibrary { final messages = _notInlinedMessages(_notInlinedMessages); static Map _notInlinedMessages(_) => { - "bold": MessageLookupByLibrary.simpleMessage(""), - "bulletedList": MessageLookupByLibrary.simpleMessage(""), - "checkbox": MessageLookupByLibrary.simpleMessage(""), - "embedCode": MessageLookupByLibrary.simpleMessage(""), - "heading1": MessageLookupByLibrary.simpleMessage(""), - "heading2": MessageLookupByLibrary.simpleMessage(""), - "heading3": MessageLookupByLibrary.simpleMessage(""), - "highlight": MessageLookupByLibrary.simpleMessage(""), - "image": MessageLookupByLibrary.simpleMessage(""), - "italic": MessageLookupByLibrary.simpleMessage(""), - "link": MessageLookupByLibrary.simpleMessage(""), - "numberedList": MessageLookupByLibrary.simpleMessage(""), - "quote": MessageLookupByLibrary.simpleMessage(""), - "strikethrough": MessageLookupByLibrary.simpleMessage(""), - "text": MessageLookupByLibrary.simpleMessage(""), - "underline": MessageLookupByLibrary.simpleMessage("") + "bold": MessageLookupByLibrary.simpleMessage("bátor"), + "bulletedList": MessageLookupByLibrary.simpleMessage("pontozott lista"), + "checkbox": MessageLookupByLibrary.simpleMessage("jelölőnégyzetet"), + "embedCode": MessageLookupByLibrary.simpleMessage("Beágyazás"), + "heading1": MessageLookupByLibrary.simpleMessage("címsor1"), + "heading2": MessageLookupByLibrary.simpleMessage("címsor2"), + "heading3": MessageLookupByLibrary.simpleMessage("címsor3"), + "highlight": MessageLookupByLibrary.simpleMessage("Kiemel"), + "image": MessageLookupByLibrary.simpleMessage("kép"), + "italic": MessageLookupByLibrary.simpleMessage("dőlt"), + "link": MessageLookupByLibrary.simpleMessage("link"), + "numberedList": MessageLookupByLibrary.simpleMessage("számozottLista"), + "quote": MessageLookupByLibrary.simpleMessage("idézet"), + "strikethrough": MessageLookupByLibrary.simpleMessage("áthúzott"), + "text": MessageLookupByLibrary.simpleMessage("szöveg"), + "underline": MessageLookupByLibrary.simpleMessage("aláhúzás") }; } diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_id-ID.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_id-ID.dart index e594bacdec..cda97336d4 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_id-ID.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_id-ID.dart @@ -22,21 +22,21 @@ class MessageLookup extends MessageLookupByLibrary { final messages = _notInlinedMessages(_notInlinedMessages); static Map _notInlinedMessages(_) => { - "bold": MessageLookupByLibrary.simpleMessage(""), - "bulletedList": MessageLookupByLibrary.simpleMessage(""), - "checkbox": MessageLookupByLibrary.simpleMessage(""), - "embedCode": MessageLookupByLibrary.simpleMessage(""), - "heading1": MessageLookupByLibrary.simpleMessage(""), - "heading2": MessageLookupByLibrary.simpleMessage(""), - "heading3": MessageLookupByLibrary.simpleMessage(""), - "highlight": MessageLookupByLibrary.simpleMessage(""), - "image": MessageLookupByLibrary.simpleMessage(""), - "italic": MessageLookupByLibrary.simpleMessage(""), - "link": MessageLookupByLibrary.simpleMessage(""), - "numberedList": MessageLookupByLibrary.simpleMessage(""), - "quote": MessageLookupByLibrary.simpleMessage(""), - "strikethrough": MessageLookupByLibrary.simpleMessage(""), - "text": MessageLookupByLibrary.simpleMessage(""), - "underline": MessageLookupByLibrary.simpleMessage("") + "bold": MessageLookupByLibrary.simpleMessage("berani"), + "bulletedList": MessageLookupByLibrary.simpleMessage("daftar berpoin"), + "checkbox": MessageLookupByLibrary.simpleMessage("kotak centang"), + "embedCode": MessageLookupByLibrary.simpleMessage("menyematkan Kode"), + "heading1": MessageLookupByLibrary.simpleMessage("pos1"), + "heading2": MessageLookupByLibrary.simpleMessage("pos2"), + "heading3": MessageLookupByLibrary.simpleMessage("pos3"), + "highlight": MessageLookupByLibrary.simpleMessage("menyorot"), + "image": MessageLookupByLibrary.simpleMessage("gambar"), + "italic": MessageLookupByLibrary.simpleMessage("miring"), + "link": MessageLookupByLibrary.simpleMessage("tautan"), + "numberedList": MessageLookupByLibrary.simpleMessage("daftar bernomor"), + "quote": MessageLookupByLibrary.simpleMessage("mengutip"), + "strikethrough": MessageLookupByLibrary.simpleMessage("coret"), + "text": MessageLookupByLibrary.simpleMessage("teks"), + "underline": MessageLookupByLibrary.simpleMessage("menggarisbawahi") }; } diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_it-IT.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_it-IT.dart index 6717858291..05ee3e1353 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_it-IT.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_it-IT.dart @@ -22,21 +22,21 @@ class MessageLookup extends MessageLookupByLibrary { final messages = _notInlinedMessages(_notInlinedMessages); static Map _notInlinedMessages(_) => { - "bold": MessageLookupByLibrary.simpleMessage(""), - "bulletedList": MessageLookupByLibrary.simpleMessage(""), - "checkbox": MessageLookupByLibrary.simpleMessage(""), - "embedCode": MessageLookupByLibrary.simpleMessage(""), - "heading1": MessageLookupByLibrary.simpleMessage(""), - "heading2": MessageLookupByLibrary.simpleMessage(""), - "heading3": MessageLookupByLibrary.simpleMessage(""), - "highlight": MessageLookupByLibrary.simpleMessage(""), - "image": MessageLookupByLibrary.simpleMessage(""), - "italic": MessageLookupByLibrary.simpleMessage(""), - "link": MessageLookupByLibrary.simpleMessage(""), - "numberedList": MessageLookupByLibrary.simpleMessage(""), - "quote": MessageLookupByLibrary.simpleMessage(""), - "strikethrough": MessageLookupByLibrary.simpleMessage(""), - "text": MessageLookupByLibrary.simpleMessage(""), - "underline": MessageLookupByLibrary.simpleMessage("") + "bold": MessageLookupByLibrary.simpleMessage("Grassetto"), + "bulletedList": MessageLookupByLibrary.simpleMessage("Elenco puntato"), + "checkbox": MessageLookupByLibrary.simpleMessage("Casella di spunta"), + "embedCode": MessageLookupByLibrary.simpleMessage("Incorpora codice"), + "heading1": MessageLookupByLibrary.simpleMessage("H1"), + "heading2": MessageLookupByLibrary.simpleMessage("H2"), + "heading3": MessageLookupByLibrary.simpleMessage("H3"), + "highlight": MessageLookupByLibrary.simpleMessage("Evidenzia"), + "image": MessageLookupByLibrary.simpleMessage("Immagine"), + "italic": MessageLookupByLibrary.simpleMessage("Corsivo"), + "link": MessageLookupByLibrary.simpleMessage("Collegamento"), + "numberedList": MessageLookupByLibrary.simpleMessage("Elenco numerato"), + "quote": MessageLookupByLibrary.simpleMessage("Cita"), + "strikethrough": MessageLookupByLibrary.simpleMessage("Barrato"), + "text": MessageLookupByLibrary.simpleMessage("Testo"), + "underline": MessageLookupByLibrary.simpleMessage("Sottolineato") }; } diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_nl-NL.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_nl-NL.dart new file mode 100644 index 0000000000..eb096b9a7a --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_nl-NL.dart @@ -0,0 +1,43 @@ +// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart +// This is a library that provides messages for a nl_NL locale. All the +// messages from the main program should be duplicated here with the same +// function name. + +// Ignore issues from commonly used lints in this file. +// ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new +// ignore_for_file:prefer_single_quotes,comment_references, directives_ordering +// ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases +// ignore_for_file:unused_import, file_names, avoid_escaping_inner_quotes +// ignore_for_file:unnecessary_string_interpolations, unnecessary_string_escapes + +import 'package:intl/intl.dart'; +import 'package:intl/message_lookup_by_library.dart'; + +final messages = new MessageLookup(); + +typedef String MessageIfAbsent(String messageStr, List args); + +class MessageLookup extends MessageLookupByLibrary { + String get localeName => 'nl_NL'; + + final messages = _notInlinedMessages(_notInlinedMessages); + static Map _notInlinedMessages(_) => { + "bold": MessageLookupByLibrary.simpleMessage("Vet"), + "bulletedList": + MessageLookupByLibrary.simpleMessage("Opsommingstekens"), + "checkbox": MessageLookupByLibrary.simpleMessage("Selectievakje"), + "embedCode": MessageLookupByLibrary.simpleMessage("Invoegcode"), + "heading1": MessageLookupByLibrary.simpleMessage("H1"), + "heading2": MessageLookupByLibrary.simpleMessage("H2"), + "heading3": MessageLookupByLibrary.simpleMessage("H3"), + "highlight": MessageLookupByLibrary.simpleMessage("Highlight"), + "image": MessageLookupByLibrary.simpleMessage("Afbeelding"), + "italic": MessageLookupByLibrary.simpleMessage("Cursief"), + "link": MessageLookupByLibrary.simpleMessage(""), + "numberedList": MessageLookupByLibrary.simpleMessage("Nummering"), + "quote": MessageLookupByLibrary.simpleMessage("Quote"), + "strikethrough": MessageLookupByLibrary.simpleMessage("Doorhalen"), + "text": MessageLookupByLibrary.simpleMessage("Tekst"), + "underline": MessageLookupByLibrary.simpleMessage("Onderstrepen") + }; +} diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_pt-BR.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_pt-BR.dart index c7551f9d78..22c53407ac 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_pt-BR.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_pt-BR.dart @@ -22,21 +22,22 @@ class MessageLookup extends MessageLookupByLibrary { final messages = _notInlinedMessages(_notInlinedMessages); static Map _notInlinedMessages(_) => { - "bold": MessageLookupByLibrary.simpleMessage(""), - "bulletedList": MessageLookupByLibrary.simpleMessage(""), - "checkbox": MessageLookupByLibrary.simpleMessage(""), - "embedCode": MessageLookupByLibrary.simpleMessage(""), - "heading1": MessageLookupByLibrary.simpleMessage(""), - "heading2": MessageLookupByLibrary.simpleMessage(""), - "heading3": MessageLookupByLibrary.simpleMessage(""), - "highlight": MessageLookupByLibrary.simpleMessage(""), - "image": MessageLookupByLibrary.simpleMessage(""), - "italic": MessageLookupByLibrary.simpleMessage(""), - "link": MessageLookupByLibrary.simpleMessage(""), - "numberedList": MessageLookupByLibrary.simpleMessage(""), - "quote": MessageLookupByLibrary.simpleMessage(""), - "strikethrough": MessageLookupByLibrary.simpleMessage(""), - "text": MessageLookupByLibrary.simpleMessage(""), - "underline": MessageLookupByLibrary.simpleMessage("") + "bold": MessageLookupByLibrary.simpleMessage("Negrito"), + "bulletedList": + MessageLookupByLibrary.simpleMessage("Lista de marcadores"), + "checkbox": MessageLookupByLibrary.simpleMessage("Caixa de seleção"), + "embedCode": MessageLookupByLibrary.simpleMessage("Código incorporado"), + "heading1": MessageLookupByLibrary.simpleMessage("H1"), + "heading2": MessageLookupByLibrary.simpleMessage("H2"), + "heading3": MessageLookupByLibrary.simpleMessage("H3"), + "highlight": MessageLookupByLibrary.simpleMessage("Destacar"), + "image": MessageLookupByLibrary.simpleMessage("Imagem"), + "italic": MessageLookupByLibrary.simpleMessage("Itálico"), + "link": MessageLookupByLibrary.simpleMessage("Link"), + "numberedList": MessageLookupByLibrary.simpleMessage("Lista numerada"), + "quote": MessageLookupByLibrary.simpleMessage("Citar"), + "strikethrough": MessageLookupByLibrary.simpleMessage("Rasurar"), + "text": MessageLookupByLibrary.simpleMessage("Texto"), + "underline": MessageLookupByLibrary.simpleMessage("Sublinhar") }; } diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_pt-PT.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_pt-PT.dart index 19a14c2509..d2d2781580 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_pt-PT.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_pt-PT.dart @@ -22,21 +22,22 @@ class MessageLookup extends MessageLookupByLibrary { final messages = _notInlinedMessages(_notInlinedMessages); static Map _notInlinedMessages(_) => { - "bold": MessageLookupByLibrary.simpleMessage(""), - "bulletedList": MessageLookupByLibrary.simpleMessage(""), - "checkbox": MessageLookupByLibrary.simpleMessage(""), - "embedCode": MessageLookupByLibrary.simpleMessage(""), - "heading1": MessageLookupByLibrary.simpleMessage(""), - "heading2": MessageLookupByLibrary.simpleMessage(""), - "heading3": MessageLookupByLibrary.simpleMessage(""), - "highlight": MessageLookupByLibrary.simpleMessage(""), - "image": MessageLookupByLibrary.simpleMessage(""), - "italic": MessageLookupByLibrary.simpleMessage(""), - "link": MessageLookupByLibrary.simpleMessage(""), - "numberedList": MessageLookupByLibrary.simpleMessage(""), - "quote": MessageLookupByLibrary.simpleMessage(""), - "strikethrough": MessageLookupByLibrary.simpleMessage(""), - "text": MessageLookupByLibrary.simpleMessage(""), - "underline": MessageLookupByLibrary.simpleMessage("") + "bold": MessageLookupByLibrary.simpleMessage("negrito"), + "bulletedList": + MessageLookupByLibrary.simpleMessage("lista com marcadores"), + "checkbox": MessageLookupByLibrary.simpleMessage("caixa de seleção"), + "embedCode": MessageLookupByLibrary.simpleMessage("Código embutido"), + "heading1": MessageLookupByLibrary.simpleMessage("Cabeçallho 1"), + "heading2": MessageLookupByLibrary.simpleMessage("Cabeçallho 2"), + "heading3": MessageLookupByLibrary.simpleMessage("Cabeçallho 3"), + "highlight": MessageLookupByLibrary.simpleMessage("realçar"), + "image": MessageLookupByLibrary.simpleMessage("imagem"), + "italic": MessageLookupByLibrary.simpleMessage("itálico"), + "link": MessageLookupByLibrary.simpleMessage("link"), + "numberedList": MessageLookupByLibrary.simpleMessage("lista numerada"), + "quote": MessageLookupByLibrary.simpleMessage("citar"), + "strikethrough": MessageLookupByLibrary.simpleMessage("tachado"), + "text": MessageLookupByLibrary.simpleMessage("texto"), + "underline": MessageLookupByLibrary.simpleMessage("sublinhado") }; } diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/l10n.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/l10n.dart index ae80f115a1..9ef7ddf4c9 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/l10n.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/l10n.dart @@ -220,6 +220,7 @@ class AppLocalizationDelegate return const [ Locale.fromSubtags(languageCode: 'en'), Locale.fromSubtags(languageCode: 'ca'), + Locale.fromSubtags(languageCode: 'cs', countryCode: 'CZ'), Locale.fromSubtags(languageCode: 'de', countryCode: 'DE'), Locale.fromSubtags(languageCode: 'es', countryCode: 'VE'), Locale.fromSubtags(languageCode: 'fr', countryCode: 'CA'), @@ -228,6 +229,7 @@ class AppLocalizationDelegate Locale.fromSubtags(languageCode: 'id', countryCode: 'ID'), Locale.fromSubtags(languageCode: 'it', countryCode: 'IT'), Locale.fromSubtags(languageCode: 'ja', countryCode: 'JP'), + Locale.fromSubtags(languageCode: 'nl', countryCode: 'NL'), Locale.fromSubtags(languageCode: 'pl', countryCode: 'PL'), Locale.fromSubtags(languageCode: 'pt', countryCode: 'BR'), Locale.fromSubtags(languageCode: 'pt', countryCode: 'PT'), diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/link_menu/link_menu.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/link_menu/link_menu.dart index 13396a33c4..3a1785391b 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/link_menu/link_menu.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/link_menu/link_menu.dart @@ -31,6 +31,7 @@ class _LinkMenuState extends State { void initState() { super.initState(); _textEditingController.text = widget.linkText ?? ''; + _focusNode.requestFocus(); _focusNode.addListener(_onFocusChange); } diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/checkbox_text.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/checkbox_text.dart index 2ca7531d2b..10b17d6b36 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/checkbox_text.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/checkbox_text.dart @@ -1,15 +1,8 @@ -import 'package:appflowy_editor/src/document/built_in_attribute_keys.dart'; -import 'package:appflowy_editor/src/document/node.dart'; -import 'package:appflowy_editor/src/editor_state.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_editor/src/commands/format_built_in_text.dart'; import 'package:appflowy_editor/src/infra/flowy_svg.dart'; import 'package:appflowy_editor/src/render/rich_text/built_in_text_widget.dart'; -import 'package:appflowy_editor/src/render/rich_text/default_selectable.dart'; -import 'package:appflowy_editor/src/render/rich_text/flowy_rich_text.dart'; -import 'package:appflowy_editor/src/render/selection/selectable.dart'; -import 'package:appflowy_editor/src/service/default_text_operations/format_rich_text_style.dart'; -import 'package:appflowy_editor/src/service/render_plugin_service.dart'; -import 'package:appflowy_editor/src/extensions/attributes_extension.dart'; import 'package:appflowy_editor/src/extensions/text_style_extension.dart'; import 'package:flutter/material.dart'; @@ -81,8 +74,12 @@ class _CheckboxNodeWidgetState extends State padding: iconPadding, name: check ? 'check' : 'uncheck', ), - onTap: () { - formatCheckbox(widget.editorState, !check); + onTap: () async { + await formatTextToCheckbox( + widget.editorState, + !check, + textNode: widget.textNode, + ); }, ), Flexible( diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/flowy_rich_text.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/flowy_rich_text.dart index 7dba4852ed..99a6d08918 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/flowy_rich_text.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/flowy_rich_text.dart @@ -18,6 +18,8 @@ import 'package:appflowy_editor/src/extensions/attributes_extension.dart'; import 'package:appflowy_editor/src/render/selection/selectable.dart'; import 'package:appflowy_editor/src/render/toolbar/toolbar_item.dart'; +const _kRichTextDebugMode = false; + typedef FlowyTextSpanDecorator = TextSpan Function(TextSpan textSpan); class FlowyRichText extends StatefulWidget { @@ -261,6 +263,17 @@ class _FlowyRichTextState extends State with SelectableMixin { ), ); } + if (_kRichTextDebugMode) { + textSpans.add( + TextSpan( + text: '${widget.textNode.path}', + style: const TextStyle( + backgroundColor: Colors.red, + fontSize: 16.0, + ), + ), + ); + } return TextSpan( children: textSpans, ); diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_item_widget.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_item_widget.dart index 3b7307f039..a4322c59fe 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_item_widget.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_item_widget.dart @@ -37,7 +37,7 @@ class SelectionMenuItemWidget extends StatelessWidget { : MaterialStateProperty.all(Colors.transparent), ), label: Text( - item.name, + item.name(), textAlign: TextAlign.left, style: const TextStyle( color: Colors.black, diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_service.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_service.dart index 8d3d1e3453..4da824db80 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_service.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_service.dart @@ -124,7 +124,7 @@ List get defaultSelectionMenuItems => _defaultSelectionMenuItems; final List _defaultSelectionMenuItems = [ SelectionMenuItem( - name: AppFlowyEditorLocalizations.current.text, + name: () => AppFlowyEditorLocalizations.current.text, icon: _selectionMenuIcon('text'), keywords: ['text'], handler: (editorState, _, __) { @@ -132,7 +132,7 @@ final List _defaultSelectionMenuItems = [ }, ), SelectionMenuItem( - name: AppFlowyEditorLocalizations.current.heading1, + name: () => AppFlowyEditorLocalizations.current.heading1, icon: _selectionMenuIcon('h1'), keywords: ['heading 1, h1'], handler: (editorState, _, __) { @@ -140,7 +140,7 @@ final List _defaultSelectionMenuItems = [ }, ), SelectionMenuItem( - name: AppFlowyEditorLocalizations.current.heading2, + name: () => AppFlowyEditorLocalizations.current.heading2, icon: _selectionMenuIcon('h2'), keywords: ['heading 2, h2'], handler: (editorState, _, __) { @@ -148,7 +148,7 @@ final List _defaultSelectionMenuItems = [ }, ), SelectionMenuItem( - name: AppFlowyEditorLocalizations.current.heading3, + name: () => AppFlowyEditorLocalizations.current.heading3, icon: _selectionMenuIcon('h3'), keywords: ['heading 3, h3'], handler: (editorState, _, __) { @@ -156,13 +156,13 @@ final List _defaultSelectionMenuItems = [ }, ), SelectionMenuItem( - name: AppFlowyEditorLocalizations.current.image, + name: () => AppFlowyEditorLocalizations.current.image, icon: _selectionMenuIcon('image'), keywords: ['image'], handler: showImageUploadMenu, ), SelectionMenuItem( - name: AppFlowyEditorLocalizations.current.bulletedList, + name: () => AppFlowyEditorLocalizations.current.bulletedList, icon: _selectionMenuIcon('bulleted_list'), keywords: ['bulleted list', 'list', 'unordered list'], handler: (editorState, _, __) { @@ -170,7 +170,15 @@ final List _defaultSelectionMenuItems = [ }, ), SelectionMenuItem( - name: AppFlowyEditorLocalizations.current.checkbox, + name: () => AppFlowyEditorLocalizations.current.numberedList, + icon: _selectionMenuIcon('number'), + keywords: ['numbered list', 'list', 'ordered list'], + handler: (editorState, _, __) { + insertNumberedListAfterSelection(editorState); + }, + ), + SelectionMenuItem( + name: () => AppFlowyEditorLocalizations.current.checkbox, icon: _selectionMenuIcon('checkbox'), keywords: ['todo list', 'list', 'checkbox list'], handler: (editorState, _, __) { @@ -178,7 +186,7 @@ final List _defaultSelectionMenuItems = [ }, ), SelectionMenuItem( - name: AppFlowyEditorLocalizations.current.quote, + name: () => AppFlowyEditorLocalizations.current.quote, icon: _selectionMenuIcon('quote'), keywords: ['quote', 'refer'], handler: (editorState, _, __) { diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_widget.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_widget.dart index 1553085349..c5a35ef73c 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_widget.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_widget.dart @@ -6,27 +6,54 @@ import 'package:appflowy_editor/src/render/selection_menu/selection_menu_service import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +typedef SelectionMenuItemHandler = void Function( + EditorState editorState, + SelectionMenuService menuService, + BuildContext context, + ); + /// Selection Menu Item class SelectionMenuItem { SelectionMenuItem({ required this.name, required this.icon, required this.keywords, - required this.handler, - }); + required SelectionMenuItemHandler handler, + }) { + this.handler = (editorState, menuService, context) { + _deleteToSlash(editorState); + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + handler(editorState, menuService, context); + }); + }; + } - final String name; + final String Function() name; final Widget icon; /// Customizes keywords for item. /// /// The keywords are used to quickly retrieve items. final List keywords; - final void Function( - EditorState editorState, - SelectionMenuService menuService, - BuildContext context, - ) handler; + late final SelectionMenuItemHandler handler; + + void _deleteToSlash(EditorState editorState) { + final selectionService = editorState.service.selectionService; + final selection = selectionService.currentSelection.value; + final nodes = selectionService.currentSelectedNodes; + if (selection != null && nodes.length == 1) { + final node = nodes.first as TextNode; + final end = selection.start.offset; + final start = node.toRawString().substring(0, end).lastIndexOf('/'); + TransactionBuilder(editorState) + ..deleteText( + node, + start, + selection.start.offset - start, + ) + ..commit(); + } + } } class SelectionMenuWidget extends StatefulWidget { @@ -204,11 +231,8 @@ class _SelectionMenuWidgetState extends State { if (event.logicalKey == LogicalKeyboardKey.enter) { if (0 <= _selectedIndex && _selectedIndex < _showingItems.length) { - _deleteLastCharacters(length: keyword.length + 1); - WidgetsBinding.instance.addPostFrameCallback((timeStamp) { - _showingItems[_selectedIndex] - .handler(widget.editorState, widget.menuService, context); - }); + _showingItems[_selectedIndex] + .handler(widget.editorState, widget.menuService, context); return KeyEventResult.handled; } } else if (event.logicalKey == LogicalKeyboardKey.escape) { diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item.dart index 68bb5023ca..6407f81cb3 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item.dart @@ -1,4 +1,5 @@ import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_editor/src/commands/format_built_in_text.dart'; import 'package:appflowy_editor/src/extensions/url_launcher_extension.dart'; import 'package:appflowy_editor/src/infra/flowy_svg.dart'; import 'package:appflowy_editor/src/render/link_menu/link_menu.dart'; @@ -345,11 +346,8 @@ void showLinkMenu( onOpenLink: () async { await safeLaunchUrl(linkText); }, - onSubmitted: (text) { - TransactionBuilder(editorState) - ..formatText( - textNode, index, length, {BuiltInAttributeKey.href: text}) - ..commit(); + onSubmitted: (text) async { + await formatLinkInText(editorState, text, textNode: textNode); _dismissLinkMenu(); }, onCopyLink: () { @@ -377,6 +375,7 @@ void showLinkMenu( Overlay.of(context)?.insert(_linkMenuOverlay!); editorState.service.scrollService?.disable(); + editorState.service.keyboardService?.disable(); editorState.service.selectionService.currentSelection .addListener(_dismissLinkMenu); } diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/default_text_operations/format_rich_text_style.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/default_text_operations/format_rich_text_style.dart index 23ddc75a69..adf45bec65 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/default_text_operations/format_rich_text_style.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/default_text_operations/format_rich_text_style.dart @@ -34,6 +34,13 @@ void insertBulletedListAfterSelection(EditorState editorState) { }); } +void insertNumberedListAfterSelection(EditorState editorState) { + insertTextNodeAfterSelection(editorState, { + BuiltInAttributeKey.subtype: BuiltInAttributeKey.numberList, + BuiltInAttributeKey.number: 1, + }); +} + bool insertTextNodeAfterSelection( EditorState editorState, Attributes attributes) { final selection = editorState.service.selectionService.currentSelection.value; @@ -103,13 +110,17 @@ bool formatTextNodes(EditorState editorState, Attributes attributes) { final builder = TransactionBuilder(editorState); for (final textNode in textNodes) { + var newAttributes = {...textNode.attributes}; + for (final globalStyleKey in BuiltInAttributeKey.globalStyleKeys) { + if (newAttributes.keys.contains(globalStyleKey)) { + newAttributes[globalStyleKey] = null; + } + } + newAttributes.addAll(attributes); builder ..updateNode( textNode, - Attributes.fromIterable( - BuiltInAttributeKey.globalStyleKeys, - value: (_) => null, - )..addAll(attributes), + newAttributes, ) ..afterSelection = Selection.collapsed( Position( diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/editor_service.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/editor_service.dart index 7174290b9c..3d9599383d 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/editor_service.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/editor_service.dart @@ -72,6 +72,7 @@ class _AppFlowyEditorState extends State { editorState.selectionMenuItems = widget.selectionMenuItems; editorState.editorStyle = widget.editorStyle; editorState.service.renderPluginService = _createRenderPlugin(); + editorState.editable = widget.editable; } @override @@ -84,6 +85,7 @@ class _AppFlowyEditorState extends State { } editorState.editorStyle = widget.editorStyle; + editorState.editable = widget.editable; services = null; } diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/input_service.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/input_service.dart index 7f1a4718f5..d6a3420099 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/input_service.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/input_service.dart @@ -297,7 +297,11 @@ class _AppFlowyInputState extends State _updateCaretPosition(textNodes.first, selection); } } else { - // close(); + // https://github.com/flutter/flutter/issues/104944 + // Disable IME for the Web. + if (kIsWeb) { + close(); + } } } diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/markdown_syntax_to_styled_text.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/markdown_syntax_to_styled_text.dart new file mode 100644 index 0000000000..760d7e0b6c --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/markdown_syntax_to_styled_text.dart @@ -0,0 +1,243 @@ +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_editor/src/extensions/text_node_extensions.dart'; +import 'package:appflowy_editor/src/service/default_text_operations/format_rich_text_style.dart'; +import 'package:flutter/material.dart'; + +bool _isCodeStyle(TextNode textNode, int index) { + return textNode.allSatisfyCodeInSelection(Selection.single( + path: textNode.path, startOffset: index, endOffset: index + 1)); +} + +// enter escape mode when start two backquote +bool _isEscapeBackquote(String text, List backquoteIndexes) { + if (backquoteIndexes.length >= 2) { + final firstBackquoteIndex = backquoteIndexes[0]; + final secondBackquoteIndex = backquoteIndexes[1]; + return firstBackquoteIndex == secondBackquoteIndex - 1; + } + return false; +} + +// find all the index of `, exclusion in code style. +List _findBackquoteIndexes(String text, TextNode textNode) { + final backquoteIndexes = []; + for (var i = 0; i < text.length; i++) { + if (text[i] == '`' && _isCodeStyle(textNode, i) == false) { + backquoteIndexes.add(i); + } + } + return backquoteIndexes; +} + +/// To denote a word or phrase as code, enclose it in backticks (`). +/// If the word or phrase you want to denote as code includes one or more +/// backticks, you can escape it by enclosing the word or phrase in double +/// backticks (``). +ShortcutEventHandler backquoteToCodeHandler = (editorState, event) { + final selectionService = editorState.service.selectionService; + final selection = selectionService.currentSelection.value; + final textNodes = selectionService.currentSelectedNodes.whereType(); + + if (selection == null || !selection.isSingle || textNodes.length != 1) { + return KeyEventResult.ignored; + } + + final textNode = textNodes.first; + final selectionText = textNode + .toRawString() + .substring(selection.start.offset, selection.end.offset); + + // toggle code style when selected some text + if (selectionText.isNotEmpty) { + formatEmbedCode(editorState); + return KeyEventResult.handled; + } + + final text = textNode.toRawString().substring(0, selection.end.offset); + final backquoteIndexes = _findBackquoteIndexes(text, textNode); + if (backquoteIndexes.isEmpty) { + return KeyEventResult.ignored; + } + + final endIndex = selection.end.offset; + + if (_isEscapeBackquote(text, backquoteIndexes)) { + final firstBackquoteIndex = backquoteIndexes[0]; + final secondBackquoteIndex = backquoteIndexes[1]; + final lastBackquoteIndex = backquoteIndexes[backquoteIndexes.length - 1]; + if (secondBackquoteIndex == lastBackquoteIndex || + secondBackquoteIndex == lastBackquoteIndex - 1 || + lastBackquoteIndex != endIndex - 1) { + // ``(`),```(`),``...`...(`) should ignored + return KeyEventResult.ignored; + } + + TransactionBuilder(editorState) + ..deleteText(textNode, lastBackquoteIndex, 1) + ..deleteText(textNode, firstBackquoteIndex, 2) + ..formatText( + textNode, + firstBackquoteIndex, + endIndex - firstBackquoteIndex - 3, + { + BuiltInAttributeKey.code: true, + }, + ) + ..afterSelection = Selection.collapsed( + Position( + path: textNode.path, + offset: endIndex - 3, + ), + ) + ..commit(); + + return KeyEventResult.handled; + } + + // handle single backquote + final startIndex = backquoteIndexes[0]; + if (startIndex == endIndex - 1) { + return KeyEventResult.ignored; + } + + // delete the backquote. + // update the style of the text surround by ` ` to code. + // and update the cursor position. + TransactionBuilder(editorState) + ..deleteText(textNode, startIndex, 1) + ..formatText( + textNode, + startIndex, + endIndex - startIndex - 1, + { + BuiltInAttributeKey.code: true, + }, + ) + ..afterSelection = Selection.collapsed( + Position( + path: textNode.path, + offset: endIndex - 1, + ), + ) + ..commit(); + + return KeyEventResult.handled; +}; + +// convert ~~abc~~ to strikethrough abc. +ShortcutEventHandler doubleTildeToStrikethrough = (editorState, event) { + final selectionService = editorState.service.selectionService; + final selection = selectionService.currentSelection.value; + final textNodes = selectionService.currentSelectedNodes.whereType(); + if (selection == null || !selection.isSingle || textNodes.length != 1) { + return KeyEventResult.ignored; + } + + final textNode = textNodes.first; + final text = textNode.toRawString().substring(0, selection.end.offset); + + // make sure the last two characters are ~~. + if (text.length < 2 || text[selection.end.offset - 1] != '~') { + return KeyEventResult.ignored; + } + + // find all the index of `~`. + final tildeIndexes = []; + for (var i = 0; i < text.length; i++) { + if (text[i] == '~') { + tildeIndexes.add(i); + } + } + + if (tildeIndexes.length < 3) { + return KeyEventResult.ignored; + } + + // make sure the second to last and third to last tildes are connected. + final thirdToLastTildeIndex = tildeIndexes[tildeIndexes.length - 3]; + final secondToLastTildeIndex = tildeIndexes[tildeIndexes.length - 2]; + final lastTildeIndex = tildeIndexes[tildeIndexes.length - 1]; + if (secondToLastTildeIndex != thirdToLastTildeIndex + 1 || + lastTildeIndex == secondToLastTildeIndex + 1) { + return KeyEventResult.ignored; + } + + // delete the last three tildes. + // update the style of the text surround by `~~ ~~` to strikethrough. + // and update the cursor position. + TransactionBuilder(editorState) + ..deleteText(textNode, lastTildeIndex, 1) + ..deleteText(textNode, thirdToLastTildeIndex, 2) + ..formatText( + textNode, + thirdToLastTildeIndex, + selection.end.offset - thirdToLastTildeIndex - 2, + { + BuiltInAttributeKey.strikethrough: true, + }, + ) + ..afterSelection = Selection.collapsed( + Position( + path: textNode.path, + offset: selection.end.offset - 3, + ), + ) + ..commit(); + + return KeyEventResult.handled; +}; + +/// To create a link, enclose the link text in brackets (e.g., [link text]). +/// Then, immediately follow it with the URL in parentheses (e.g., (https://example.com)). +ShortcutEventHandler markdownLinkToLinkHandler = (editorState, event) { + final selectionService = editorState.service.selectionService; + final selection = selectionService.currentSelection.value; + final textNodes = selectionService.currentSelectedNodes.whereType(); + if (selection == null || !selection.isSingle || textNodes.length != 1) { + return KeyEventResult.ignored; + } + + // find all of the indexs for important characters + final textNode = textNodes.first; + final text = textNode.toRawString(); + final firstOpeningBracket = text.indexOf('['); + final firstClosingBracket = text.indexOf(']'); + + // use regex to validate the format of the link + // note: this enforces that the link has http or https + final regexp = RegExp(r'\[([\w\s\d]+)\]\(((?:\/|https?:\/\/)[\w\d./?=#]+)$'); + final match = regexp.firstMatch(text); + if (match == null) { + return KeyEventResult.ignored; + } + + // extract the text and the url of the link + final linkText = match.group(1); + final linkUrl = match.group(2); + + // Delete the initial opening bracket, + // update the href attribute of the text surrounded by [ ] to the url, + // delete everything after the text, + // and update the cursor position. + TransactionBuilder(editorState) + ..deleteText(textNode, firstOpeningBracket, 1) + ..formatText( + textNode, + firstOpeningBracket, + firstClosingBracket - firstOpeningBracket - 1, + { + BuiltInAttributeKey.href: linkUrl, + }, + ) + ..deleteText(textNode, firstClosingBracket - 1, + selection.end.offset - firstClosingBracket) + ..afterSelection = Selection.collapsed( + Position( + path: textNode.path, + offset: firstOpeningBracket + linkText!.length, + ), + ) + ..commit(); + + return KeyEventResult.handled; +}; diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/space_on_web_handler.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/space_on_web_handler.dart new file mode 100644 index 0000000000..6d942135ba --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/space_on_web_handler.dart @@ -0,0 +1,21 @@ +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_editor/src/commands/edit_text.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +ShortcutEventHandler spaceOnWebHandler = (editorState, event) { + final selection = editorState.service.selectionService.currentSelection.value; + final textNodes = editorState.service.selectionService.currentSelectedNodes + .whereType() + .toList(growable: false); + if (selection == null || + !selection.isCollapsed || + !kIsWeb || + textNodes.length != 1) { + return KeyEventResult.ignored; + } + + insertContextInText(editorState, selection.startIndex, ' '); + + return KeyEventResult.handled; +}; diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/shortcut_event/built_in_shortcut_events.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/shortcut_event/built_in_shortcut_events.dart index 38eb9ee7c5..21e3e90b0d 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/shortcut_event/built_in_shortcut_events.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/shortcut_event/built_in_shortcut_events.dart @@ -4,14 +4,17 @@ import 'package:appflowy_editor/src/service/internal_key_event_handlers/arrow_ke import 'package:appflowy_editor/src/service/internal_key_event_handlers/backspace_handler.dart'; import 'package:appflowy_editor/src/service/internal_key_event_handlers/copy_paste_handler.dart'; import 'package:appflowy_editor/src/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart'; +import 'package:appflowy_editor/src/service/internal_key_event_handlers/markdown_syntax_to_styled_text.dart'; import 'package:appflowy_editor/src/service/internal_key_event_handlers/page_up_down_handler.dart'; import 'package:appflowy_editor/src/service/internal_key_event_handlers/redo_undo_handler.dart'; import 'package:appflowy_editor/src/service/internal_key_event_handlers/select_all_handler.dart'; import 'package:appflowy_editor/src/service/internal_key_event_handlers/slash_handler.dart'; import 'package:appflowy_editor/src/service/internal_key_event_handlers/format_style_handler.dart'; +import 'package:appflowy_editor/src/service/internal_key_event_handlers/space_on_web_handler.dart'; import 'package:appflowy_editor/src/service/internal_key_event_handlers/tab_handler.dart'; import 'package:appflowy_editor/src/service/internal_key_event_handlers/whitespace_handler.dart'; import 'package:appflowy_editor/src/service/shortcut_event/shortcut_event.dart'; +import 'package:flutter/foundation.dart'; // List builtInShortcutEvents = [ @@ -249,4 +252,29 @@ List builtInShortcutEvents = [ command: 'tab', handler: tabHandler, ), + ShortcutEvent( + key: 'Backquote to code', + command: 'backquote', + handler: backquoteToCodeHandler, + ), + ShortcutEvent( + key: 'Double tilde to strikethrough', + command: 'shift+tilde', + handler: doubleTildeToStrikethrough, + ), + ShortcutEvent( + key: 'Markdown link to link', + command: 'shift+parenthesis right', + handler: markdownLinkToLinkHandler, + ), + // https://github.com/flutter/flutter/issues/104944 + // Workaround: Using space editing on the web platform often results in errors, + // so adding a shortcut event to handle the space input instead of using the + // `input_service`. + if (kIsWeb) + ShortcutEvent( + key: 'Space on the Web', + command: 'space', + handler: spaceOnWebHandler, + ), ]; diff --git a/frontend/app_flowy/packages/appflowy_editor/test/infra/test_raw_key_event.dart b/frontend/app_flowy/packages/appflowy_editor/test/infra/test_raw_key_event.dart index 5ad8ddfe3e..81fb46dff9 100644 --- a/frontend/app_flowy/packages/appflowy_editor/test/infra/test_raw_key_event.dart +++ b/frontend/app_flowy/packages/appflowy_editor/test/infra/test_raw_key_event.dart @@ -139,6 +139,9 @@ extension on LogicalKeyboardKey { if (this == LogicalKeyboardKey.keyZ) { return PhysicalKeyboardKey.keyZ; } + if (this == LogicalKeyboardKey.tilde) { + return PhysicalKeyboardKey.backquote; + } throw UnimplementedError(); } } diff --git a/frontend/app_flowy/packages/appflowy_editor/test/render/selection_menu/selection_menu_widget_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/render/selection_menu/selection_menu_widget_test.dart index 6ac10d593a..9f9046ae52 100644 --- a/frontend/app_flowy/packages/appflowy_editor/test/render/selection_menu/selection_menu_widget_test.dart +++ b/frontend/app_flowy/packages/appflowy_editor/test/render/selection_menu/selection_menu_widget_test.dart @@ -1,7 +1,6 @@ import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/src/render/selection_menu/selection_menu_item_widget.dart'; import 'package:appflowy_editor/src/render/selection_menu/selection_menu_service.dart'; -import 'package:appflowy_editor/src/render/selection_menu/selection_menu_widget.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import '../../infra/test_editor.dart'; @@ -12,29 +11,40 @@ void main() async { }); group('selection_menu_widget.dart', () { - // const i = defaultSelectionMenuItems.length; - // - // Because the `defaultSelectionMenuItems` uses localization, - // and the MaterialApp has not been initialized at the time of getting the value, - // it will crash. - // - // Use const value temporarily instead. - const i = 7; - testWidgets('Selects number.$i item in selection menu', (tester) async { - final editor = await _prepare(tester); - for (var j = 0; j < i; j++) { - await editor.pressLogicKey(LogicalKeyboardKey.arrowDown); - } + for (var i = 0; i < defaultSelectionMenuItems.length; i += 1) { + testWidgets('Selects number.$i item in selection menu with enter', + (tester) async { + final editor = await _prepare(tester); + for (var j = 0; j < i; j++) { + await editor.pressLogicKey(LogicalKeyboardKey.arrowDown); + } - await editor.pressLogicKey(LogicalKeyboardKey.enter); - expect( - find.byType(SelectionMenuWidget, skipOffstage: false), - findsNothing, - ); - if (defaultSelectionMenuItems[i].name != 'Image') { - await _testDefaultSelectionMenuItems(i, editor); - } - }); + await editor.pressLogicKey(LogicalKeyboardKey.enter); + expect( + find.byType(SelectionMenuWidget, skipOffstage: false), + findsNothing, + ); + if (defaultSelectionMenuItems[i].name() != 'Image') { + await _testDefaultSelectionMenuItems(i, editor); + } + }); + + testWidgets('Selects number.$i item in selection menu with click', + (tester) async { + final editor = await _prepare(tester); + + await tester.tap(find.byType(SelectionMenuItemWidget).at(i)); + await tester.pumpAndSettle(); + + expect( + find.byType(SelectionMenuWidget, skipOffstage: false), + findsNothing, + ); + if (defaultSelectionMenuItems[i].name() != 'Image') { + await _testDefaultSelectionMenuItems(i, editor); + } + }); + } testWidgets('Search item in selection menu util no results', (tester) async { @@ -48,7 +58,7 @@ void main() async { await editor.pressLogicKey(LogicalKeyboardKey.backspace); expect( find.byType(SelectionMenuItemWidget, skipOffstage: false), - findsNWidgets(4), + findsNWidgets(5), ); await editor.pressLogicKey(LogicalKeyboardKey.keyE); expect( @@ -137,23 +147,28 @@ Future _testDefaultSelectionMenuItems( int index, EditorWidgetTester editor) async { expect(editor.documentLength, 4); expect(editor.documentSelection, Selection.single(path: [2], startOffset: 0)); + expect((editor.nodeAtPath([1]) as TextNode).toRawString(), + 'Welcome to Appflowy 😁'); final node = editor.nodeAtPath([2]); final item = defaultSelectionMenuItems[index]; - if (item.name == 'Text') { + final itemName = item.name(); + if (itemName == 'Text') { expect(node?.subtype == null, true); - } else if (item.name == 'Heading 1') { + } else if (itemName == 'Heading 1') { expect(node?.subtype, BuiltInAttributeKey.heading); expect(node?.attributes.heading, BuiltInAttributeKey.h1); - } else if (item.name == 'Heading 2') { + } else if (itemName == 'Heading 2') { expect(node?.subtype, BuiltInAttributeKey.heading); expect(node?.attributes.heading, BuiltInAttributeKey.h2); - } else if (item.name == 'Heading 3') { + } else if (itemName == 'Heading 3') { expect(node?.subtype, BuiltInAttributeKey.heading); expect(node?.attributes.heading, BuiltInAttributeKey.h3); - } else if (item.name == 'Bulleted list') { + } else if (itemName == 'Bulleted list') { expect(node?.subtype, BuiltInAttributeKey.bulletedList); - } else if (item.name == 'Checkbox') { + } else if (itemName == 'Checkbox') { expect(node?.subtype, BuiltInAttributeKey.checkbox); expect(node?.attributes.check, false); + } else if (itemName == 'Quote') { + expect(node?.subtype, BuiltInAttributeKey.quote); } } diff --git a/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/markdown_syntax_to_styled_text_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/markdown_syntax_to_styled_text_test.dart new file mode 100644 index 0000000000..d0ca407ea1 --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/markdown_syntax_to_styled_text_test.dart @@ -0,0 +1,260 @@ +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_editor/src/extensions/text_node_extensions.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import '../../infra/test_editor.dart'; + +void main() async { + setUpAll(() { + TestWidgetsFlutterBinding.ensureInitialized(); + }); + + group('markdown_syntax_to_styled_text.dart', () { + group('convert single backquote to code', () { + Future insertBackquote( + EditorWidgetTester editor, { + int repeat = 1, + }) async { + for (var i = 0; i < repeat; i++) { + await editor.pressLogicKey( + LogicalKeyboardKey.backquote, + ); + } + } + + testWidgets('`AppFlowy` to code AppFlowy', (tester) async { + const text = '`AppFlowy'; + final editor = tester.editor..insertTextNode(''); + await editor.startTesting(); + await editor.updateSelection( + Selection.single(path: [0], startOffset: 0), + ); + final textNode = editor.nodeAtPath([0]) as TextNode; + for (var i = 0; i < text.length; i++) { + await editor.insertText(textNode, text[i], i); + } + await insertBackquote(editor); + final allCode = textNode.allSatisfyCodeInSelection( + Selection.single( + path: [0], + startOffset: 0, + endOffset: textNode.toRawString().length, + ), + ); + expect(allCode, true); + expect(textNode.toRawString(), 'AppFlowy'); + }); + + testWidgets('App`Flowy` to code AppFlowy', (tester) async { + const text = 'App`Flowy'; + final editor = tester.editor..insertTextNode(''); + await editor.startTesting(); + await editor.updateSelection( + Selection.single(path: [0], startOffset: 0), + ); + final textNode = editor.nodeAtPath([0]) as TextNode; + for (var i = 0; i < text.length; i++) { + await editor.insertText(textNode, text[i], i); + } + await insertBackquote(editor); + final allCode = textNode.allSatisfyCodeInSelection( + Selection.single( + path: [0], + startOffset: 3, + endOffset: textNode.toRawString().length, + ), + ); + expect(allCode, true); + expect(textNode.toRawString(), 'AppFlowy'); + }); + + testWidgets('`` nothing changes', (tester) async { + const text = '`'; + final editor = tester.editor..insertTextNode(''); + await editor.startTesting(); + await editor.updateSelection( + Selection.single(path: [0], startOffset: 0), + ); + final textNode = editor.nodeAtPath([0]) as TextNode; + for (var i = 0; i < text.length; i++) { + await editor.insertText(textNode, text[i], i); + } + await insertBackquote(editor); + final allCode = textNode.allSatisfyCodeInSelection( + Selection.single( + path: [0], + startOffset: 0, + endOffset: textNode.toRawString().length, + ), + ); + expect(allCode, false); + expect(textNode.toRawString(), text); + }); + }); + + group('convert double backquote to code', () { + Future insertBackquote( + EditorWidgetTester editor, { + int repeat = 1, + }) async { + for (var i = 0; i < repeat; i++) { + await editor.pressLogicKey( + LogicalKeyboardKey.backquote, + ); + } + } + + testWidgets('```AppFlowy`` to code `AppFlowy', (tester) async { + const text = '```AppFlowy`'; + final editor = tester.editor..insertTextNode(''); + await editor.startTesting(); + await editor.updateSelection( + Selection.single(path: [0], startOffset: 0), + ); + final textNode = editor.nodeAtPath([0]) as TextNode; + for (var i = 0; i < text.length; i++) { + await editor.insertText(textNode, text[i], i); + } + await insertBackquote(editor); + final allCode = textNode.allSatisfyCodeInSelection( + Selection.single( + path: [0], + startOffset: 1, + endOffset: textNode.toRawString().length, + ), + ); + expect(allCode, true); + expect(textNode.toRawString(), '`AppFlowy'); + }); + + testWidgets('```` nothing changes', (tester) async { + const text = '```'; + final editor = tester.editor..insertTextNode(''); + await editor.startTesting(); + await editor.updateSelection( + Selection.single(path: [0], startOffset: 0), + ); + final textNode = editor.nodeAtPath([0]) as TextNode; + for (var i = 0; i < text.length; i++) { + await editor.insertText(textNode, text[i], i); + } + await insertBackquote(editor); + final allCode = textNode.allSatisfyCodeInSelection( + Selection.single( + path: [0], + startOffset: 0, + endOffset: textNode.toRawString().length, + ), + ); + expect(allCode, false); + expect(textNode.toRawString(), text); + }); + }); + + group('convert double tilde to strikethrough', () { + Future insertTilde( + EditorWidgetTester editor, { + int repeat = 1, + }) async { + for (var i = 0; i < repeat; i++) { + await editor.pressLogicKey( + LogicalKeyboardKey.tilde, + isShiftPressed: true, + ); + } + } + + testWidgets('~~AppFlowy~~ to strikethrough AppFlowy', (tester) async { + const text = '~~AppFlowy~'; + final editor = tester.editor..insertTextNode(''); + await editor.startTesting(); + await editor.updateSelection( + Selection.single(path: [0], startOffset: 0), + ); + final textNode = editor.nodeAtPath([0]) as TextNode; + for (var i = 0; i < text.length; i++) { + await editor.insertText(textNode, text[i], i); + } + await insertTilde(editor); + final allStrikethrough = textNode.allSatisfyStrikethroughInSelection( + Selection.single( + path: [0], + startOffset: 0, + endOffset: textNode.toRawString().length, + ), + ); + expect(allStrikethrough, true); + expect(textNode.toRawString(), 'AppFlowy'); + }); + + testWidgets('App~~Flowy~~ to strikethrough AppFlowy', (tester) async { + const text = 'App~~Flowy~'; + final editor = tester.editor..insertTextNode(''); + await editor.startTesting(); + await editor.updateSelection( + Selection.single(path: [0], startOffset: 0), + ); + final textNode = editor.nodeAtPath([0]) as TextNode; + for (var i = 0; i < text.length; i++) { + await editor.insertText(textNode, text[i], i); + } + await insertTilde(editor); + final allStrikethrough = textNode.allSatisfyStrikethroughInSelection( + Selection.single( + path: [0], + startOffset: 3, + endOffset: textNode.toRawString().length, + ), + ); + expect(allStrikethrough, true); + expect(textNode.toRawString(), 'AppFlowy'); + }); + + testWidgets('~~~AppFlowy~~ to bold ~AppFlowy', (tester) async { + const text = '~~~AppFlowy~'; + final editor = tester.editor..insertTextNode(''); + await editor.startTesting(); + await editor.updateSelection( + Selection.single(path: [0], startOffset: 0), + ); + final textNode = editor.nodeAtPath([0]) as TextNode; + for (var i = 0; i < text.length; i++) { + await editor.insertText(textNode, text[i], i); + } + await insertTilde(editor); + final allStrikethrough = textNode.allSatisfyStrikethroughInSelection( + Selection.single( + path: [0], + startOffset: 1, + endOffset: textNode.toRawString().length, + ), + ); + expect(allStrikethrough, true); + expect(textNode.toRawString(), '~AppFlowy'); + }); + + testWidgets('~~~~ nothing changes', (tester) async { + const text = '~~~'; + final editor = tester.editor..insertTextNode(''); + await editor.startTesting(); + await editor.updateSelection( + Selection.single(path: [0], startOffset: 0), + ); + final textNode = editor.nodeAtPath([0]) as TextNode; + for (var i = 0; i < text.length; i++) { + await editor.insertText(textNode, text[i], i); + } + await insertTilde(editor); + final allStrikethrough = textNode.allSatisfyStrikethroughInSelection( + Selection.single( + path: [0], + startOffset: 0, + endOffset: textNode.toRawString().length, + ), + ); + expect(allStrikethrough, false); + expect(textNode.toRawString(), text); + }); + }); + }); +} diff --git a/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/space_on_web_handler_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/space_on_web_handler_test.dart new file mode 100644 index 0000000000..fbbe016d30 --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/space_on_web_handler_test.dart @@ -0,0 +1,45 @@ +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import '../../infra/test_editor.dart'; + +void main() async { + setUpAll(() { + TestWidgetsFlutterBinding.ensureInitialized(); + }); + + group('space_on_web_handler.dart', () { + testWidgets('Presses space key on web', (tester) async { + if (!kIsWeb) return; + const count = 10; + const text = 'Welcome to Appflowy 😁'; + final editor = tester.editor; + for (var i = 0; i < count; i++) { + editor.insertTextNode(text); + } + await editor.startTesting(); + + for (var i = 0; i < count; i++) { + await editor.updateSelection( + Selection.single(path: [i], startOffset: 1), + ); + await editor.pressLogicKey(LogicalKeyboardKey.space); + expect( + (editor.nodeAtPath([i]) as TextNode).toRawString(), + 'W elcome to Appflowy 😁', + ); + } + for (var i = 0; i < count; i++) { + await editor.updateSelection( + Selection.single(path: [i], startOffset: text.length + 1), + ); + await editor.pressLogicKey(LogicalKeyboardKey.space); + expect( + (editor.nodeAtPath([i]) as TextNode).toRawString(), + 'W elcome to Appflowy 😁 ', + ); + } + }); + }); +} diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index 66d576a455..4abc6f80ef 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -1444,9 +1444,9 @@ checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7" [[package]] name = "hashbrown" -version = "0.11.2" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "heck" @@ -1610,9 +1610,9 @@ checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" [[package]] name = "indexmap" -version = "1.8.1" +version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f647032dfaa1f8b6dc29bd3edb7bbef4861b8b8007ebb118d6db284fd59f6ee" +checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" dependencies = [ "autocfg", "hashbrown", diff --git a/frontend/rust-lib/flowy-grid/Cargo.toml b/frontend/rust-lib/flowy-grid/Cargo.toml index 709359560b..b52bb4582d 100644 --- a/frontend/rust-lib/flowy-grid/Cargo.toml +++ b/frontend/rust-lib/flowy-grid/Cargo.toml @@ -34,7 +34,7 @@ rayon = "1.5.2" serde = { version = "1.0", features = ["derive"] } serde_json = {version = "1.0"} serde_repr = "0.1" -indexmap = {version = "1.8.1", features = ["serde"]} +indexmap = {version = "1.9.1", features = ["serde"]} fancy-regex = "0.10.0" regex = "1.5.6" url = { version = "2"} diff --git a/frontend/rust-lib/flowy-grid/src/entities/cell_entities.rs b/frontend/rust-lib/flowy-grid/src/entities/cell_entities.rs index 3493f0940c..48886f1918 100644 --- a/frontend/rust-lib/flowy-grid/src/entities/cell_entities.rs +++ b/frontend/rust-lib/flowy-grid/src/entities/cell_entities.rs @@ -1,3 +1,4 @@ +use crate::entities::FieldType; use flowy_derive::ProtoBuf; use flowy_error::ErrorCode; use flowy_grid_data_model::parser::NotEmptyStr; @@ -74,15 +75,20 @@ pub struct GridCellPB { #[pb(index = 1)] pub field_id: String, + // The data was encoded in field_type's data type #[pb(index = 2)] pub data: Vec, + + #[pb(index = 3, one_of)] + pub field_type: Option, } impl GridCellPB { - pub fn new(field_id: &str, data: Vec) -> Self { + pub fn new(field_id: &str, field_type: FieldType, data: Vec) -> Self { Self { field_id: field_id.to_owned(), data, + field_type: Some(field_type), } } @@ -90,6 +96,7 @@ impl GridCellPB { Self { field_id: field_id.to_owned(), data: vec![], + field_type: None, } } } diff --git a/frontend/rust-lib/flowy-grid/src/entities/group_entities/group_changeset.rs b/frontend/rust-lib/flowy-grid/src/entities/group_entities/group_changeset.rs index 63b883d570..b6c9aafaff 100644 --- a/frontend/rust-lib/flowy-grid/src/entities/group_entities/group_changeset.rs +++ b/frontend/rust-lib/flowy-grid/src/entities/group_entities/group_changeset.rs @@ -98,6 +98,7 @@ pub struct MoveGroupPayloadPB { pub to_group_id: String, } +#[derive(Debug)] pub struct MoveGroupParams { pub view_id: String, pub from_group_id: String, diff --git a/frontend/rust-lib/flowy-grid/src/services/cell/cell_operation.rs b/frontend/rust-lib/flowy-grid/src/services/cell/cell_operation.rs index 0c657b8fd1..43b2eb420d 100644 --- a/frontend/rust-lib/flowy-grid/src/services/cell/cell_operation.rs +++ b/frontend/rust-lib/flowy-grid/src/services/cell/cell_operation.rs @@ -24,6 +24,13 @@ pub trait CellDisplayable { decoded_field_type: &FieldType, field_rev: &FieldRevision, ) -> FlowyResult; + + fn display_string( + &self, + cell_data: CellData, + decoded_field_type: &FieldType, + field_rev: &FieldRevision, + ) -> FlowyResult; } // CD: Short for CellData. This type is the type return by apply_changeset function. @@ -84,16 +91,16 @@ pub fn apply_cell_data_changeset>( pub fn decode_any_cell_data + Debug>( data: T, field_rev: &FieldRevision, -) -> CellBytes { +) -> (FieldType, CellBytes) { + let to_field_type = field_rev.ty.into(); match data.try_into() { Ok(any_cell_data) => { let AnyCellData { data, field_type } = any_cell_data; - let to_field_type = field_rev.ty.into(); match try_decode_cell_data(data.into(), &field_type, &to_field_type, field_rev) { - Ok(cell_bytes) => cell_bytes, + Ok(cell_bytes) => (field_type, cell_bytes), Err(e) => { tracing::error!("Decode cell data failed, {:?}", e); - CellBytes::default() + (field_type, CellBytes::default()) } } } @@ -101,12 +108,58 @@ pub fn decode_any_cell_data + Debug> // It's okay to ignore this error, because it's okay that the current cell can't // display the existing cell data. For example, the UI of the text cell will be blank if // the type of the data of cell is Number. - CellBytes::default() + + (to_field_type, CellBytes::default()) } } } -/// Use the `to_field_type`'s TypeOption to parse the cell data into `from_field_type`'s data. +pub fn decode_cell_data_to_string( + cell_data: CellData, + from_field_type: &FieldType, + to_field_type: &FieldType, + field_rev: &FieldRevision, +) -> FlowyResult { + let cell_data = cell_data.try_into_inner()?; + let get_cell_display_str = || { + let field_type: FieldTypeRevision = to_field_type.into(); + let result = match to_field_type { + FieldType::RichText => field_rev + .get_type_option::(field_type)? + .display_string(cell_data.into(), from_field_type, field_rev), + FieldType::Number => field_rev + .get_type_option::(field_type)? + .display_string(cell_data.into(), from_field_type, field_rev), + FieldType::DateTime => field_rev + .get_type_option::(field_type)? + .display_string(cell_data.into(), from_field_type, field_rev), + FieldType::SingleSelect => field_rev + .get_type_option::(field_type)? + .display_string(cell_data.into(), from_field_type, field_rev), + FieldType::MultiSelect => field_rev + .get_type_option::(field_type)? + .display_string(cell_data.into(), from_field_type, field_rev), + FieldType::Checkbox => field_rev + .get_type_option::(field_type)? + .display_string(cell_data.into(), from_field_type, field_rev), + FieldType::URL => field_rev + .get_type_option::(field_type)? + .display_string(cell_data.into(), from_field_type, field_rev), + }; + Some(result) + }; + + match get_cell_display_str() { + Some(Ok(s)) => Ok(s), + Some(Err(err)) => { + tracing::error!("{:?}", err); + Ok("".to_owned()) + } + None => Ok("".to_owned()), + } +} + +/// Use the `to_field_type`'s TypeOption to parse the cell data into `from_field_type` type's data. /// /// Each `FieldType` has its corresponding `TypeOption` that implements the `CellDisplayable` /// and `CellDataOperation` traits. diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/checkbox_type_option/checkbox_type_option.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/checkbox_type_option/checkbox_type_option.rs index fbb1ba64e8..e6d7b39d48 100644 --- a/frontend/rust-lib/flowy-grid/src/services/field/type_options/checkbox_type_option/checkbox_type_option.rs +++ b/frontend/rust-lib/flowy-grid/src/services/field/type_options/checkbox_type_option/checkbox_type_option.rs @@ -48,6 +48,16 @@ impl CellDisplayable for CheckboxTypeOptionPB { let cell_data = cell_data.try_into_inner()?; Ok(CellBytes::new(cell_data)) } + + fn display_string( + &self, + cell_data: CellData, + _decoded_field_type: &FieldType, + _field_rev: &FieldRevision, + ) -> FlowyResult { + let cell_data = cell_data.try_into_inner()?; + Ok(cell_data.to_string()) + } } impl CellDataOperation for CheckboxTypeOptionPB { diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/date_type_option/date_type_option.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/date_type_option/date_type_option.rs index e79ae3574c..f883790524 100644 --- a/frontend/rust-lib/flowy-grid/src/services/field/type_options/date_type_option/date_type_option.rs +++ b/frontend/rust-lib/flowy-grid/src/services/field/type_options/date_type_option/date_type_option.rs @@ -127,6 +127,17 @@ impl CellDisplayable for DateTypeOptionPB { let date_cell_data = self.today_desc_from_timestamp(timestamp); CellBytes::from(date_cell_data) } + + fn display_string( + &self, + cell_data: CellData, + _decoded_field_type: &FieldType, + _field_rev: &FieldRevision, + ) -> FlowyResult { + let timestamp = cell_data.try_into_inner()?; + let date_cell_data = self.today_desc_from_timestamp(timestamp); + Ok(date_cell_data.date) + } } impl CellDataOperation for DateTypeOptionPB { diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/number_type_option/number_type_option.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/number_type_option/number_type_option.rs index 984e7561bc..d35e812d77 100644 --- a/frontend/rust-lib/flowy-grid/src/services/field/type_options/number_type_option/number_type_option.rs +++ b/frontend/rust-lib/flowy-grid/src/services/field/type_options/number_type_option/number_type_option.rs @@ -1,6 +1,6 @@ use crate::entities::FieldType; use crate::impl_type_option; -use crate::services::cell::{CellBytes, CellData, CellDataChangeset, CellDataOperation}; +use crate::services::cell::{CellBytes, CellData, CellDataChangeset, CellDataOperation, CellDisplayable}; use crate::services::field::type_options::number_type_option::format::*; use crate::services::field::{BoxTypeOptionBuilder, NumberCellData, TypeOptionBuilder}; use bytes::Bytes; @@ -102,17 +102,13 @@ pub(crate) fn strip_currency_symbol(s: T) -> String { s } -impl CellDataOperation for NumberTypeOptionPB { - fn decode_cell_data( +impl CellDisplayable for NumberTypeOptionPB { + fn display_data( &self, cell_data: CellData, - decoded_field_type: &FieldType, + _decoded_field_type: &FieldType, _field_rev: &FieldRevision, ) -> FlowyResult { - if decoded_field_type.is_date() { - return Ok(CellBytes::default()); - } - let cell_data: String = cell_data.try_into_inner()?; match self.format_cell_data(&cell_data) { Ok(num) => Ok(CellBytes::new(num.to_string())), @@ -120,6 +116,31 @@ impl CellDataOperation for NumberTypeOptionPB { } } + fn display_string( + &self, + cell_data: CellData, + _decoded_field_type: &FieldType, + _field_rev: &FieldRevision, + ) -> FlowyResult { + let cell_data: String = cell_data.try_into_inner()?; + Ok(cell_data) + } +} + +impl CellDataOperation for NumberTypeOptionPB { + fn decode_cell_data( + &self, + cell_data: CellData, + decoded_field_type: &FieldType, + field_rev: &FieldRevision, + ) -> FlowyResult { + if decoded_field_type.is_date() { + return Ok(CellBytes::default()); + } + + self.display_data(cell_data, decoded_field_type, field_rev) + } + fn apply_changeset( &self, changeset: CellDataChangeset, diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/select_option.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/select_option.rs index ff579f8172..7d40903fe8 100644 --- a/frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/select_option.rs +++ b/frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/select_option.rs @@ -120,6 +120,21 @@ where ) -> FlowyResult { CellBytes::from(self.selected_select_option(cell_data)) } + + fn display_string( + &self, + cell_data: CellData, + _decoded_field_type: &FieldType, + _field_rev: &FieldRevision, + ) -> FlowyResult { + Ok(self + .selected_select_option(cell_data) + .select_options + .into_iter() + .map(|option| option.name) + .collect::>() + .join(SELECTION_IDS_SEPARATOR)) + } } pub fn select_option_operation(field_rev: &FieldRevision) -> FlowyResult> { diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/text_type_option/text_tests.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/text_type_option/text_tests.rs index 6e0073a0d0..2f2a5ed188 100644 --- a/frontend/rust-lib/flowy-grid/src/services/field/type_options/text_type_option/text_tests.rs +++ b/frontend/rust-lib/flowy-grid/src/services/field/type_options/text_type_option/text_tests.rs @@ -17,10 +17,10 @@ mod tests { type_option .decode_cell_data(1647251762.into(), &field_type, &field_rev) .unwrap() - .parser::() + .parser::() .unwrap() - .date, - "Mar 14,2022".to_owned() + .as_ref(), + "Mar 14,2022" ); } @@ -40,10 +40,10 @@ mod tests { type_option .decode_cell_data(option_id.into(), &field_type, &field_rev) .unwrap() - .parser::() + .parser::() .unwrap() - .select_options, - vec![done_option], + .to_string(), + done_option.name, ); } } diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/text_type_option/text_type_option.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/text_type_option/text_type_option.rs index 6636d2bcc7..ab33c1345b 100644 --- a/frontend/rust-lib/flowy-grid/src/services/field/type_options/text_type_option/text_type_option.rs +++ b/frontend/rust-lib/flowy-grid/src/services/field/type_options/text_type_option/text_type_option.rs @@ -1,8 +1,8 @@ use crate::entities::FieldType; use crate::impl_type_option; use crate::services::cell::{ - try_decode_cell_data, CellBytes, CellBytesParser, CellData, CellDataChangeset, CellDataOperation, CellDisplayable, - FromCellString, + decode_cell_data_to_string, CellBytes, CellBytesParser, CellData, CellDataChangeset, CellDataOperation, + CellDisplayable, FromCellString, }; use crate::services::field::{BoxTypeOptionBuilder, TypeOptionBuilder}; use bytes::Bytes; @@ -44,6 +44,16 @@ impl CellDisplayable for RichTextTypeOptionPB { let cell_str: String = cell_data.try_into_inner()?; Ok(CellBytes::new(cell_str)) } + + fn display_string( + &self, + cell_data: CellData, + _decoded_field_type: &FieldType, + _field_rev: &FieldRevision, + ) -> FlowyResult { + let cell_str: String = cell_data.try_into_inner()?; + Ok(cell_str) + } } impl CellDataOperation for RichTextTypeOptionPB { @@ -57,8 +67,10 @@ impl CellDataOperation for RichTextTypeOptionPB { || decoded_field_type.is_single_select() || decoded_field_type.is_multi_select() || decoded_field_type.is_number() + || decoded_field_type.is_url() { - try_decode_cell_data(cell_data, decoded_field_type, decoded_field_type, field_rev) + let s = decode_cell_data_to_string(cell_data, decoded_field_type, decoded_field_type, field_rev); + Ok(CellBytes::new(s.unwrap_or_else(|_| "".to_owned()))) } else { self.display_data(cell_data, decoded_field_type, field_rev) } @@ -85,6 +97,14 @@ impl AsRef for TextCellData { } } +impl std::ops::Deref for TextCellData { + type Target = String; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + impl FromCellString for TextCellData { fn from_cell_str(s: &str) -> FlowyResult where @@ -94,6 +114,12 @@ impl FromCellString for TextCellData { } } +impl ToString for TextCellData { + fn to_string(&self) -> String { + self.0.clone() + } +} + pub struct TextCellDataParser(); impl CellBytesParser for TextCellDataParser { type Object = TextCellData; diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/url_type_option/url_type_option.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/url_type_option/url_type_option.rs index d34dfc12d3..5e426f368f 100644 --- a/frontend/rust-lib/flowy-grid/src/services/field/type_options/url_type_option/url_type_option.rs +++ b/frontend/rust-lib/flowy-grid/src/services/field/type_options/url_type_option/url_type_option.rs @@ -42,6 +42,16 @@ impl CellDisplayable for URLTypeOptionPB { let cell_data: URLCellDataPB = cell_data.try_into_inner()?; CellBytes::from(cell_data) } + + fn display_string( + &self, + cell_data: CellData, + _decoded_field_type: &FieldType, + _field_rev: &FieldRevision, + ) -> FlowyResult { + let cell_data: URLCellDataPB = cell_data.try_into_inner()?; + Ok(cell_data.content) + } } impl CellDataOperation for URLTypeOptionPB { diff --git a/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs b/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs index ccad53bf5c..29f7b759b8 100644 --- a/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs +++ b/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs @@ -368,6 +368,7 @@ impl GridRevisionEditor { Ok(row_pb) } + #[tracing::instrument(level = "trace", skip_all, err)] pub async fn move_group(&self, params: MoveGroupParams) -> FlowyResult<()> { let _ = self.view_manager.move_group(params).await?; Ok(()) @@ -435,14 +436,18 @@ impl GridRevisionEditor { } pub async fn get_cell(&self, params: &GridCellIdParams) -> Option { - let cell_bytes = self.get_cell_bytes(params).await?; - Some(GridCellPB::new(¶ms.field_id, cell_bytes.to_vec())) + let (field_type, cell_bytes) = self.decode_any_cell_data(params).await?; + Some(GridCellPB::new(¶ms.field_id, field_type, cell_bytes.to_vec())) } pub async fn get_cell_bytes(&self, params: &GridCellIdParams) -> Option { + let (_, cell_data) = self.decode_any_cell_data(params).await?; + Some(cell_data) + } + + async fn decode_any_cell_data(&self, params: &GridCellIdParams) -> Option<(FieldType, CellBytes)> { let field_rev = self.get_field_rev(¶ms.field_id).await?; let row_rev = self.block_manager.get_row_rev(¶ms.row_id).await.ok()??; - let cell_rev = row_rev.cells.get(¶ms.field_id)?.clone(); Some(decode_any_cell_data(cell_rev.data, &field_rev)) } diff --git a/frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs b/frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs index c7736c6fdc..6316135f5a 100644 --- a/frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs +++ b/frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs @@ -173,6 +173,7 @@ impl GridViewRevisionEditor { Ok(groups.into_iter().map(GroupPB::from).collect()) } + #[tracing::instrument(level = "trace", skip(self), err)] pub(crate) async fn move_group(&self, params: MoveGroupParams) -> FlowyResult<()> { let _ = self .group_controller @@ -180,7 +181,7 @@ impl GridViewRevisionEditor { .await .move_group(¶ms.from_group_id, ¶ms.to_group_id)?; match self.group_controller.read().await.get_group(¶ms.from_group_id) { - None => {} + None => tracing::warn!("Can not find the group with id: {}", params.from_group_id), Some((index, group)) => { let inserted_group = InsertedGroupPB { group: GroupPB::from(group), @@ -228,7 +229,11 @@ impl GridViewRevisionEditor { let _ = self .modify(|pad| { let configuration = default_group_configuration(&field_rev); - let changeset = pad.insert_group(¶ms.field_id, ¶ms.field_type_rev, configuration)?; + let changeset = pad.insert_or_update_group_configuration( + ¶ms.field_id, + ¶ms.field_type_rev, + configuration, + )?; Ok(changeset) }) .await?; @@ -496,10 +501,11 @@ impl GroupConfigurationWriter for GroupConfigurationWriterImpl { let field_id = field_id.to_owned(); wrap_future(async move { - let changeset = view_pad - .write() - .await - .insert_group(&field_id, &field_type, group_configuration)?; + let changeset = view_pad.write().await.insert_or_update_group_configuration( + &field_id, + &field_type, + group_configuration, + )?; if let Some(changeset) = changeset { let _ = apply_change(&user_id, rev_manager, changeset).await?; diff --git a/frontend/rust-lib/flowy-grid/src/services/group/configuration.rs b/frontend/rust-lib/flowy-grid/src/services/group/configuration.rs index 07e5ba45d4..8f3df66727 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/configuration.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/configuration.rs @@ -1,5 +1,5 @@ use crate::entities::{GroupPB, GroupViewChangesetPB}; -use crate::services::group::{default_group_configuration, GeneratedGroup, Group}; +use crate::services::group::{default_group_configuration, make_default_group, GeneratedGroup, Group}; use flowy_error::{FlowyError, FlowyResult}; use flowy_grid_data_model::revision::{ FieldRevision, FieldTypeRevision, GroupConfigurationContentSerde, GroupConfigurationRevision, GroupRevision, @@ -29,10 +29,7 @@ impl std::fmt::Display for GroupContext { self.groups_map.iter().for_each(|(_, group)| { let _ = f.write_fmt(format_args!("Group:{} has {} rows \n", group.id, group.rows.len())); }); - let _ = f.write_fmt(format_args!( - "Default group has {} rows \n", - self.default_group.rows.len() - )); + Ok(()) } } @@ -44,7 +41,7 @@ pub struct GroupContext { field_rev: Arc, groups_map: IndexMap, /// default_group is used to store the rows that don't belong to any groups. - default_group: Group, + // default_group: Group, writer: Arc, } @@ -59,16 +56,6 @@ where reader: Arc, writer: Arc, ) -> FlowyResult { - let default_group_id = format!("{}_default_group", view_id); - let default_group = Group { - id: default_group_id, - field_id: field_rev.id.clone(), - name: format!("No {}", field_rev.name), - is_default: true, - is_visible: true, - rows: vec![], - filter_content: "".to_string(), - }; let configuration = match reader.get_configuration().await { None => { let default_configuration = default_group_configuration(&field_rev); @@ -80,24 +67,22 @@ where Some(configuration) => configuration, }; - // let configuration = C::from_configuration_content(&configuration_rev.content)?; Ok(Self { view_id, field_rev, groups_map: IndexMap::new(), - default_group, writer, configuration, configuration_content: PhantomData, }) } - pub(crate) fn get_default_group(&self) -> &Group { - &self.default_group + pub(crate) fn get_default_group(&self) -> Option<&Group> { + self.groups_map.get(&self.field_rev.id) } - pub(crate) fn get_mut_default_group(&mut self) -> &mut Group { - &mut self.default_group + pub(crate) fn get_mut_default_group(&mut self) -> Option<&mut Group> { + self.groups_map.get_mut(&self.field_rev.id) } /// Returns the groups without the default group @@ -122,8 +107,6 @@ where self.groups_map.iter_mut().for_each(|(_, group)| { each(group); }); - - each(&mut self.default_group); } pub(crate) fn move_group(&mut self, from_id: &str, to_id: &str) -> FlowyResult<()> { @@ -131,18 +114,23 @@ where let to_index = self.groups_map.get_index_of(to_id); match (from_index, to_index) { (Some(from_index), Some(to_index)) => { - self.groups_map.swap_indices(from_index, to_index); + self.groups_map.move_index(from_index, to_index); + self.mut_configuration(|configuration| { let from_index = configuration.groups.iter().position(|group| group.id == from_id); let to_index = configuration.groups.iter().position(|group| group.id == to_id); - if let (Some(from), Some(to)) = (from_index, to_index) { - configuration.groups.swap(from, to); + tracing::info!("Configuration groups: {:?} ", configuration.groups); + if let (Some(from), Some(to)) = &(from_index, to_index) { + tracing::trace!("Move group from index:{:?} to index:{:?}", from_index, to_index); + let group = configuration.groups.remove(*from); + configuration.groups.insert(*to, group); } - true + + from_index.is_some() && to_index.is_some() })?; Ok(()) } - _ => Err(FlowyError::out_of_bounds()), + _ => Err(FlowyError::record_not_found().context("Moving group failed. Groups are not exist")), } } @@ -150,7 +138,6 @@ where pub(crate) fn init_groups( &mut self, generated_groups: Vec, - reset: bool, ) -> FlowyResult> { let mut new_groups = vec![]; let mut filter_content_map = HashMap::new(); @@ -159,16 +146,17 @@ where new_groups.push(generate_group.group_rev); }); + let mut old_groups = self.configuration.groups.clone(); + if !old_groups.iter().any(|group| group.id == self.field_rev.id) { + old_groups.push(make_default_group(&self.field_rev)); + } + let MergeGroupResult { mut all_group_revs, new_group_revs, updated_group_revs: _, deleted_group_revs, - } = if reset { - merge_groups(&[], new_groups) - } else { - merge_groups(&self.configuration.groups, new_groups) - }; + } = merge_groups(old_groups, new_groups); let deleted_group_ids = deleted_group_revs .into_iter() @@ -197,31 +185,23 @@ where Some(pos) => { let mut old_group = configuration.groups.remove(pos); group_rev.update_with_other(&old_group); + is_changed = is_group_changed(group_rev, &old_group); - // Take the GroupRevision if the name has changed - if is_group_changed(group_rev, &old_group) { - old_group.name = group_rev.name.clone(); - is_changed = true; - configuration.groups.insert(pos, old_group); - } + old_group.name = group_rev.name.clone(); + configuration.groups.insert(pos, old_group); } } } is_changed })?; - // The len of the filter_content_map should equal to the len of the all_group_revs - debug_assert_eq!(filter_content_map.len(), all_group_revs.len()); all_group_revs.into_iter().for_each(|group_rev| { - if let Some(filter_content) = filter_content_map.get(&group_rev.id) { - let group = Group::new( - group_rev.id, - self.field_rev.id.clone(), - group_rev.name, - filter_content.clone(), - ); - self.groups_map.insert(group.id.clone(), group); - } + let filter_content = filter_content_map + .get(&group_rev.id) + .cloned() + .unwrap_or_else(|| "".to_owned()); + let group = Group::new(group_rev.id, self.field_rev.id.clone(), group_rev.name, filter_content); + self.groups_map.insert(group.id.clone(), group); }); let new_groups = new_group_revs @@ -269,6 +249,7 @@ where Ok(()) } + #[tracing::instrument(level = "trace", skip_all, err)] pub fn save_configuration(&self) -> FlowyResult<()> { let configuration = (&*self.configuration).clone(); let writer = self.writer.clone(); @@ -311,13 +292,14 @@ where } } -fn merge_groups(old_groups: &[GroupRevision], new_groups: Vec) -> MergeGroupResult { +fn merge_groups(old_groups: Vec, new_groups: Vec) -> MergeGroupResult { let mut merge_result = MergeGroupResult::new(); - if old_groups.is_empty() { - merge_result.all_group_revs = new_groups.clone(); - merge_result.new_group_revs = new_groups; - return merge_result; - } + // if old_groups.is_empty() { + // merge_result.all_group_revs.extend(new_groups.clone()); + // merge_result.all_group_revs.push(default_group); + // merge_result.new_group_revs = new_groups; + // return merge_result; + // } // group_map is a helper map is used to filter out the new groups. let mut new_group_map: IndexMap = IndexMap::new(); @@ -329,19 +311,20 @@ fn merge_groups(old_groups: &[GroupRevision], new_groups: Vec) -> for old in old_groups { if let Some(new) = new_group_map.remove(&old.id) { merge_result.all_group_revs.push(new.clone()); - if is_group_changed(&new, old) { + if is_group_changed(&new, &old) { merge_result.updated_group_revs.push(new); } } else { - merge_result.deleted_group_revs.push(old.clone()); + merge_result.all_group_revs.push(old); } } // Find out the new groups + new_group_map.reverse(); let new_groups = new_group_map.into_values(); for (_, group) in new_groups.into_iter().enumerate() { - merge_result.all_group_revs.push(group.clone()); - merge_result.new_group_revs.push(group); + merge_result.all_group_revs.insert(0, group.clone()); + merge_result.new_group_revs.insert(0, group); } merge_result } diff --git a/frontend/rust-lib/flowy-grid/src/services/group/controller.rs b/frontend/rust-lib/flowy-grid/src/services/group/controller.rs index f65b87e829..1a7bc8d218 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/controller.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/controller.rs @@ -88,7 +88,7 @@ where pub async fn new(field_rev: &Arc, mut configuration: GroupContext) -> FlowyResult { let type_option = field_rev.get_type_option::(field_rev.ty); let groups = G::generate_groups(&field_rev.id, &configuration, &type_option); - let _ = configuration.init_groups(groups, true)?; + let _ = configuration.init_groups(groups)?; Ok(Self { field_id: field_rev.id.clone(), @@ -105,8 +105,8 @@ where &mut self, row_rev: &RowRevision, other_group_changesets: &[GroupChangesetPB], - ) -> GroupChangesetPB { - let default_group = self.group_ctx.get_mut_default_group(); + ) -> Option { + let default_group = self.group_ctx.get_mut_default_group()?; // [other_group_inserted_row] contains all the inserted rows except the default group. let other_group_inserted_row = other_group_changesets @@ -163,7 +163,7 @@ where } default_group.rows.retain(|row| !deleted_row_ids.contains(&row.id)); changeset.deleted_rows.extend(deleted_row_ids); - changeset + Some(changeset) } } @@ -182,11 +182,14 @@ where fn groups(&self) -> Vec { if self.use_default_group() { - let mut groups: Vec = self.group_ctx.groups().into_iter().cloned().collect(); - groups.push(self.group_ctx.get_default_group().clone()); - groups - } else { self.group_ctx.groups().into_iter().cloned().collect() + } else { + self.group_ctx + .groups() + .into_iter() + .filter(|group| group.id != self.field_id) + .cloned() + .collect::>() } } @@ -205,7 +208,7 @@ where if let Some(cell_rev) = cell_rev { let mut grouped_rows: Vec = vec![]; - let cell_bytes = decode_any_cell_data(cell_rev.data, field_rev); + let cell_bytes = decode_any_cell_data(cell_rev.data, field_rev).1; let cell_data = cell_bytes.parser::

()?; for group in self.group_ctx.groups() { if self.can_group(&group.filter_content, &cell_data) { @@ -216,17 +219,18 @@ where } } - if grouped_rows.is_empty() { - self.group_ctx.get_mut_default_group().add_row(row_rev.into()); - } else { + if !grouped_rows.is_empty() { for group_row in grouped_rows { if let Some(group) = self.group_ctx.get_mut_group(&group_row.group_id) { group.add_row(group_row.row); } } + continue; } - } else { - self.group_ctx.get_mut_default_group().add_row(row_rev.into()); + } + match self.group_ctx.get_mut_default_group() { + None => {} + Some(default_group) => default_group.add_row(row_rev.into()), } } @@ -244,13 +248,14 @@ where field_rev: &FieldRevision, ) -> FlowyResult> { if let Some(cell_rev) = row_rev.cells.get(&self.field_id) { - let cell_bytes = decode_any_cell_data(cell_rev.data.clone(), field_rev); + let cell_bytes = decode_any_cell_data(cell_rev.data.clone(), field_rev).1; let cell_data = cell_bytes.parser::

()?; let mut changesets = self.add_row_if_match(row_rev, &cell_data); - let default_group_changeset = self.update_default_group(row_rev, &changesets); - tracing::trace!("default_group_changeset: {}", default_group_changeset); - if !default_group_changeset.is_empty() { - changesets.push(default_group_changeset); + if let Some(default_group_changeset) = self.update_default_group(row_rev, &changesets) { + tracing::trace!("default_group_changeset: {}", default_group_changeset); + if !default_group_changeset.is_empty() { + changesets.push(default_group_changeset); + } } Ok(changesets) } else { @@ -265,15 +270,16 @@ where ) -> FlowyResult> { // if the cell_rev is none, then the row must be crated from the default group. if let Some(cell_rev) = row_rev.cells.get(&self.field_id) { - let cell_bytes = decode_any_cell_data(cell_rev.data.clone(), field_rev); + let cell_bytes = decode_any_cell_data(cell_rev.data.clone(), field_rev).1; let cell_data = cell_bytes.parser::

()?; Ok(self.remove_row_if_match(row_rev, &cell_data)) - } else { - let group = self.group_ctx.get_default_group(); + } else if let Some(group) = self.group_ctx.get_default_group() { Ok(vec![GroupChangesetPB::delete( group.id.clone(), vec![row_rev.id.clone()], )]) + } else { + Ok(vec![]) } } @@ -285,7 +291,7 @@ where }; if let Some(cell_rev) = cell_rev { - let cell_bytes = decode_any_cell_data(cell_rev.data, context.field_rev); + let cell_bytes = decode_any_cell_data(cell_rev.data, context.field_rev).1; let cell_data = cell_bytes.parser::

()?; Ok(self.move_row(&cell_data, context)) } else { @@ -297,7 +303,7 @@ where fn did_update_field(&mut self, field_rev: &FieldRevision) -> FlowyResult> { let type_option = field_rev.get_type_option::(field_rev.ty); let groups = G::generate_groups(&field_rev.id, &self.group_ctx, &type_option); - let changeset = self.group_ctx.init_groups(groups, false)?; + let changeset = self.group_ctx.init_groups(groups)?; Ok(changeset) } } diff --git a/frontend/rust-lib/flowy-grid/src/services/group/entities.rs b/frontend/rust-lib/flowy-grid/src/services/group/entities.rs index c4687859f3..baa4842402 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/entities.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/entities.rs @@ -9,16 +9,17 @@ pub struct Group { pub is_visible: bool, pub(crate) rows: Vec, - /// [content] is used to determine which group the cell belongs to. + /// [filter_content] is used to determine which group the cell belongs to. pub filter_content: String, } impl Group { pub fn new(id: String, field_id: String, name: String, filter_content: String) -> Self { + let is_default = id == field_id; Self { id, field_id, - is_default: false, + is_default, is_visible: true, name, rows: vec![], diff --git a/frontend/rust-lib/flowy-grid/src/services/group/group_util.rs b/frontend/rust-lib/flowy-grid/src/services/group/group_util.rs index c15717e2a7..8a901fd869 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/group_util.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/group_util.rs @@ -8,8 +8,8 @@ use crate::services::group::{ use flowy_error::FlowyResult; use flowy_grid_data_model::revision::{ CheckboxGroupConfigurationRevision, DateGroupConfigurationRevision, FieldRevision, GroupConfigurationRevision, - LayoutRevision, NumberGroupConfigurationRevision, RowRevision, SelectOptionGroupConfigurationRevision, - TextGroupConfigurationRevision, UrlGroupConfigurationRevision, + GroupRevision, LayoutRevision, NumberGroupConfigurationRevision, RowRevision, + SelectOptionGroupConfigurationRevision, TextGroupConfigurationRevision, UrlGroupConfigurationRevision, }; use std::sync::Arc; @@ -79,7 +79,7 @@ pub fn default_group_configuration(field_rev: &FieldRevision) -> GroupConfigurat let field_id = field_rev.id.clone(); let field_type_rev = field_rev.ty; let field_type: FieldType = field_rev.ty.into(); - match field_type { + let mut group_configuration_rev = match field_type { FieldType::RichText => { GroupConfigurationRevision::new(field_id, field_type_rev, TextGroupConfigurationRevision::default()) .unwrap() @@ -112,5 +112,23 @@ pub fn default_group_configuration(field_rev: &FieldRevision) -> GroupConfigurat FieldType::URL => { GroupConfigurationRevision::new(field_id, field_type_rev, UrlGroupConfigurationRevision::default()).unwrap() } + }; + + // Append the no `status` group + let default_group_rev = GroupRevision { + id: field_rev.id.clone(), + name: format!("No {}", field_rev.name), + visible: true, + }; + + group_configuration_rev.groups.push(default_group_rev); + group_configuration_rev +} + +pub fn make_default_group(field_rev: &FieldRevision) -> GroupRevision { + GroupRevision { + id: field_rev.id.clone(), + name: format!("No {}", field_rev.name), + visible: true, } } diff --git a/frontend/rust-lib/flowy-grid/tests/grid/group_test/test.rs b/frontend/rust-lib/flowy-grid/tests/grid/group_test/test.rs index a205420498..42fdc6b61c 100644 --- a/frontend/rust-lib/flowy-grid/tests/grid/group_test/test.rs +++ b/frontend/rust-lib/flowy-grid/tests/grid/group_test/test.rs @@ -370,6 +370,28 @@ async fn group_move_group_test() { test.run_scripts(scripts).await; } +#[tokio::test] +async fn group_default_move_group_test() { + let mut test = GridGroupTest::new().await; + let group_0 = test.group_at_index(0).await; + let group_3 = test.group_at_index(3).await; + let scripts = vec![ + MoveGroup { + from_group_index: 3, + to_group_index: 0, + }, + AssertGroup { + group_index: 0, + expected_group: group_3, + }, + AssertGroup { + group_index: 1, + expected_group: group_0, + }, + ]; + test.run_scripts(scripts).await; +} + #[tokio::test] async fn group_insert_single_select_option_test() { let mut test = GridGroupTest::new().await; @@ -402,7 +424,7 @@ async fn group_group_by_other_field() { group_index: 1, row_count: 2, }, - AssertGroupCount(4), + AssertGroupCount(5), ]; test.run_scripts(scripts).await; } diff --git a/frontend/rust-lib/flowy-user/src/entities/user_setting.rs b/frontend/rust-lib/flowy-user/src/entities/user_setting.rs index 23c74b6d5f..1d4e77896b 100644 --- a/frontend/rust-lib/flowy-user/src/entities/user_setting.rs +++ b/frontend/rust-lib/flowy-user/src/entities/user_setting.rs @@ -1,5 +1,6 @@ use flowy_derive::ProtoBuf; use serde::{Deserialize, Serialize}; +use std::collections::HashMap; #[derive(ProtoBuf, Default, Debug, Clone)] pub struct UserPreferencesPB { @@ -21,7 +22,11 @@ pub struct AppearanceSettingsPB { #[pb(index = 3)] #[serde(default = "DEFAULT_RESET_VALUE")] - pub reset_as_default: bool, + pub reset_to_default: bool, + + #[pb(index = 4)] + #[serde(default)] + pub setting_key_value: HashMap, } const DEFAULT_RESET_VALUE: fn() -> bool = || APPEARANCE_RESET_AS_DEFAULT; @@ -52,7 +57,8 @@ impl std::default::Default for AppearanceSettingsPB { AppearanceSettingsPB { theme: APPEARANCE_DEFAULT_THEME.to_owned(), locale: LocaleSettingsPB::default(), - reset_as_default: APPEARANCE_RESET_AS_DEFAULT, + reset_to_default: APPEARANCE_RESET_AS_DEFAULT, + setting_key_value: HashMap::default(), } } } diff --git a/frontend/scripts/docker-buildfiles/Dockerfile b/frontend/scripts/docker-buildfiles/Dockerfile index a15a31d2e3..4c1ec1e3ea 100644 --- a/frontend/scripts/docker-buildfiles/Dockerfile +++ b/frontend/scripts/docker-buildfiles/Dockerfile @@ -47,7 +47,7 @@ RUN pacman -Syy && \ RUN xdg-user-dirs-update COPY --from=builder /usr/sbin/yay /usr/sbin/yay -RUN yay -S --noconfirm gtk3 +RUN yay -S --noconfirm gtk3 libkeybinder3 ARG user=appflowy ARG uid=1000 diff --git a/shared-lib/Cargo.lock b/shared-lib/Cargo.lock index 79357da09b..e519a829cd 100644 --- a/shared-lib/Cargo.lock +++ b/shared-lib/Cargo.lock @@ -650,9 +650,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.11.2" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "heck" @@ -732,9 +732,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "1.8.1" +version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f647032dfaa1f8b6dc29bd3edb7bbef4861b8b8007ebb118d6db284fd59f6ee" +checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" dependencies = [ "autocfg", "hashbrown", diff --git a/shared-lib/flowy-grid-data-model/Cargo.toml b/shared-lib/flowy-grid-data-model/Cargo.toml index 3e640ec1af..671fdbb4e6 100644 --- a/shared-lib/flowy-grid-data-model/Cargo.toml +++ b/shared-lib/flowy-grid-data-model/Cargo.toml @@ -12,7 +12,7 @@ serde_json = {version = "1.0"} serde_repr = "0.1" nanoid = "0.4.0" flowy-error-code = { path = "../flowy-error-code"} -indexmap = {version = "1.8.1", features = ["serde"]} +indexmap = {version = "1.9.1", features = ["serde"]} tracing = { version = "0.1", features = ["log"] } [build-dependencies] diff --git a/shared-lib/flowy-grid-data-model/src/revision/group_rev.rs b/shared-lib/flowy-grid-data-model/src/revision/group_rev.rs index 0aded0d3c7..44a7a4e1f6 100644 --- a/shared-lib/flowy-grid-data-model/src/revision/group_rev.rs +++ b/shared-lib/flowy-grid-data-model/src/revision/group_rev.rs @@ -128,14 +128,6 @@ impl GroupRevision { } } - pub fn default_group(id: String, group_name: String) -> Self { - Self { - id, - name: group_name, - visible: true, - } - } - pub fn update_with_other(&mut self, other: &GroupRevision) { self.visible = other.visible } diff --git a/shared-lib/flowy-sync/src/client_grid/view_revision_pad.rs b/shared-lib/flowy-sync/src/client_grid/view_revision_pad.rs index 8613418c40..11e9d5a07e 100644 --- a/shared-lib/flowy-sync/src/client_grid/view_revision_pad.rs +++ b/shared-lib/flowy-sync/src/client_grid/view_revision_pad.rs @@ -66,7 +66,7 @@ impl GridViewRevisionPad { } #[tracing::instrument(level = "trace", skip_all, err)] - pub fn insert_group( + pub fn insert_or_update_group_configuration( &mut self, field_id: &str, field_type: &FieldTypeRevision,