Merge branch 'main' into latex

This commit is contained in:
Lucas.Xu 2022-10-06 10:32:38 +08:00 committed by GitHub
commit c9fc9e7497
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
107 changed files with 2133 additions and 710 deletions

View File

@ -199,7 +199,8 @@
"delete": "Delete", "delete": "Delete",
"textPlaceholder": "Empty", "textPlaceholder": "Empty",
"copyProperty": "Copied property to clipboard", "copyProperty": "Copied property to clipboard",
"count": "Count" "count": "Count",
"newRow": "New row"
}, },
"selectOption": { "selectOption": {
"create": "Create", "create": "Create",

View File

@ -94,7 +94,20 @@
}, },
"tooltip": { "tooltip": {
"lightMode": "Passer en mode clair", "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": { "contactsPage": {
"title": "Contacts", "title": "Contacts",
@ -123,7 +136,7 @@
"failedMsg": "Assurez-vous d'avoir terminé le processus de connexion dans votre navigateur." "failedMsg": "Assurez-vous d'avoir terminé le processus de connexion dans votre navigateur."
}, },
"google": { "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.", "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:", "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:", "instruction3": "Accédez au lien suivant dans votre navigateur web et saisissez le code ci-dessus:",
@ -135,6 +148,7 @@
"menu": { "menu": {
"appearance": "Apparence", "appearance": "Apparence",
"language": "Langue", "language": "Langue",
"user": "Utilisateur",
"open": "Ouvrir les paramètres" "open": "Ouvrir les paramètres"
}, },
"appearance": { "appearance": {
@ -142,15 +156,12 @@
"darkLabel": "Mode sombre" "darkLabel": "Mode sombre"
} }
}, },
"sideBar": {
"openSidebar": "Open sidebar",
"closeSidebar": "Close sidebar"
},
"grid": { "grid": {
"settings": { "settings": {
"filter": "Filtrer", "filter": "Filtrer",
"sortBy": "Trier par", "sortBy": "Filtrer par",
"Properties": "Propriétés" "Properties": "Propriétés",
"group": "Groupe"
}, },
"field": { "field": {
"hide": "Cacher", "hide": "Cacher",
@ -179,13 +190,17 @@
"addSelectOption": "Ajouter une option", "addSelectOption": "Ajouter une option",
"optionTitle": "Options", "optionTitle": "Options",
"addOption": "Ajouter une option", "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": { "row": {
"duplicate": "Dupliquer", "duplicate": "Dupliquer",
"delete": "Supprimer", "delete": "Supprimer",
"textPlaceholder": "Vide", "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": { "selectOption": {
"create": "Créer", "create": "Créer",
@ -211,5 +226,10 @@
"timeHintTextInTwelveHour": "01:00 PM", "timeHintTextInTwelveHour": "01:00 PM",
"timeHintTextInTwentyFourHour": "13:00" "timeHintTextInTwentyFourHour": "13:00"
} }
},
"board": {
"column": {
"create_new_card": "Nouveau"
}
} }
} }

View File

@ -94,7 +94,14 @@
}, },
"tooltip": { "tooltip": {
"lightMode": "切换到亮色模式", "lightMode": "切换到亮色模式",
"darkMode": "切换到暗色模式" "darkMode": "切换到暗色模式",
"openAsPage": "作为页面打开",
"addNewRow": "增加一行",
"openMenu": "点击打开菜单"
},
"sideBar": {
"openSidebar": "打开侧边栏",
"closeSidebar": "关闭侧边栏"
}, },
"notifications": { "notifications": {
"export": { "export": {
@ -149,15 +156,12 @@
"darkLabel": "夜间模式" "darkLabel": "夜间模式"
} }
}, },
"sideBar": {
"openSidebar": "打开侧边栏",
"closeSidebar": "关闭侧边栏"
},
"grid": { "grid": {
"settings": { "settings": {
"filter": "过滤器", "filter": "过滤器",
"sortBy": "排序", "sortBy": "排序",
"Properties": "属性" "Properties": "属性",
"group": "组"
}, },
"field": { "field": {
"hide": "隐藏", "hide": "隐藏",
@ -186,13 +190,17 @@
"addSelectOption": "添加一个标签", "addSelectOption": "添加一个标签",
"optionTitle": "标签", "optionTitle": "标签",
"addOption": "添加标签", "addOption": "添加标签",
"editProperty": "编辑列属性" "editProperty": "编辑列属性",
"newColumn": "增加一列",
"deleteFieldPromptMessage": "确定要删除这个属性吗? "
}, },
"row": { "row": {
"duplicate": "复制", "duplicate": "复制",
"delete": "删除", "delete": "删除",
"textPlaceholder": "空", "textPlaceholder": "空",
"copyProperty": "复制列" "copyProperty": "复制列",
"count": "数量",
"newRow": "添加一行"
}, },
"selectOption": { "selectOption": {
"create": "新建", "create": "新建",
@ -218,5 +226,11 @@
"timeHintTextInTwelveHour": "01:00 PM", "timeHintTextInTwelveHour": "01:00 PM",
"timeHintTextInTwentyFourHour": "13:00" "timeHintTextInTwentyFourHour": "13:00"
} }
},
"board": {
"column": {
"create_new_card": "新建"
},
"menuName": "看板"
} }
} }

View File

@ -36,21 +36,21 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
super(BoardState.initial(view.id)) { super(BoardState.initial(view.id)) {
boardController = AppFlowyBoardController( boardController = AppFlowyBoardController(
onMoveGroup: ( onMoveGroup: (
fromColumnId, fromGroupId,
fromIndex, fromIndex,
toColumnId, toGroupId,
toIndex, toIndex,
) { ) {
_moveGroup(fromColumnId, toColumnId); _moveGroup(fromGroupId, toGroupId);
}, },
onMoveGroupItem: ( onMoveGroupItem: (
columnId, groupId,
fromIndex, fromIndex,
toIndex, toIndex,
) { ) {
final fromRow = groupControllers[columnId]?.rowAtIndex(fromIndex); final fromRow = groupControllers[groupId]?.rowAtIndex(fromIndex);
final toRow = groupControllers[columnId]?.rowAtIndex(toIndex); final toRow = groupControllers[groupId]?.rowAtIndex(toIndex);
_moveRow(fromRow, columnId, toRow); _moveRow(fromRow, groupId, toRow);
}, },
onMoveGroupItemToGroup: ( onMoveGroupItemToGroup: (
fromGroupId, fromGroupId,

View File

@ -69,7 +69,6 @@ class BoardContent extends StatefulWidget {
class _BoardContentState extends State<BoardContent> { class _BoardContentState extends State<BoardContent> {
late AppFlowyBoardScrollController scrollManager; late AppFlowyBoardScrollController scrollManager;
final Map<String, ValueKey> cardKeysCache = {};
final config = AppFlowyBoardConfig( final config = AppFlowyBoardConfig(
groupBackgroundColor: HexColor.fromHex('#F7F8FC'), groupBackgroundColor: HexColor.fromHex('#F7F8FC'),
@ -104,8 +103,8 @@ class _BoardContentState extends State<BoardContent> {
Widget _buildBoard(BuildContext context) { Widget _buildBoard(BuildContext context) {
return ChangeNotifierProvider.value( return ChangeNotifierProvider.value(
value: Provider.of<AppearanceSettingModel>(context, listen: true), value: Provider.of<AppearanceSetting>(context, listen: true),
child: Selector<AppearanceSettingModel, AppTheme>( child: Selector<AppearanceSetting, AppTheme>(
selector: (ctx, notifier) => notifier.theme, selector: (ctx, notifier) => notifier.theme,
builder: (ctx, theme, child) => Expanded( builder: (ctx, theme, child) => Expanded(
child: AppFlowyBoard( child: AppFlowyBoard(
@ -139,7 +138,7 @@ class _BoardContentState extends State<BoardContent> {
.read<BoardBloc>() .read<BoardBloc>()
.add(BoardEvent.endEditRow(editingRow.row.id)); .add(BoardEvent.endEditRow(editingRow.row.id));
} else { } else {
scrollManager.scrollToBottom(editingRow.columnId, () { scrollManager.scrollToBottom(editingRow.columnId, (boardContext) {
context context
.read<BoardBloc>() .read<BoardBloc>()
.add(BoardEvent.endEditRow(editingRow.row.id)); .add(BoardEvent.endEditRow(editingRow.row.id));
@ -247,15 +246,8 @@ class _BoardContentState extends State<BoardContent> {
); );
final groupItemId = columnItem.id + group.id; final groupItemId = columnItem.id + group.id;
ValueKey? key = cardKeysCache[groupItemId];
if (key == null) {
final newKey = ValueKey(groupItemId);
cardKeysCache[groupItemId] = newKey;
key = newKey;
}
return AppFlowyGroupCard( return AppFlowyGroupCard(
key: key, key: ValueKey(groupItemId),
margin: config.cardPadding, margin: config.cardPadding,
decoration: _makeBoxDecoration(context), decoration: _makeBoxDecoration(context),
child: BoardCard( child: BoardCard(
@ -331,8 +323,8 @@ class _ToolbarBlocAdaptor extends StatelessWidget {
); );
return ChangeNotifierProvider.value( return ChangeNotifierProvider.value(
value: Provider.of<AppearanceSettingModel>(context, listen: true), value: Provider.of<AppearanceSetting>(context, listen: true),
child: Selector<AppearanceSettingModel, AppTheme>( child: Selector<AppearanceSetting, AppTheme>(
selector: (ctx, notifier) => notifier.theme, selector: (ctx, notifier) => notifier.theme,
builder: (ctx, theme, child) { builder: (ctx, theme, child) {
return BoardToolbar(toolbarContext: toolbarContext); return BoardToolbar(toolbarContext: toolbarContext);

View File

@ -131,8 +131,8 @@ class DocumentShareButton extends StatelessWidget {
child: BlocBuilder<DocShareBloc, DocShareState>( child: BlocBuilder<DocShareBloc, DocShareState>(
builder: (context, state) { builder: (context, state) {
return ChangeNotifierProvider.value( return ChangeNotifierProvider.value(
value: Provider.of<AppearanceSettingModel>(context, listen: true), value: Provider.of<AppearanceSetting>(context, listen: true),
child: Selector<AppearanceSettingModel, Locale>( child: Selector<AppearanceSetting, Locale>(
selector: (ctx, notifier) => notifier.locale, selector: (ctx, notifier) => notifier.locale,
builder: (ctx, _, child) => ConstrainedBox( builder: (ctx, _, child) => ConstrainedBox(
constraints: const BoxConstraints.expand( constraints: const BoxConstraints.expand(

View File

@ -134,7 +134,7 @@ class _DocumentPageState extends State<DocumentPage> {
Widget _renderToolbar(quill.QuillController controller) { Widget _renderToolbar(quill.QuillController controller) {
return ChangeNotifierProvider.value( return ChangeNotifierProvider.value(
value: Provider.of<AppearanceSettingModel>(context, listen: true), value: Provider.of<AppearanceSetting>(context, listen: true),
child: EditorToolbar.basic( child: EditorToolbar.basic(
controller: controller, controller: controller,
), ),

View File

@ -1,6 +1,8 @@
import 'package:app_flowy/generated/locale_keys.g.dart'; 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/cell/date_cal_bloc.dart';
import 'package:app_flowy/plugins/grid/application/field/type_option/type_option_context.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:appflowy_popover/appflowy_popover.dart';
import 'package:dartz/dartz.dart' show Either; import 'package:dartz/dartz.dart' show Either;
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
@ -167,6 +169,18 @@ class _CellCalendarWidgetState extends State<_CellCalendarWidget> {
rightChevronMargin: EdgeInsets.zero, rightChevronMargin: EdgeInsets.zero,
rightChevronIcon: svgWidget("home/arrow_right"), 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( calendarStyle: CalendarStyle(
selectedDecoration: BoxDecoration( selectedDecoration: BoxDecoration(
color: theme.main1, color: theme.main1,
@ -230,11 +244,13 @@ class _IncludeTimeButton extends StatelessWidget {
FlowyText.medium(LocaleKeys.grid_field_includeTime.tr(), FlowyText.medium(LocaleKeys.grid_field_includeTime.tr(),
fontSize: 14), fontSize: 14),
const Spacer(), const Spacer(),
Switch( Toggle(
value: includeTime, value: includeTime,
onChanged: (newValue) => context onChanged: (value) => context
.read<DateCalBloc>() .read<DateCalBloc>()
.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), offset: const Offset(20, 0),
constraints: BoxConstraints.loose(const Size(140, 100)), constraints: BoxConstraints.loose(const Size(140, 100)),
child: FlowyButton( child: FlowyButton(
text: FlowyText.medium(title, fontSize: 12), text: FlowyText.medium(title, fontSize: 14),
hoverColor: theme.hover, hoverColor: theme.hover,
margin: kMargin, margin: kMargin,
rightIcon: svgWidget("grid/more", color: theme.iconColor), rightIcon: svgWidget("grid/more", color: theme.iconColor),

View File

@ -130,14 +130,10 @@ class SelectOptionTagCell extends StatelessWidget {
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Flexible( SelectOptionTag.fromOption(
fit: FlexFit.loose, context: context,
flex: 2, option: option,
child: SelectOptionTag.fromOption( onSelected: () => onSelected(option),
context: context,
option: option,
onSelected: () => onSelected(option),
),
), ),
const Spacer(), const Spacer(),
...children, ...children,

View File

@ -248,32 +248,25 @@ class _SelectOptionCellState extends State<_SelectOptionCell> {
mutex: widget.popoverMutex, mutex: widget.popoverMutex,
child: SizedBox( child: SizedBox(
height: GridSize.typeOptionItemHeight, height: GridSize.typeOptionItemHeight,
child: Row( child: SelectOptionTagCell(
option: widget.option,
onSelected: (option) {
context
.read<SelectOptionCellEditorBloc>()
.add(SelectOptionEditorEvent.selectOption(option.id));
},
children: [ children: [
Flexible( if (widget.isSelected)
fit: FlexFit.loose, Padding(
child: SelectOptionTagCell( padding: const EdgeInsets.only(right: 6),
option: widget.option, child: svgWidget("grid/checkmark"),
onSelected: (option) {
context
.read<SelectOptionCellEditorBloc>()
.add(SelectOptionEditorEvent.selectOption(option.id));
},
children: [
if (widget.isSelected)
Padding(
padding: const EdgeInsets.only(right: 6),
child: svgWidget("grid/checkmark"),
),
],
), ),
),
FlowyIconButton( FlowyIconButton(
width: 30, width: 30,
onPressed: () => _popoverController.show(), onPressed: () => _popoverController.show(),
iconPadding: const EdgeInsets.fromLTRB(4, 4, 4, 4), iconPadding: const EdgeInsets.fromLTRB(4, 4, 4, 4),
icon: svgWidget("editor/details", color: theme.iconColor), icon: svgWidget("editor/details", color: theme.iconColor),
) ),
], ],
), ),
), ),

View File

@ -97,7 +97,6 @@ class GridURLCell extends GridCellWidget {
class _GridURLCellState extends GridCellState<GridURLCell> { class _GridURLCellState extends GridCellState<GridURLCell> {
final _popoverController = PopoverController(); final _popoverController = PopoverController();
GridURLCellController? _cellContext;
late URLCellBloc _cellBloc; late URLCellBloc _cellBloc;
@override @override
@ -132,6 +131,7 @@ class _GridURLCellState extends GridCellState<GridURLCell> {
controller: _popoverController, controller: _popoverController,
constraints: BoxConstraints.loose(const Size(300, 160)), constraints: BoxConstraints.loose(const Size(300, 160)),
direction: PopoverDirection.bottomWithLeftAligned, direction: PopoverDirection.bottomWithLeftAligned,
triggerActions: PopoverTriggerFlags.none,
offset: const Offset(0, 20), offset: const Offset(0, 20),
child: SizedBox.expand( child: SizedBox.expand(
child: GestureDetector( child: GestureDetector(
@ -144,7 +144,8 @@ class _GridURLCellState extends GridCellState<GridURLCell> {
), ),
popupBuilder: (BuildContext popoverContext) { popupBuilder: (BuildContext popoverContext) {
return URLEditorPopover( return URLEditorPopover(
cellController: _cellContext!, cellController: widget.cellControllerBuilder.build()
as GridURLCellController,
); );
}, },
onClose: () { onClose: () {
@ -166,17 +167,13 @@ class _GridURLCellState extends GridCellState<GridURLCell> {
final uri = Uri.parse(url); final uri = Uri.parse(url);
if (url.isNotEmpty && await canLaunchUrl(uri)) { if (url.isNotEmpty && await canLaunchUrl(uri)) {
await launchUrl(uri); await launchUrl(uri);
} else {
_cellContext =
widget.cellControllerBuilder.build() as GridURLCellController;
widget.onCellEditing.value = true;
_popoverController.show();
} }
} }
@override @override
void requestBeginFocus() { void requestBeginFocus() {
_openUrlOrEdit(_cellBloc.state.url); widget.onCellEditing.value = true;
_popoverController.show();
} }
@override @override

View File

@ -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: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/image.dart';
import 'package:flowy_infra/theme.dart'; import 'package:flowy_infra/theme.dart';
import 'package:flowy_infra_ui/style_widget/button.dart'; import 'package:flowy_infra_ui/style_widget/button.dart';
@ -13,7 +15,7 @@ class GridAddRowButton extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = context.watch<AppTheme>(); final theme = context.watch<AppTheme>();
return FlowyButton( return FlowyButton(
text: const FlowyText.medium('New row', fontSize: 12), text: FlowyText.medium(LocaleKeys.grid_row_newRow.tr(), fontSize: 12),
hoverColor: theme.shader6, hoverColor: theme.shader6,
onTap: () => context.read<GridBloc>().add(const GridEvent.createRow()), onTap: () => context.read<GridBloc>().add(const GridEvent.createRow()),
leftIcon: svgWidget("home/add", color: theme.iconColor), leftIcon: svgWidget("home/add", color: theme.iconColor),

View File

@ -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/date_bloc.dart';
import 'package:app_flowy/plugins/grid/application/field/type_option/type_option_context.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:easy_localization/easy_localization.dart' hide DateFormat;
import 'package:app_flowy/generated/locale_keys.g.dart'; import 'package:app_flowy/generated/locale_keys.g.dart';
import 'package:flowy_infra/image.dart'; import 'package:flowy_infra/image.dart';
@ -161,6 +163,7 @@ class _IncludeTimeButton extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = context.watch<AppTheme>();
return BlocSelector<DateTypeOptionBloc, DateTypeOptionState, bool>( return BlocSelector<DateTypeOptionBloc, DateTypeOptionState, bool>(
selector: (state) => state.typeOption.includeTime, selector: (state) => state.typeOption.includeTime,
builder: (context, includeTime) { builder: (context, includeTime) {
@ -173,13 +176,15 @@ class _IncludeTimeButton extends StatelessWidget {
FlowyText.medium(LocaleKeys.grid_field_includeTime.tr(), FlowyText.medium(LocaleKeys.grid_field_includeTime.tr(),
fontSize: 12), fontSize: 12),
const Spacer(), const Spacer(),
Switch( Toggle(
value: includeTime, value: includeTime,
onChanged: (newValue) { onChanged: (value) {
context context
.read<DateTypeOptionBloc>() .read<DateTypeOptionBloc>()
.add(DateTypeOptionEvent.includeTime(newValue)); .add(DateTypeOptionEvent.includeTime(!value));
}, },
style: ToggleStyle.big(theme),
padding: EdgeInsets.zero,
), ),
], ],
), ),

View File

@ -33,8 +33,8 @@ class MenuTrash extends StatelessWidget {
Widget _render(BuildContext context) { Widget _render(BuildContext context) {
return Row(children: [ return Row(children: [
ChangeNotifierProvider.value( ChangeNotifierProvider.value(
value: Provider.of<AppearanceSettingModel>(context, listen: true), value: Provider.of<AppearanceSetting>(context, listen: true),
child: Selector<AppearanceSettingModel, AppTheme>( child: Selector<AppearanceSetting, AppTheme>(
selector: (ctx, notifier) => notifier.theme, selector: (ctx, notifier) => notifier.theme,
builder: (ctx, theme, child) => SizedBox( builder: (ctx, theme, child) => SizedBox(
width: 16, width: 16,
@ -44,8 +44,8 @@ class MenuTrash extends StatelessWidget {
), ),
const HSpace(6), const HSpace(6),
ChangeNotifierProvider.value( ChangeNotifierProvider.value(
value: Provider.of<AppearanceSettingModel>(context, listen: true), value: Provider.of<AppearanceSetting>(context, listen: true),
child: Selector<AppearanceSettingModel, Locale>( child: Selector<AppearanceSetting, Locale>(
selector: (ctx, notifier) => notifier.locale, selector: (ctx, notifier) => notifier.locale,
builder: (ctx, _, child) => builder: (ctx, _, child) =>
FlowyText.medium(LocaleKeys.trash_text.tr(), fontSize: 12), FlowyText.medium(LocaleKeys.trash_text.tr(), fontSize: 12),

View File

@ -17,8 +17,8 @@ class InitAppWidgetTask extends LaunchTask {
@override @override
Future<void> initialize(LaunchContext context) async { Future<void> initialize(LaunchContext context) async {
final widget = context.getIt<EntryPoint>().create(); final widget = context.getIt<EntryPoint>().create();
final setting = await UserSettingsService().getAppearanceSettings(); final setting = await SettingsFFIService().getAppearanceSetting();
final settingModel = AppearanceSettingModel(setting); final settingModel = AppearanceSetting(setting);
final app = ApplicationWidget( final app = ApplicationWidget(
settingModel: settingModel, settingModel: settingModel,
child: widget, child: widget,
@ -58,7 +58,7 @@ class InitAppWidgetTask extends LaunchTask {
class ApplicationWidget extends StatelessWidget { class ApplicationWidget extends StatelessWidget {
final Widget child; final Widget child;
final AppearanceSettingModel settingModel; final AppearanceSetting settingModel;
const ApplicationWidget({ const ApplicationWidget({
Key? key, Key? key,
@ -75,10 +75,10 @@ class ApplicationWidget extends StatelessWidget {
const minWidth = 600.0; const minWidth = 600.0;
setWindowMinSize(const Size(minWidth, minWidth / ratio)); setWindowMinSize(const Size(minWidth, minWidth / ratio));
settingModel.readLocaleWhenAppLaunch(context); settingModel.readLocaleWhenAppLaunch(context);
AppTheme theme = context.select<AppearanceSettingModel, AppTheme>( AppTheme theme = context.select<AppearanceSetting, AppTheme>(
(value) => value.theme, (value) => value.theme,
); );
Locale locale = context.select<AppearanceSettingModel, Locale>( Locale locale = context.select<AppearanceSetting, Locale>(
(value) => value.locale, (value) => value.locale,
); );

View File

@ -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-error/errors.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-user/user_setting.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-user/user_setting.pb.dart';
class UserSettingsService { class SettingsFFIService {
Future<AppearanceSettingsPB> getAppearanceSettings() async { Future<AppearanceSettingsPB> getAppearanceSetting() async {
final result = await UserEventGetAppearanceSetting().send(); final result = await UserEventGetAppearanceSetting().send();
return result.fold( return result.fold(
@ -18,7 +18,8 @@ class UserSettingsService {
); );
} }
Future<Either<Unit, FlowyError>> setAppearanceSettings(AppearanceSettingsPB settings) { Future<Either<Unit, FlowyError>> setAppearanceSetting(
return UserEventSetAppearanceSetting(settings).send(); AppearanceSettingsPB setting) {
return UserEventSetAppearanceSetting(setting).send();
} }
} }

View File

@ -8,72 +8,114 @@ import 'package:flowy_sdk/protobuf/flowy-user/user_setting.pb.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
class AppearanceSettingModel extends ChangeNotifier with EquatableMixin { /// [AppearanceSetting] is used to modify the appear setting of AppFlowy application. Including the [Locale], [AppTheme], etc.
AppearanceSettingsPB setting; class AppearanceSetting extends ChangeNotifier with EquatableMixin {
final AppearanceSettingsPB _setting;
AppTheme _theme; AppTheme _theme;
Locale _locale; Locale _locale;
Timer? _saveOperation; Timer? _debounceSaveOperation;
AppearanceSettingModel(this.setting) AppearanceSetting(AppearanceSettingsPB setting)
: _theme = AppTheme.fromName(name: setting.theme), : _setting = setting,
_locale = _theme = AppTheme.fromName(name: setting.theme),
Locale(setting.locale.languageCode, setting.locale.countryCode); _locale = Locale(
setting.locale.languageCode,
setting.locale.countryCode,
);
/// Returns the current [AppTheme]
AppTheme get theme => _theme; AppTheme get theme => _theme;
/// Returns the current [Locale]
Locale get locale => _locale; Locale get locale => _locale;
Future<void> save() async { /// Updates the current theme and notify the listeners the theme was changed.
_saveOperation?.cancel(); /// Do nothing if the passed in themeType equal to the current theme type.
_saveOperation = Timer(const Duration(seconds: 2), () async { ///
await UserSettingsService().setAppearanceSettings(setting); void setTheme(ThemeType themeType) {
}); if (_theme.ty == themeType) {
} return;
@override
List<Object> 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();
} }
_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) { void setLocale(BuildContext context, Locale newLocale) {
if (!context.supportedLocales.contains(newLocale)) { if (!context.supportedLocales.contains(newLocale)) {
Log.warn("Unsupported locale: $newLocale"); Log.warn("Unsupported locale: $newLocale, Fallback to locale: en");
newLocale = const Locale('en'); newLocale = const Locale('en');
Log.debug("Fallback to locale: $newLocale");
} }
context.setLocale(newLocale); context.setLocale(newLocale);
if (_locale != newLocale) { if (_locale != newLocale) {
_locale = newLocale; _locale = newLocale;
setting.locale.languageCode = _locale.languageCode; _setting.locale.languageCode = _locale.languageCode;
setting.locale.countryCode = _locale.countryCode ?? ""; _setting.locale.countryCode = _locale.countryCode ?? "";
_saveAppearSetting();
notifyListeners(); notifyListeners();
save();
} }
} }
void readLocaleWhenAppLaunch(BuildContext context) { /// Saves key/value setting to disk.
if (setting.resetAsDefault) { /// Removes the key if the passed in value is null
setting.resetAsDefault = false; void setKeyValue(String key, String? value) {
save(); 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); setLocale(context, context.deviceLocale);
return; return;
} }
// when opening app the first time
setLocale(context, _locale); setLocale(context, _locale);
} }
Future<void> _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<Object> get props {
return [_setting.hashCode];
}
} }

View File

@ -99,6 +99,7 @@ class MenuAppHeader extends StatelessWidget {
app.name, app.name,
fontSize: 12, fontSize: 12,
color: theme.textColor, color: theme.textColor,
overflow: TextOverflow.ellipsis,
), ),
), ),
), ),

View File

@ -89,8 +89,7 @@ class _MenuAppState extends State<MenuApp> {
hasIcon: false, hasIcon: false,
), ),
header: ChangeNotifierProvider.value( header: ChangeNotifierProvider.value(
value: value: Provider.of<AppearanceSetting>(context, listen: true),
Provider.of<AppearanceSettingModel>(context, listen: true),
child: MenuAppHeader(widget.app), child: MenuAppHeader(widget.app),
), ),
expanded: ViewSection(appViewData: viewDataContext), expanded: ViewSection(appViewData: viewDataContext),

View File

@ -82,7 +82,7 @@ class ViewSectionItem extends StatelessWidget {
child: FlowyText.regular( child: FlowyText.regular(
state.view.name, state.view.name,
fontSize: 12, fontSize: 12,
overflow: TextOverflow.clip, overflow: TextOverflow.ellipsis,
), ),
), ),
]; ];

View File

@ -28,8 +28,9 @@ class MenuUser extends StatelessWidget {
children: [ children: [
_renderAvatar(context), _renderAvatar(context),
const HSpace(10), const HSpace(10),
_renderUserName(context), Expanded(
const Spacer(), child: _renderUserName(context),
),
_renderSettingsButton(context), _renderSettingsButton(context),
//ToDo: when the user is allowed to create another workspace, //ToDo: when the user is allowed to create another workspace,
//we get the below block back //we get the below block back
@ -63,7 +64,7 @@ class MenuUser extends StatelessWidget {
if (name.isEmpty) { if (name.isEmpty) {
name = context.read<MenuUserBloc>().state.userProfile.email; name = context.read<MenuUserBloc>().state.userProfile.email;
} }
return FlowyText(name, fontSize: 12); return FlowyText(name, fontSize: 12, overflow: TextOverflow.ellipsis);
} }
Widget _renderSettingsButton(BuildContext context) { Widget _renderSettingsButton(BuildContext context) {

View File

@ -33,8 +33,7 @@ class SettingsDialog extends StatelessWidget {
..add(const SettingsDialogEvent.initial()), ..add(const SettingsDialogEvent.initial()),
child: BlocBuilder<SettingsDialogBloc, SettingsDialogState>( child: BlocBuilder<SettingsDialogBloc, SettingsDialogState>(
builder: (context, state) => ChangeNotifierProvider.value( builder: (context, state) => ChangeNotifierProvider.value(
value: Provider.of<AppearanceSettingModel>(context, value: Provider.of<AppearanceSetting>(context, listen: true),
listen: true),
child: FlowyDialog( child: FlowyDialog(
title: Text( title: Text(
LocaleKeys.settings_title.tr(), LocaleKeys.settings_title.tr(),

View File

@ -30,9 +30,7 @@ class SettingsAppearanceView extends StatelessWidget {
), ),
Toggle( Toggle(
value: theme.isDark, value: theme.isDark,
onChanged: (val) { onChanged: (_) => setTheme(context),
context.read<AppearanceSettingModel>().swapTheme();
},
style: ToggleStyle.big(theme), style: ToggleStyle.big(theme),
), ),
Text( Text(
@ -48,4 +46,13 @@ class SettingsAppearanceView extends StatelessWidget {
), ),
); );
} }
void setTheme(BuildContext context) {
final theme = context.read<AppTheme>();
if (theme.isDark) {
context.read<AppearanceSetting>().setTheme(ThemeType.light);
} else {
context.read<AppearanceSetting>().setTheme(ThemeType.dark);
}
}
} }

View File

@ -13,7 +13,7 @@ class SettingsLanguageView extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
context.watch<AppTheme>(); context.watch<AppTheme>();
return ChangeNotifierProvider.value( return ChangeNotifierProvider.value(
value: Provider.of<AppearanceSettingModel>(context, listen: true), value: Provider.of<AppearanceSetting>(context, listen: true),
child: SingleChildScrollView( child: SingleChildScrollView(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -43,7 +43,8 @@ class LanguageSelectorDropdown extends StatefulWidget {
}) : super(key: key); }) : super(key: key);
@override @override
State<LanguageSelectorDropdown> createState() => _LanguageSelectorDropdownState(); State<LanguageSelectorDropdown> createState() =>
_LanguageSelectorDropdownState();
} }
class _LanguageSelectorDropdownState extends State<LanguageSelectorDropdown> { class _LanguageSelectorDropdownState extends State<LanguageSelectorDropdown> {
@ -77,10 +78,10 @@ class _LanguageSelectorDropdownState extends State<LanguageSelectorDropdown> {
), ),
child: DropdownButtonHideUnderline( child: DropdownButtonHideUnderline(
child: DropdownButton<Locale>( child: DropdownButton<Locale>(
value: context.read<AppearanceSettingModel>().locale, value: context.read<AppearanceSetting>().locale,
onChanged: (val) { onChanged: (val) {
setState(() { setState(() {
context.read<AppearanceSettingModel>().setLocale(context, val!); context.read<AppearanceSetting>().setLocale(context, val!);
}); });
}, },
icon: const Visibility( icon: const Visibility(

View File

@ -1,3 +1,5 @@
# 0.0.8
* Enable drag and drop group
# 0.0.7 # 0.0.7
* Rename some classes * Rename some classes
* Add documentation * Add documentation
@ -7,7 +9,7 @@
# 0.0.5 # 0.0.5
* Optimize insert card animation * 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 * Fix some bugs
# 0.0.4 # 0.0.4
@ -24,6 +26,5 @@
# 0.0.1 # 0.0.1
* Support drag and drop column * Support drag and drop group items from one to another
* Support drag and drop column items from one to another

View File

@ -34,7 +34,7 @@ class _MyAppState extends State<MyApp> {
appBar: AppBar( appBar: AppBar(
title: const Text('AppFlowy Board'), title: const Text('AppFlowy Board'),
), ),
body: _examples[_currentIndex], body: Container(color: Colors.white, child: _examples[_currentIndex]),
bottomNavigationBar: BottomNavigationBar( bottomNavigationBar: BottomNavigationBar(
fixedColor: _bottomNavigationColor, fixedColor: _bottomNavigationColor,
showSelectedLabels: true, showSelectedLabels: true,

View File

@ -21,8 +21,11 @@ class _MultiBoardListExampleState extends State<MultiBoardListExample> {
}, },
); );
late AppFlowyBoardScrollController boardController;
@override @override
void initState() { void initState() {
boardController = AppFlowyBoardScrollController();
final group1 = AppFlowyGroupData(id: "To Do", name: "To Do", items: [ final group1 = AppFlowyGroupData(id: "To Do", name: "To Do", items: [
TextItem("Card 1"), TextItem("Card 1"),
TextItem("Card 2"), TextItem("Card 2"),
@ -67,12 +70,16 @@ class _MultiBoardListExampleState extends State<MultiBoardListExample> {
child: _buildCard(groupItem), child: _buildCard(groupItem),
); );
}, },
boardScrollController: boardController,
footerBuilder: (context, columnData) { footerBuilder: (context, columnData) {
return AppFlowyGroupFooter( return AppFlowyGroupFooter(
icon: const Icon(Icons.add, size: 20), icon: const Icon(Icons.add, size: 20),
title: const Text('New'), title: const Text('New'),
height: 50, height: 50,
margin: config.groupItemPadding, margin: config.groupItemPadding,
onAddButtonClick: () {
boardController.scrollToBottom(columnData.id, (p0) {});
},
); );
}, },
headerBuilder: (context, columnData) { headerBuilder: (context, columnData) {

View File

@ -13,10 +13,8 @@ import '../rendering/board_overlay.dart';
class AppFlowyBoardScrollController { class AppFlowyBoardScrollController {
AppFlowyBoardState? _groupState; AppFlowyBoardState? _groupState;
void scrollToBottom(String groupId, VoidCallback? completed) { void scrollToBottom(String groupId, void Function(BuildContext)? completed) {
_groupState _groupState?.reorderFlexActionMap[groupId]?.scrollToBottom(completed);
?.getReorderFlexState(groupId: groupId)
?.scrollToBottom(completed);
} }
} }
@ -133,7 +131,7 @@ class AppFlowyBoard extends StatelessWidget {
dataController: controller, dataController: controller,
scrollController: scrollController, scrollController: scrollController,
scrollManager: boardScrollController, scrollManager: boardScrollController,
columnsState: _groupState, groupState: _groupState,
background: background, background: background,
delegate: _phantomController, delegate: _phantomController,
groupConstraints: groupConstraints, groupConstraints: groupConstraints,
@ -158,7 +156,7 @@ class _AppFlowyBoardContent extends StatefulWidget {
final ReorderFlexConfig reorderFlexConfig; final ReorderFlexConfig reorderFlexConfig;
final BoxConstraints groupConstraints; final BoxConstraints groupConstraints;
final AppFlowyBoardScrollController? scrollManager; final AppFlowyBoardScrollController? scrollManager;
final AppFlowyBoardState columnsState; final AppFlowyBoardState groupState;
final AppFlowyBoardCardBuilder cardBuilder; final AppFlowyBoardCardBuilder cardBuilder;
final AppFlowyBoardHeaderBuilder? headerBuilder; final AppFlowyBoardHeaderBuilder? headerBuilder;
final AppFlowyBoardFooterBuilder? footerBuilder; final AppFlowyBoardFooterBuilder? footerBuilder;
@ -171,7 +169,7 @@ class _AppFlowyBoardContent extends StatefulWidget {
required this.delegate, required this.delegate,
required this.dataController, required this.dataController,
required this.scrollManager, required this.scrollManager,
required this.columnsState, required this.groupState,
this.scrollController, this.scrollController,
this.background, this.background,
required this.groupConstraints, required this.groupConstraints,
@ -192,8 +190,6 @@ class _AppFlowyBoardContentState extends State<_AppFlowyBoardContent> {
GlobalKey(debugLabel: '$_AppFlowyBoardContent overlay key'); GlobalKey(debugLabel: '$_AppFlowyBoardContent overlay key');
late BoardOverlayEntry _overlayEntry; late BoardOverlayEntry _overlayEntry;
final Map<String, GlobalObjectKey> _reorderFlexKeys = {};
@override @override
void initState() { void initState() {
_overlayEntry = BoardOverlayEntry( _overlayEntry = BoardOverlayEntry(
@ -202,7 +198,7 @@ class _AppFlowyBoardContentState extends State<_AppFlowyBoardContent> {
reorderFlexId: widget.dataController.identifier, reorderFlexId: widget.dataController.identifier,
acceptedReorderFlexId: widget.dataController.groupIds, acceptedReorderFlexId: widget.dataController.groupIds,
delegate: widget.delegate, delegate: widget.delegate,
columnsState: widget.columnsState, columnsState: widget.groupState,
); );
final reorderFlex = ReorderFlex( final reorderFlex = ReorderFlex(
@ -212,7 +208,7 @@ class _AppFlowyBoardContentState extends State<_AppFlowyBoardContent> {
dataSource: widget.dataController, dataSource: widget.dataController,
direction: Axis.horizontal, direction: Axis.horizontal,
interceptor: interceptor, interceptor: interceptor,
reorderable: false, reorderable: true,
children: _buildColumns(), children: _buildColumns(),
); );
@ -257,18 +253,16 @@ class _AppFlowyBoardContentState extends State<_AppFlowyBoardContent> {
dataController: widget.dataController, dataController: widget.dataController,
); );
if (_reorderFlexKeys[columnData.id] == null) { final reorderFlexAction = ReorderFlexActionImpl();
_reorderFlexKeys[columnData.id] = GlobalObjectKey(columnData.id); widget.groupState.reorderFlexActionMap[columnData.id] =
} reorderFlexAction;
GlobalObjectKey reorderFlexKey = _reorderFlexKeys[columnData.id]!;
return ChangeNotifierProvider.value( return ChangeNotifierProvider.value(
key: ValueKey(columnData.id), key: ValueKey(columnData.id),
value: widget.dataController.getGroupController(columnData.id), value: widget.dataController.getGroupController(columnData.id),
child: Consumer<AppFlowyGroupController>( child: Consumer<AppFlowyGroupController>(
builder: (context, value, child) { builder: (context, value, child) {
final boardColumn = AppFlowyBoardGroup( final boardColumn = AppFlowyBoardGroup(
reorderFlexKey: reorderFlexKey,
// key: PageStorageKey<String>(columnData.id), // key: PageStorageKey<String>(columnData.id),
margin: _marginFromIndex(columnIndex), margin: _marginFromIndex(columnIndex),
itemMargin: widget.config.groupItemPadding, itemMargin: widget.config.groupItemPadding,
@ -281,11 +275,11 @@ class _AppFlowyBoardContentState extends State<_AppFlowyBoardContent> {
onReorder: widget.dataController.moveGroupItem, onReorder: widget.dataController.moveGroupItem,
cornerRadius: widget.config.cornerRadius, cornerRadius: widget.config.cornerRadius,
backgroundColor: widget.config.groupBackgroundColor, backgroundColor: widget.config.groupBackgroundColor,
dragStateStorage: widget.columnsState, dragStateStorage: widget.groupState,
dragTargetIndexKeyStorage: widget.columnsState, dragTargetKeys: widget.groupState,
reorderFlexAction: reorderFlexAction,
); );
widget.columnsState.addGroup(columnData.id, boardColumn);
return ConstrainedBox( return ConstrainedBox(
constraints: widget.groupConstraints, constraints: widget.groupConstraints,
child: boardColumn, child: boardColumn,
@ -356,71 +350,61 @@ class AppFlowyGroupContext {
} }
class AppFlowyBoardState extends DraggingStateStorage class AppFlowyBoardState extends DraggingStateStorage
with ReorderDragTargetIndexKeyStorage { with ReorderDragTargeKeys {
final Map<String, DraggingState> groupDragStates = {};
final Map<String, Map<String, GlobalObjectKey>> groupDragTargetKeys = {};
/// Quick access to the [AppFlowyBoardGroup], the [GlobalKey] is bind to the /// Quick access to the [AppFlowyBoardGroup], the [GlobalKey] is bind to the
/// AppFlowyBoardGroup's [ReorderFlex] widget. /// AppFlowyBoardGroup's [ReorderFlex] widget.
final Map<String, GlobalKey> groupReorderFlexKeys = {}; final Map<String, ReorderFlexActionImpl> reorderFlexActionMap = {};
final Map<String, DraggingState> groupDragStates = {};
final Map<String, Map<String, GlobalObjectKey>> 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;
}
@override @override
DraggingState? read(String reorderFlexId) { DraggingState? readState(String reorderFlexId) {
return groupDragStates[reorderFlexId]; return groupDragStates[reorderFlexId];
} }
@override @override
void write(String reorderFlexId, DraggingState state) { void insertState(String reorderFlexId, DraggingState state) {
Log.trace('$reorderFlexId Write dragging state: $state'); Log.trace('$reorderFlexId Write dragging state: $state');
groupDragStates[reorderFlexId] = state; groupDragStates[reorderFlexId] = state;
} }
@override @override
void remove(String reorderFlexId) { void removeState(String reorderFlexId) {
groupDragStates.remove(reorderFlexId); groupDragStates.remove(reorderFlexId);
} }
@override @override
void addKey( void insertDragTarget(
String reorderFlexId, String reorderFlexId,
String key, String key,
GlobalObjectKey<State<StatefulWidget>> value, GlobalObjectKey<State<StatefulWidget>> value,
) { ) {
Map<String, GlobalObjectKey>? group = groupDragDragTargets[reorderFlexId]; Map<String, GlobalObjectKey>? group = groupDragTargetKeys[reorderFlexId];
if (group == null) { if (group == null) {
group = {}; group = {};
groupDragDragTargets[reorderFlexId] = group; groupDragTargetKeys[reorderFlexId] = group;
} }
group[key] = value; group[key] = value;
} }
@override @override
GlobalObjectKey<State<StatefulWidget>>? readKey( GlobalObjectKey<State<StatefulWidget>>? getDragTarget(
String reorderFlexId, String key) { String reorderFlexId,
Map<String, GlobalObjectKey>? group = groupDragDragTargets[reorderFlexId]; String key,
) {
Map<String, GlobalObjectKey>? group = groupDragTargetKeys[reorderFlexId];
if (group != null) { if (group != null) {
return group[key]; return group[key];
} else { } else {
return null; return null;
} }
} }
@override
void removeDragTarget(String reorderFlexId) {
groupDragTargetKeys.remove(reorderFlexId);
}
} }
class ReorderFlexActionImpl extends ReorderFlexAction {}

View File

@ -90,21 +90,21 @@ class AppFlowyBoardGroup extends StatefulWidget {
final DraggingStateStorage? dragStateStorage; final DraggingStateStorage? dragStateStorage;
final ReorderDragTargetIndexKeyStorage? dragTargetIndexKeyStorage; final ReorderDragTargeKeys? dragTargetKeys;
final GlobalObjectKey reorderFlexKey; final ReorderFlexAction? reorderFlexAction;
const AppFlowyBoardGroup({ const AppFlowyBoardGroup({
Key? key, Key? key,
required this.reorderFlexKey,
this.headerBuilder, this.headerBuilder,
this.footerBuilder, this.footerBuilder,
required this.cardBuilder, required this.cardBuilder,
required this.onReorder, required this.onReorder,
required this.dataSource, required this.dataSource,
required this.phantomController, required this.phantomController,
this.reorderFlexAction,
this.dragStateStorage, this.dragStateStorage,
this.dragTargetIndexKeyStorage, this.dragTargetKeys,
this.scrollController, this.scrollController,
this.onDragStarted, this.onDragStarted,
this.onDragEnded, this.onDragEnded,
@ -146,9 +146,9 @@ class _AppFlowyBoardGroupState extends State<AppFlowyBoardGroup> {
); );
Widget reorderFlex = ReorderFlex( Widget reorderFlex = ReorderFlex(
key: widget.reorderFlexKey, key: ValueKey(widget.groupId),
dragStateStorage: widget.dragStateStorage, dragStateStorage: widget.dragStateStorage,
dragTargetIndexKeyStorage: widget.dragTargetIndexKeyStorage, dragTargetKeys: widget.dragTargetKeys,
scrollController: widget.scrollController, scrollController: widget.scrollController,
config: widget.config, config: widget.config,
onDragStarted: (index) { onDragStarted: (index) {
@ -168,6 +168,7 @@ class _AppFlowyBoardGroupState extends State<AppFlowyBoardGroup> {
}, },
dataSource: widget.dataSource, dataSource: widget.dataSource,
interceptor: interceptor, interceptor: interceptor,
reorderFlexAction: widget.reorderFlexAction,
children: children, children: children,
); );

View File

@ -41,7 +41,7 @@ class AppFlowyGroupController extends ChangeNotifier with EquatableMixin {
void updateGroupName(String newName) { void updateGroupName(String newName) {
if (groupData.headerData.groupName != newName) { if (groupData.headerData.groupName != newName) {
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'); Log.debug('[$AppFlowyGroupController] $groupData remove item at $index');
final item = groupData._items.removeAt(index); final item = groupData._items.removeAt(index);
if (notify) { if (notify) {
notifyListeners(); _notify();
} }
return item; return item;
} }
@ -81,7 +81,7 @@ class AppFlowyGroupController extends ChangeNotifier with EquatableMixin {
'[$AppFlowyGroupController] $groupData move item from $fromIndex to $toIndex'); '[$AppFlowyGroupController] $groupData move item from $fromIndex to $toIndex');
final item = groupData._items.removeAt(fromIndex); final item = groupData._items.removeAt(fromIndex);
groupData._items.insert(toIndex, item); groupData._items.insert(toIndex, item);
notifyListeners(); _notify();
return true; return true;
} }
@ -102,7 +102,7 @@ class AppFlowyGroupController extends ChangeNotifier with EquatableMixin {
groupData._items.add(item); groupData._items.add(item);
} }
if (notify) notifyListeners(); if (notify) _notify();
return true; return true;
} }
} }
@ -112,7 +112,7 @@ class AppFlowyGroupController extends ChangeNotifier with EquatableMixin {
return false; return false;
} else { } else {
groupData._items.add(item); groupData._items.add(item);
if (notify) notifyListeners(); if (notify) _notify();
return true; return true;
} }
} }
@ -135,7 +135,7 @@ class AppFlowyGroupController extends ChangeNotifier with EquatableMixin {
'[$AppFlowyGroupController] $groupData replace $removedItem with $newItem at $index'); '[$AppFlowyGroupController] $groupData replace $removedItem with $newItem at $index');
} }
notifyListeners(); _notify();
} }
void replaceOrInsertItem(AppFlowyGroupItem newItem) { void replaceOrInsertItem(AppFlowyGroupItem newItem) {
@ -143,10 +143,10 @@ class AppFlowyGroupController extends ChangeNotifier with EquatableMixin {
if (index != -1) { if (index != -1) {
groupData._items.removeAt(index); groupData._items.removeAt(index);
groupData._items.insert(index, newItem); groupData._items.insert(index, newItem);
notifyListeners(); _notify();
} else { } else {
groupData._items.add(newItem); 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) != return groupData._items.indexWhere((element) => element.id == item.id) !=
-1; -1;
} }
void _notify() {
notifyListeners();
}
} }
/// [AppFlowyGroupData] represents the data of each group of the Board. /// [AppFlowyGroupData] represents the data of each group of the Board.

View File

@ -69,9 +69,9 @@ class FlexDragTargetData extends DragTargetData {
} }
abstract class DraggingStateStorage { abstract class DraggingStateStorage {
void write(String reorderFlexId, DraggingState state); void insertState(String reorderFlexId, DraggingState state);
void remove(String reorderFlexId); void removeState(String reorderFlexId);
DraggingState? read(String reorderFlexId); DraggingState? readState(String reorderFlexId);
} }
class DraggingState { class DraggingState {

View File

@ -73,11 +73,15 @@ class ReorderDragTarget<T extends DragTargetData> extends StatefulWidget {
final ReorderFlexDraggableTargetBuilder? draggableTargetBuilder; final ReorderFlexDraggableTargetBuilder? draggableTargetBuilder;
final AnimationController insertAnimationController; final AnimationController insertAnimationController;
final AnimationController deleteAnimationController; final AnimationController deleteAnimationController;
final bool useMoveAnimation; final bool useMoveAnimation;
final bool draggable; final bool draggable;
final double draggingOpacity;
const ReorderDragTarget({ const ReorderDragTarget({
Key? key, Key? key,
required this.child, required this.child,
@ -94,6 +98,7 @@ class ReorderDragTarget<T extends DragTargetData> extends StatefulWidget {
this.onAccept, this.onAccept,
this.onLeave, this.onLeave,
this.draggableTargetBuilder, this.draggableTargetBuilder,
this.draggingOpacity = 0.3,
}) : super(key: key); }) : super(key: key);
@override @override
@ -164,6 +169,7 @@ class _ReorderDragTargetState<T extends DragTargetData>
feedback: feedbackBuilder, feedback: feedbackBuilder,
childWhenDragging: IgnorePointerWidget( childWhenDragging: IgnorePointerWidget(
useIntrinsicSize: !widget.useMoveAnimation, useIntrinsicSize: !widget.useMoveAnimation,
opacity: widget.draggingOpacity,
child: widget.child, child: widget.child,
), ),
onDragStarted: () { onDragStarted: () {
@ -195,7 +201,10 @@ class _ReorderDragTargetState<T extends DragTargetData>
} }
Widget _buildDraggableFeedback( Widget _buildDraggableFeedback(
BuildContext context, BoxConstraints constraints, Widget child) { BuildContext context,
BoxConstraints constraints,
Widget child,
) {
return Transform( return Transform(
transform: Matrix4.rotationZ(0), transform: Matrix4.rotationZ(0),
alignment: FractionalOffset.topLeft, alignment: FractionalOffset.topLeft,
@ -205,7 +214,7 @@ class _ReorderDragTargetState<T extends DragTargetData>
clipBehavior: Clip.hardEdge, clipBehavior: Clip.hardEdge,
child: ConstrainedBox( child: ConstrainedBox(
constraints: constraints, 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 { class IgnorePointerWidget extends StatelessWidget {
final Widget? child; final Widget? child;
final bool useIntrinsicSize; final bool useIntrinsicSize;
final double opacity;
const IgnorePointerWidget({ const IgnorePointerWidget({
required this.child, required this.child,
required this.opacity,
this.useIntrinsicSize = false, this.useIntrinsicSize = false,
Key? key, Key? key,
}) : super(key: key); }) : super(key: key);
@ -286,11 +298,10 @@ class IgnorePointerWidget extends StatelessWidget {
? child ? child
: SizedBox(width: 0.0, height: 0.0, child: child); : SizedBox(width: 0.0, height: 0.0, child: child);
final opacity = useIntrinsicSize ? 0.3 : 0.0;
return IgnorePointer( return IgnorePointer(
ignoring: true, ignoring: true,
child: Opacity( child: Opacity(
opacity: opacity, opacity: useIntrinsicSize ? opacity : 0.0,
child: sizedChild, child: sizedChild,
), ),
); );
@ -300,8 +311,10 @@ class IgnorePointerWidget extends StatelessWidget {
class AbsorbPointerWidget extends StatelessWidget { class AbsorbPointerWidget extends StatelessWidget {
final Widget? child; final Widget? child;
final bool useIntrinsicSize; final bool useIntrinsicSize;
final double opacity;
const AbsorbPointerWidget({ const AbsorbPointerWidget({
required this.child, required this.child,
required this.opacity,
this.useIntrinsicSize = false, this.useIntrinsicSize = false,
Key? key, Key? key,
}) : super(key: key); }) : super(key: key);
@ -312,10 +325,9 @@ class AbsorbPointerWidget extends StatelessWidget {
? child ? child
: SizedBox(width: 0.0, height: 0.0, child: child); : SizedBox(width: 0.0, height: 0.0, child: child);
final opacity = useIntrinsicSize ? 0.3 : 0.0;
return AbsorbPointer( return AbsorbPointer(
child: Opacity( child: Opacity(
opacity: opacity, opacity: useIntrinsicSize ? opacity : 0.0,
child: sizedChild, child: sizedChild,
), ),
); );
@ -494,6 +506,7 @@ class _FakeDragTargetState<T extends DragTargetData>
sizeFactor: widget.deleteAnimationController, sizeFactor: widget.deleteAnimationController,
axis: Axis.vertical, axis: Axis.vertical,
child: AbsorbPointerWidget( child: AbsorbPointerWidget(
opacity: 0.3,
child: widget.child, child: widget.child,
), ),
); );
@ -503,6 +516,7 @@ class _FakeDragTargetState<T extends DragTargetData>
axis: Axis.vertical, axis: Axis.vertical,
child: AbsorbPointerWidget( child: AbsorbPointerWidget(
useIntrinsicSize: true, useIntrinsicSize: true,
opacity: 0.3,
child: widget.child, child: widget.child,
), ),
); );

View File

@ -81,7 +81,7 @@ class OverlappingDragTargetInterceptor extends DragTargetInterceptor {
delegate.cancel(); delegate.cancel();
} else { } else {
// Ignore the event if the dragTarget overlaps with the other column's dragTargets. // 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) { if (columnKeys != null) {
final keys = columnKeys.values.toList(); final keys = columnKeys.values.toList();
if (dragTargetData.isOverlapWithWidgets(keys)) { if (dragTargetData.isOverlapWithWidgets(keys)) {
@ -102,8 +102,7 @@ class OverlappingDragTargetInterceptor extends DragTargetInterceptor {
delegate.dragTargetDidMoveToReorderFlex( delegate.dragTargetDidMoveToReorderFlex(
dragTargetId, dragTargetData, index); dragTargetId, dragTargetData, index);
columnsState columnsState.reorderFlexActionMap[dragTargetId]
.getReorderFlexState(groupId: dragTargetId)
?.resetDragTargetIndex(index); ?.resetDragTargetIndex(index);
} }
}); });

View File

@ -31,9 +31,32 @@ abstract class ReoderFlexItem {
String get id; String get id;
} }
abstract class ReorderDragTargetIndexKeyStorage { /// Cache each dragTarget's key.
void addKey(String reorderFlexId, String key, GlobalObjectKey value); /// For the moment, the key is used to locate the render object that will
GlobalObjectKey? readKey(String reorderFlexId, String key); /// 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 { class ReorderFlexConfig {
@ -78,9 +101,12 @@ class ReorderFlex extends StatefulWidget {
final DragTargetInterceptor? interceptor; final DragTargetInterceptor? interceptor;
/// Save the [DraggingState] if the current [ReorderFlex] get reinitialize.
final DraggingStateStorage? dragStateStorage; final DraggingStateStorage? dragStateStorage;
final ReorderDragTargetIndexKeyStorage? dragTargetIndexKeyStorage; final ReorderDragTargeKeys? dragTargetKeys;
final ReorderFlexAction? reorderFlexAction;
final bool reorderable; final bool reorderable;
@ -93,10 +119,11 @@ class ReorderFlex extends StatefulWidget {
required this.onReorder, required this.onReorder,
this.reorderable = true, this.reorderable = true,
this.dragStateStorage, this.dragStateStorage,
this.dragTargetIndexKeyStorage, this.dragTargetKeys,
this.onDragStarted, this.onDragStarted,
this.onDragEnded, this.onDragEnded,
this.interceptor, this.interceptor,
this.reorderFlexAction,
this.direction = Axis.vertical, this.direction = Axis.vertical,
}) : assert(children.every((Widget w) => w.key != null), }) : assert(children.every((Widget w) => w.key != null),
'All child must have a key.'), 'All child must have a key.'),
@ -109,7 +136,7 @@ class ReorderFlex extends StatefulWidget {
} }
class ReorderFlexState extends State<ReorderFlex> class ReorderFlexState extends State<ReorderFlex>
with ReorderFlexMinxi, TickerProviderStateMixin<ReorderFlex> { with ReorderFlexMixin, TickerProviderStateMixin<ReorderFlex> {
/// Controls scrolls and measures scroll progress. /// Controls scrolls and measures scroll progress.
late ScrollController _scrollController; late ScrollController _scrollController;
@ -131,11 +158,11 @@ class ReorderFlexState extends State<ReorderFlex>
void initState() { void initState() {
_notifier = ReorderFlexNotifier(); _notifier = ReorderFlexNotifier();
final flexId = widget.reorderFlexId; final flexId = widget.reorderFlexId;
dragState = widget.dragStateStorage?.read(flexId) ?? dragState = widget.dragStateStorage?.readState(flexId) ??
DraggingState(widget.reorderFlexId); DraggingState(widget.reorderFlexId);
Log.trace('[DragTarget] init dragState: $dragState'); Log.trace('[DragTarget] init dragState: $dragState');
widget.dragStateStorage?.remove(flexId); widget.dragStateStorage?.removeState(flexId);
_animation = DragTargetAnimation( _animation = DragTargetAnimation(
reorderAnimationDuration: widget.config.reorderAnimationDuration, reorderAnimationDuration: widget.config.reorderAnimationDuration,
@ -148,6 +175,14 @@ class ReorderFlexState extends State<ReorderFlex>
vsync: this, vsync: this,
); );
widget.reorderFlexAction?._scrollToBottom = (fn) {
scrollToBottom(fn);
};
widget.reorderFlexAction?._resetDragTargetIndex = (index) {
resetDragTargetIndex(index);
};
super.initState(); super.initState();
} }
@ -184,7 +219,7 @@ class ReorderFlexState extends State<ReorderFlex>
final indexKey = GlobalObjectKey(child.key!); final indexKey = GlobalObjectKey(child.key!);
// Save the index key for quick access // Save the index key for quick access
widget.dragTargetIndexKeyStorage?.addKey( widget.dragTargetKeys?.insertDragTarget(
widget.reorderFlexId, widget.reorderFlexId,
item.id, item.id,
indexKey, indexKey,
@ -236,8 +271,12 @@ class ReorderFlexState extends State<ReorderFlex>
/// [childIndex]: the index of the child in a list /// [childIndex]: the index of the child in a list
Widget _wrap(Widget child, int childIndex, GlobalObjectKey indexKey) { Widget _wrap(Widget child, int childIndex, GlobalObjectKey indexKey) {
return Builder(builder: (context) { return Builder(builder: (context) {
final ReorderDragTarget dragTarget = final ReorderDragTarget dragTarget = _buildDragTarget(
_buildDragTarget(context, child, childIndex, indexKey); context,
child,
childIndex,
indexKey,
);
int shiftedIndex = childIndex; int shiftedIndex = childIndex;
if (dragState.isOverlapWithPhantom()) { if (dragState.isOverlapWithPhantom()) {
@ -342,6 +381,15 @@ class ReorderFlexState extends State<ReorderFlex>
}); });
} }
static ReorderFlexState of(BuildContext context) {
if (context is StatefulElement && context.state is ReorderFlexState) {
return context.state as ReorderFlexState;
}
final ReorderFlexState? result =
context.findAncestorStateOfType<ReorderFlexState>();
return result!;
}
ReorderDragTarget _buildDragTarget( ReorderDragTarget _buildDragTarget(
BuildContext builderContext, BuildContext builderContext,
Widget child, Widget child,
@ -364,7 +412,7 @@ class ReorderFlexState extends State<ReorderFlex>
"[DragTarget] Group:[${widget.dataSource.identifier}] start dragging item at $draggingIndex"); "[DragTarget] Group:[${widget.dataSource.identifier}] start dragging item at $draggingIndex");
_startDragging(draggingWidget, draggingIndex, size); _startDragging(draggingWidget, draggingIndex, size);
widget.onDragStarted?.call(draggingIndex); widget.onDragStarted?.call(draggingIndex);
widget.dragStateStorage?.remove(widget.reorderFlexId); widget.dragStateStorage?.removeState(widget.reorderFlexId);
}, },
onDragMoved: (dragTargetData, offset) { onDragMoved: (dragTargetData, offset) {
dragTargetData.dragTargetOffset = offset; dragTargetData.dragTargetOffset = offset;
@ -435,6 +483,7 @@ class ReorderFlexState extends State<ReorderFlex>
draggableTargetBuilder: widget.interceptor?.draggableTargetBuilder, draggableTargetBuilder: widget.interceptor?.draggableTargetBuilder,
useMoveAnimation: widget.config.useMoveAnimation, useMoveAnimation: widget.config.useMoveAnimation,
draggable: widget.reorderable, draggable: widget.reorderable,
draggingOpacity: widget.config.draggingWidgetOpacity,
child: child, child: child,
); );
} }
@ -487,7 +536,7 @@ class ReorderFlexState extends State<ReorderFlex>
} }
dragState.setStartDraggingIndex(dragTargetIndex); dragState.setStartDraggingIndex(dragTargetIndex);
widget.dragStateStorage?.write( widget.dragStateStorage?.insertState(
widget.reorderFlexId, widget.reorderFlexId,
dragState, dragState,
); );
@ -581,46 +630,46 @@ class ReorderFlexState extends State<ReorderFlex>
} }
} }
void scrollToBottom(VoidCallback? completed) { void scrollToBottom(void Function(BuildContext)? completed) {
if (_scrolling) { if (_scrolling) {
completed?.call(); completed?.call(context);
return; return;
} }
if (widget.dataSource.items.isNotEmpty) { if (widget.dataSource.items.isNotEmpty) {
final item = widget.dataSource.items.last; final item = widget.dataSource.items.last;
final indexKey = widget.dragTargetIndexKeyStorage?.readKey( final dragTargetKey = widget.dragTargetKeys?.getDragTarget(
widget.reorderFlexId, widget.reorderFlexId,
item.id, item.id,
); );
if (indexKey == null) { if (dragTargetKey == null) {
completed?.call(); completed?.call(context);
return; return;
} }
final indexContext = indexKey.currentContext; final dragTargetContext = dragTargetKey.currentContext;
if (indexContext == null || _scrollController.hasClients == false) { if (dragTargetContext == null || _scrollController.hasClients == false) {
completed?.call(); completed?.call(context);
return; return;
} }
final renderObject = indexContext.findRenderObject(); final dragTargetRenderObject = dragTargetContext.findRenderObject();
if (renderObject != null) { if (dragTargetRenderObject != null) {
_scrolling = true; _scrolling = true;
_scrollController.position _scrollController.position
.ensureVisible( .ensureVisible(
renderObject, dragTargetRenderObject,
alignment: 0.5, alignment: 0.5,
duration: const Duration(milliseconds: 120), duration: const Duration(milliseconds: 120),
) )
.then((value) { .then((value) {
setState(() { setState(() {
_scrolling = false; _scrolling = false;
completed?.call(); completed?.call(context);
}); });
}); });
} else { } else {
completed?.call(); completed?.call(context);
} }
} }
} }

View File

@ -3,7 +3,7 @@ import 'package:flutter/widgets.dart';
import '../transitions.dart'; import '../transitions.dart';
import 'drag_target.dart'; import 'drag_target.dart';
mixin ReorderFlexMinxi { mixin ReorderFlexMixin {
@protected @protected
Widget makeAppearingWidget( Widget makeAppearingWidget(
Widget child, Widget child,

View File

@ -1,6 +1,6 @@
name: appflowy_board name: appflowy_board
description: AppFlowyBoard is a board-style widget that consists of multi-groups. It supports drag and drop between different groups. 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 homepage: https://github.com/AppFlowy-IO/AppFlowy
repository: https://github.com/AppFlowy-IO/AppFlowy/tree/main/frontend/app_flowy/packages/appflowy_board repository: https://github.com/AppFlowy-IO/AppFlowy/tree/main/frontend/app_flowy/packages/appflowy_board

View File

@ -45,7 +45,7 @@ ShortcutEventHandler _ignorekHandler = (editorState, event) {
}; };
SelectionMenuItem codeBlockMenuItem = SelectionMenuItem( SelectionMenuItem codeBlockMenuItem = SelectionMenuItem(
name: 'Code Block', name: () => 'Code Block',
icon: const Icon(Icons.abc), icon: const Icon(Icons.abc),
keywords: ['code block'], keywords: ['code block'],
handler: (editorState, _, __) { handler: (editorState, _, __) {

View File

@ -24,7 +24,7 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos
SPEC CHECKSUMS: SPEC CHECKSUMS:
FlutterMacOS: 57701585bf7de1b3fc2bb61f6378d73bbdea8424 FlutterMacOS: ae6af50a8ea7d6103d888583d46bd8328a7e9811
path_provider_macos: 3c0c3b4b0d4a76d2bf989a913c2de869c5641a19 path_provider_macos: 3c0c3b4b0d4a76d2bf989a913c2de869c5641a19
rich_clipboard_macos: 43364b66b9dc69d203eb8dd6d758e2d12e02723c rich_clipboard_macos: 43364b66b9dc69d203eb8dd6d758e2d12e02723c
url_launcher_macos: 597e05b8e514239626bcf4a850fcf9ef5c856ec3 url_launcher_macos: 597e05b8e514239626bcf4a850fcf9ef5c856ec3

View File

@ -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": {}
}

View File

@ -1,35 +1,35 @@
{ {
"@@locale": "fr-CA", "@@locale": "fr-CA",
"bold": "", "bold": "gras",
"@bold": {}, "@bold": {},
"bulletedList": "", "bulletedList": "liste à puces",
"@bulletedList": {}, "@bulletedList": {},
"checkbox": "", "checkbox": "case à cocher",
"@checkbox": {}, "@checkbox": {},
"embedCode": "", "embedCode": "incorporer Code",
"@embedCode": {}, "@embedCode": {},
"heading1": "", "heading1": "en-tête1",
"@heading1": {}, "@heading1": {},
"heading2": "", "heading2": "en-tête2",
"@heading2": {}, "@heading2": {},
"heading3": "", "heading3": "en-tête3",
"@heading3": {}, "@heading3": {},
"highlight": "", "highlight": "mettre en évidence",
"@highlight": {}, "@highlight": {},
"image": "", "image": "limage",
"@image": {}, "@image": {},
"italic": "", "italic": "italique",
"@italic": {}, "@italic": {},
"link": "", "link": "lien",
"@link": {}, "@link": {},
"numberedList": "", "numberedList": "liste numérotée",
"@numberedList": {}, "@numberedList": {},
"quote": "", "quote": "citation",
"@quote": {}, "@quote": {},
"strikethrough": "", "strikethrough": "barré",
"@strikethrough": {}, "@strikethrough": {},
"text": "", "text": "texte",
"@text": {}, "@text": {},
"underline": "", "underline": "souligner",
"@underline": {} "@underline": {}
} }

View File

@ -1,35 +1,35 @@
{ {
"@@locale": "fr-FR", "@@locale": "fr-FR",
"bold": "", "bold": "Gras",
"@bold": {}, "@bold": {},
"bulletedList": "", "bulletedList": "List à puces",
"@bulletedList": {}, "@bulletedList": {},
"checkbox": "", "checkbox": "Case à cocher",
"@checkbox": {}, "@checkbox": {},
"embedCode": "", "embedCode": "Incorporer code",
"@embedCode": {}, "@embedCode": {},
"heading1": "", "heading1": "Titre 1",
"@heading1": {}, "@heading1": {},
"heading2": "", "heading2": "Titre 2",
"@heading2": {}, "@heading2": {},
"heading3": "", "heading3": "Titre 3",
"@heading3": {}, "@heading3": {},
"highlight": "", "highlight": "Surligné",
"@highlight": {}, "@highlight": {},
"image": "", "image": "Image",
"@image": {}, "@image": {},
"italic": "", "italic": "Italique",
"@italic": {}, "@italic": {},
"link": "", "link": "Lien",
"@link": {}, "@link": {},
"numberedList": "", "numberedList": "Liste numérotée",
"@numberedList": {}, "@numberedList": {},
"quote": "", "quote": "Citation",
"@quote": {}, "@quote": {},
"strikethrough": "", "strikethrough": "Barré",
"@strikethrough": {}, "@strikethrough": {},
"text": "", "text": "Texte",
"@text": {}, "@text": {},
"underline": "", "underline": "Souligné",
"@underline": {} "@underline": {}
} }

View File

@ -1,35 +1,35 @@
{ {
"@@locale": "hu-HU", "@@locale": "hu-HU",
"bold": "", "bold": "bátor",
"@bold": {}, "@bold": {},
"bulletedList": "", "bulletedList": "pontozott lista",
"@bulletedList": {}, "@bulletedList": {},
"checkbox": "", "checkbox": "jelölőnégyzetet",
"@checkbox": {}, "@checkbox": {},
"embedCode": "", "embedCode": "Beágyazás",
"@embedCode": {}, "@embedCode": {},
"heading1": "", "heading1": "címsor1",
"@heading1": {}, "@heading1": {},
"heading2": "", "heading2": "címsor2",
"@heading2": {}, "@heading2": {},
"heading3": "", "heading3": "címsor3",
"@heading3": {}, "@heading3": {},
"highlight": "", "highlight": "Kiemel",
"@highlight": {}, "@highlight": {},
"image": "", "image": "kép",
"@image": {}, "@image": {},
"italic": "", "italic": "dőlt",
"@italic": {}, "@italic": {},
"link": "", "link": "link",
"@link": {}, "@link": {},
"numberedList": "", "numberedList": "számozottLista",
"@numberedList": {}, "@numberedList": {},
"quote": "", "quote": "idézet",
"@quote": {}, "@quote": {},
"strikethrough": "", "strikethrough": "áthúzott",
"@strikethrough": {}, "@strikethrough": {},
"text": "", "text": "szöveg",
"@text": {}, "@text": {},
"underline": "", "underline": "aláhúzás",
"@underline": {} "@underline": {}
} }

View File

@ -1,35 +1,35 @@
{ {
"@@locale": "id-ID", "@@locale": "id-ID",
"bold": "", "bold": "berani",
"@bold": {}, "@bold": {},
"bulletedList": "", "bulletedList": "daftar berpoin",
"@bulletedList": {}, "@bulletedList": {},
"checkbox": "", "checkbox": "kotak centang",
"@checkbox": {}, "@checkbox": {},
"embedCode": "", "embedCode": "menyematkan Kode",
"@embedCode": {}, "@embedCode": {},
"heading1": "", "heading1": "pos1",
"@heading1": {}, "@heading1": {},
"heading2": "", "heading2": "pos2",
"@heading2": {}, "@heading2": {},
"heading3": "", "heading3": "pos3",
"@heading3": {}, "@heading3": {},
"highlight": "", "highlight": "menyorot",
"@highlight": {}, "@highlight": {},
"image": "", "image": "gambar",
"@image": {}, "@image": {},
"italic": "", "italic": "miring",
"@italic": {}, "@italic": {},
"link": "", "link": "tautan",
"@link": {}, "@link": {},
"numberedList": "", "numberedList": "daftar bernomor",
"@numberedList": {}, "@numberedList": {},
"quote": "", "quote": "mengutip",
"@quote": {}, "@quote": {},
"strikethrough": "", "strikethrough": "coret",
"@strikethrough": {}, "@strikethrough": {},
"text": "", "text": "teks",
"@text": {}, "@text": {},
"underline": "", "underline": "menggarisbawahi",
"@underline": {} "@underline": {}
} }

View File

@ -1,35 +1,35 @@
{ {
"@@locale": "it-IT", "@@locale": "it-IT",
"bold": "", "bold": "Grassetto",
"@bold": {}, "@bold": {},
"bulletedList": "", "bulletedList": "Elenco puntato",
"@bulletedList": {}, "@bulletedList": {},
"checkbox": "", "checkbox": "Casella di spunta",
"@checkbox": {}, "@checkbox": {},
"embedCode": "", "embedCode": "Incorpora codice",
"@embedCode": {}, "@embedCode": {},
"heading1": "", "heading1": "H1",
"@heading1": {}, "@heading1": {},
"heading2": "", "heading2": "H2",
"@heading2": {}, "@heading2": {},
"heading3": "", "heading3": "H3",
"@heading3": {}, "@heading3": {},
"highlight": "", "highlight": "Evidenzia",
"@highlight": {}, "@highlight": {},
"image": "", "image": "Immagine",
"@image": {}, "@image": {},
"italic": "", "italic": "Corsivo",
"@italic": {}, "@italic": {},
"link": "", "link": "Collegamento",
"@link": {}, "@link": {},
"numberedList": "", "numberedList": "Elenco numerato",
"@numberedList": {}, "@numberedList": {},
"quote": "", "quote": "Cita",
"@quote": {}, "@quote": {},
"strikethrough": "", "strikethrough": "Barrato",
"@strikethrough": {}, "@strikethrough": {},
"text": "", "text": "Testo",
"@text": {}, "@text": {},
"underline": "", "underline": "Sottolineato",
"@underline": {} "@underline": {}
} }

View File

@ -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": {}
}

View File

@ -1,35 +1,35 @@
{ {
"@@locale": "pt-BR", "@@locale": "pt-BR",
"bold": "", "bold": "Negrito",
"@bold": {}, "@bold": {},
"bulletedList": "", "bulletedList": "Lista de marcadores",
"@bulletedList": {}, "@bulletedList": {},
"checkbox": "", "checkbox": "Caixa de seleção",
"@checkbox": {}, "@checkbox": {},
"embedCode": "", "embedCode": "Código incorporado",
"@embedCode": {}, "@embedCode": {},
"heading1": "", "heading1": "H1",
"@heading1": {}, "@heading1": {},
"heading2": "", "heading2": "H2",
"@heading2": {}, "@heading2": {},
"heading3": "", "heading3": "H3",
"@heading3": {}, "@heading3": {},
"highlight": "", "highlight": "Destacar",
"@highlight": {}, "@highlight": {},
"image": "", "image": "Imagem",
"@image": {}, "@image": {},
"italic": "", "italic": "Itálico",
"@italic": {}, "@italic": {},
"link": "", "link": "Link",
"@link": {}, "@link": {},
"numberedList": "", "numberedList": "Lista numerada",
"@numberedList": {}, "@numberedList": {},
"quote": "", "quote": "Citar",
"@quote": {}, "@quote": {},
"strikethrough": "", "strikethrough": "Rasurar",
"@strikethrough": {}, "@strikethrough": {},
"text": "", "text": "Texto",
"@text": {}, "@text": {},
"underline": "", "underline": "Sublinhar",
"@underline": {} "@underline": {}
} }

View File

@ -1,35 +1,35 @@
{ {
"@@locale": "pt-PT", "@@locale": "pt-PT",
"bold": "", "bold": "negrito",
"@bold": {}, "@bold": {},
"bulletedList": "", "bulletedList": "lista com marcadores",
"@bulletedList": {}, "@bulletedList": {},
"checkbox": "", "checkbox": "caixa de seleção",
"@checkbox": {}, "@checkbox": {},
"embedCode": "", "embedCode": "Código embutido",
"@embedCode": {}, "@embedCode": {},
"heading1": "", "heading1": "Cabeçallho 1",
"@heading1": {}, "@heading1": {},
"heading2": "", "heading2": "Cabeçallho 2",
"@heading2": {}, "@heading2": {},
"heading3": "", "heading3": "Cabeçallho 3",
"@heading3": {}, "@heading3": {},
"highlight": "", "highlight": "realçar",
"@highlight": {}, "@highlight": {},
"image": "", "image": "imagem",
"@image": {}, "@image": {},
"italic": "", "italic": "itálico",
"@italic": {}, "@italic": {},
"link": "", "link": "link",
"@link": {}, "@link": {},
"numberedList": "", "numberedList": "lista numerada",
"@numberedList": {}, "@numberedList": {},
"quote": "", "quote": "citar",
"@quote": {}, "@quote": {},
"strikethrough": "", "strikethrough": "tachado",
"@strikethrough": {}, "@strikethrough": {},
"text": "", "text": "texto",
"@text": {}, "@text": {},
"underline": "", "underline": "sublinhado",
"@underline": {} "@underline": {}
} }

View File

@ -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<void> insertContextInText(
EditorState editorState,
int index,
String content, {
Path? path,
TextNode? textNode,
}) async {
final result = getTextNodeToBeFormatted(
editorState,
path: path,
textNode: textNode,
);
final completer = Completer<void>();
TransactionBuilder(editorState)
..insertText(result, index, content)
..commit();
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
completer.complete();
});
return completer.future;
}

View File

@ -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<void> 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<void> formatTextToCheckbox(
EditorState editorState,
bool check, {
Path? path,
TextNode? textNode,
}) async {
return formatBuiltInTextAttributes(
editorState,
BuiltInAttributeKey.checkbox,
{
BuiltInAttributeKey.checkbox: check,
},
path: path,
textNode: textNode,
);
}
Future<void> formatLinkInText(
EditorState editorState,
String? link, {
Path? path,
TextNode? textNode,
}) async {
return formatBuiltInTextAttributes(
editorState,
BuiltInAttributeKey.href,
{
BuiltInAttributeKey.href: link,
},
path: path,
textNode: textNode,
);
}

View File

@ -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<void> updateTextNodeAttributes(
EditorState editorState,
Attributes attributes, {
Path? path,
TextNode? textNode,
}) async {
final result = getTextNodeToBeFormatted(
editorState,
path: path,
textNode: textNode,
);
final completer = Completer<void>();
TransactionBuilder(editorState)
..updateNode(result, attributes)
..commit();
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
completer.complete();
});
return completer.future;
}
Future<void> 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<void>();
TransactionBuilder(editorState)
..formatText(
result,
newSelection.startIndex,
newSelection.length,
attributes,
)
..commit();
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
completer.complete();
});
return completer.future;
}

View File

@ -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;
}

View File

@ -53,6 +53,10 @@ class Selection {
Selection get reversed => copyWith(start: end, end: start); 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}) { Selection collapse({bool atStart = false}) {
if (atStart) { if (atStart) {
return Selection(start: start, end: start); return Selection(start: start, end: start);

View File

@ -72,6 +72,8 @@ class EditorState {
// TODO: only for testing. // TODO: only for testing.
bool disableSealTimer = false; bool disableSealTimer = false;
bool editable = true;
Selection? get cursorSelection { Selection? get cursorSelection {
return _cursorSelection; return _cursorSelection;
} }
@ -112,6 +114,9 @@ class EditorState {
/// should record the transaction in undo/redo stack. /// should record the transaction in undo/redo stack.
apply(Transaction transaction, apply(Transaction transaction,
[ApplyOptions options = const ApplyOptions()]) { [ApplyOptions options = const ApplyOptions()]) {
if (!editable) {
return;
}
// TODO: validate the transation. // TODO: validate the transation.
for (final op in transaction.operations) { for (final op in transaction.operations) {
_applyOperation(op); _applyOperation(op);

View File

@ -54,6 +54,11 @@ extension TextNodeExtension on TextNode {
return value == true; return value == true;
}); });
bool allSatisfyCodeInSelection(Selection selection) =>
allSatisfyInSelection(selection, BuiltInAttributeKey.code, (value) {
return value == true;
});
bool allSatisfyInSelection( bool allSatisfyInSelection(
Selection selection, Selection selection,
String styleKey, String styleKey,

View File

@ -16,6 +16,7 @@ import 'package:intl/message_lookup_by_library.dart';
import 'package:intl/src/intl_helpers.dart'; import 'package:intl/src/intl_helpers.dart';
import 'messages_ca.dart' as messages_ca; 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_de-DE.dart' as messages_de_de;
import 'messages_en.dart' as messages_en; import 'messages_en.dart' as messages_en;
import 'messages_es-VE.dart' as messages_es_ve; 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_id-ID.dart' as messages_id_id;
import 'messages_it-IT.dart' as messages_it_it; import 'messages_it-IT.dart' as messages_it_it;
import 'messages_ja-JP.dart' as messages_ja_jp; 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_pl-PL.dart' as messages_pl_pl;
import 'messages_pt-BR.dart' as messages_pt_br; import 'messages_pt-BR.dart' as messages_pt_br;
import 'messages_pt-PT.dart' as messages_pt_pt; import 'messages_pt-PT.dart' as messages_pt_pt;
@ -36,6 +38,7 @@ import 'messages_zh-TW.dart' as messages_zh_tw;
typedef Future<dynamic> LibraryLoader(); typedef Future<dynamic> LibraryLoader();
Map<String, LibraryLoader> _deferredLibraries = { Map<String, LibraryLoader> _deferredLibraries = {
'ca': () => new Future.value(null), 'ca': () => new Future.value(null),
'cs_CZ': () => new Future.value(null),
'de_DE': () => new Future.value(null), 'de_DE': () => new Future.value(null),
'en': () => new Future.value(null), 'en': () => new Future.value(null),
'es_VE': () => new Future.value(null), 'es_VE': () => new Future.value(null),
@ -45,6 +48,7 @@ Map<String, LibraryLoader> _deferredLibraries = {
'id_ID': () => new Future.value(null), 'id_ID': () => new Future.value(null),
'it_IT': () => new Future.value(null), 'it_IT': () => new Future.value(null),
'ja_JP': () => new Future.value(null), 'ja_JP': () => new Future.value(null),
'nl_NL': () => new Future.value(null),
'pl_PL': () => new Future.value(null), 'pl_PL': () => new Future.value(null),
'pt_BR': () => new Future.value(null), 'pt_BR': () => new Future.value(null),
'pt_PT': () => new Future.value(null), 'pt_PT': () => new Future.value(null),
@ -58,6 +62,8 @@ MessageLookupByLibrary? _findExact(String localeName) {
switch (localeName) { switch (localeName) {
case 'ca': case 'ca':
return messages_ca.messages; return messages_ca.messages;
case 'cs_CZ':
return messages_cs_cz.messages;
case 'de_DE': case 'de_DE':
return messages_de_de.messages; return messages_de_de.messages;
case 'en': case 'en':
@ -76,6 +82,8 @@ MessageLookupByLibrary? _findExact(String localeName) {
return messages_it_it.messages; return messages_it_it.messages;
case 'ja_JP': case 'ja_JP':
return messages_ja_jp.messages; return messages_ja_jp.messages;
case 'nl_NL':
return messages_nl_nl.messages;
case 'pl_PL': case 'pl_PL':
return messages_pl_pl.messages; return messages_pl_pl.messages;
case 'pt_BR': case 'pt_BR':

View File

@ -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<dynamic> args);
class MessageLookup extends MessageLookupByLibrary {
String get localeName => 'cs_CZ';
final messages = _notInlinedMessages(_notInlinedMessages);
static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
"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í")
};
}

View File

@ -22,21 +22,21 @@ class MessageLookup extends MessageLookupByLibrary {
final messages = _notInlinedMessages(_notInlinedMessages); final messages = _notInlinedMessages(_notInlinedMessages);
static Map<String, Function> _notInlinedMessages(_) => <String, Function>{ static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
"bold": MessageLookupByLibrary.simpleMessage(""), "bold": MessageLookupByLibrary.simpleMessage("gras"),
"bulletedList": MessageLookupByLibrary.simpleMessage(""), "bulletedList": MessageLookupByLibrary.simpleMessage("liste à puces"),
"checkbox": MessageLookupByLibrary.simpleMessage(""), "checkbox": MessageLookupByLibrary.simpleMessage("case à cocher"),
"embedCode": MessageLookupByLibrary.simpleMessage(""), "embedCode": MessageLookupByLibrary.simpleMessage("incorporer Code"),
"heading1": MessageLookupByLibrary.simpleMessage(""), "heading1": MessageLookupByLibrary.simpleMessage("en-tête1"),
"heading2": MessageLookupByLibrary.simpleMessage(""), "heading2": MessageLookupByLibrary.simpleMessage("en-tête2"),
"heading3": MessageLookupByLibrary.simpleMessage(""), "heading3": MessageLookupByLibrary.simpleMessage("en-tête3"),
"highlight": MessageLookupByLibrary.simpleMessage(""), "highlight": MessageLookupByLibrary.simpleMessage("mettre en évidence"),
"image": MessageLookupByLibrary.simpleMessage(""), "image": MessageLookupByLibrary.simpleMessage("limage"),
"italic": MessageLookupByLibrary.simpleMessage(""), "italic": MessageLookupByLibrary.simpleMessage("italique"),
"link": MessageLookupByLibrary.simpleMessage(""), "link": MessageLookupByLibrary.simpleMessage("lien"),
"numberedList": MessageLookupByLibrary.simpleMessage(""), "numberedList": MessageLookupByLibrary.simpleMessage("liste numérotée"),
"quote": MessageLookupByLibrary.simpleMessage(""), "quote": MessageLookupByLibrary.simpleMessage("citation"),
"strikethrough": MessageLookupByLibrary.simpleMessage(""), "strikethrough": MessageLookupByLibrary.simpleMessage("barré"),
"text": MessageLookupByLibrary.simpleMessage(""), "text": MessageLookupByLibrary.simpleMessage("texte"),
"underline": MessageLookupByLibrary.simpleMessage("") "underline": MessageLookupByLibrary.simpleMessage("souligner")
}; };
} }

View File

@ -22,21 +22,21 @@ class MessageLookup extends MessageLookupByLibrary {
final messages = _notInlinedMessages(_notInlinedMessages); final messages = _notInlinedMessages(_notInlinedMessages);
static Map<String, Function> _notInlinedMessages(_) => <String, Function>{ static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
"bold": MessageLookupByLibrary.simpleMessage(""), "bold": MessageLookupByLibrary.simpleMessage("Gras"),
"bulletedList": MessageLookupByLibrary.simpleMessage(""), "bulletedList": MessageLookupByLibrary.simpleMessage("List à puces"),
"checkbox": MessageLookupByLibrary.simpleMessage(""), "checkbox": MessageLookupByLibrary.simpleMessage("Case à cocher"),
"embedCode": MessageLookupByLibrary.simpleMessage(""), "embedCode": MessageLookupByLibrary.simpleMessage("Incorporer code"),
"heading1": MessageLookupByLibrary.simpleMessage(""), "heading1": MessageLookupByLibrary.simpleMessage("Titre 1"),
"heading2": MessageLookupByLibrary.simpleMessage(""), "heading2": MessageLookupByLibrary.simpleMessage("Titre 2"),
"heading3": MessageLookupByLibrary.simpleMessage(""), "heading3": MessageLookupByLibrary.simpleMessage("Titre 3"),
"highlight": MessageLookupByLibrary.simpleMessage(""), "highlight": MessageLookupByLibrary.simpleMessage("Surligné"),
"image": MessageLookupByLibrary.simpleMessage(""), "image": MessageLookupByLibrary.simpleMessage("Image"),
"italic": MessageLookupByLibrary.simpleMessage(""), "italic": MessageLookupByLibrary.simpleMessage("Italique"),
"link": MessageLookupByLibrary.simpleMessage(""), "link": MessageLookupByLibrary.simpleMessage("Lien"),
"numberedList": MessageLookupByLibrary.simpleMessage(""), "numberedList": MessageLookupByLibrary.simpleMessage("Liste numérotée"),
"quote": MessageLookupByLibrary.simpleMessage(""), "quote": MessageLookupByLibrary.simpleMessage("Citation"),
"strikethrough": MessageLookupByLibrary.simpleMessage(""), "strikethrough": MessageLookupByLibrary.simpleMessage("Barré"),
"text": MessageLookupByLibrary.simpleMessage(""), "text": MessageLookupByLibrary.simpleMessage("Texte"),
"underline": MessageLookupByLibrary.simpleMessage("") "underline": MessageLookupByLibrary.simpleMessage("Souligné")
}; };
} }

View File

@ -22,21 +22,21 @@ class MessageLookup extends MessageLookupByLibrary {
final messages = _notInlinedMessages(_notInlinedMessages); final messages = _notInlinedMessages(_notInlinedMessages);
static Map<String, Function> _notInlinedMessages(_) => <String, Function>{ static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
"bold": MessageLookupByLibrary.simpleMessage(""), "bold": MessageLookupByLibrary.simpleMessage("bátor"),
"bulletedList": MessageLookupByLibrary.simpleMessage(""), "bulletedList": MessageLookupByLibrary.simpleMessage("pontozott lista"),
"checkbox": MessageLookupByLibrary.simpleMessage(""), "checkbox": MessageLookupByLibrary.simpleMessage("jelölőnégyzetet"),
"embedCode": MessageLookupByLibrary.simpleMessage(""), "embedCode": MessageLookupByLibrary.simpleMessage("Beágyazás"),
"heading1": MessageLookupByLibrary.simpleMessage(""), "heading1": MessageLookupByLibrary.simpleMessage("címsor1"),
"heading2": MessageLookupByLibrary.simpleMessage(""), "heading2": MessageLookupByLibrary.simpleMessage("címsor2"),
"heading3": MessageLookupByLibrary.simpleMessage(""), "heading3": MessageLookupByLibrary.simpleMessage("címsor3"),
"highlight": MessageLookupByLibrary.simpleMessage(""), "highlight": MessageLookupByLibrary.simpleMessage("Kiemel"),
"image": MessageLookupByLibrary.simpleMessage(""), "image": MessageLookupByLibrary.simpleMessage("kép"),
"italic": MessageLookupByLibrary.simpleMessage(""), "italic": MessageLookupByLibrary.simpleMessage("dőlt"),
"link": MessageLookupByLibrary.simpleMessage(""), "link": MessageLookupByLibrary.simpleMessage("link"),
"numberedList": MessageLookupByLibrary.simpleMessage(""), "numberedList": MessageLookupByLibrary.simpleMessage("számozottLista"),
"quote": MessageLookupByLibrary.simpleMessage(""), "quote": MessageLookupByLibrary.simpleMessage("idézet"),
"strikethrough": MessageLookupByLibrary.simpleMessage(""), "strikethrough": MessageLookupByLibrary.simpleMessage("áthúzott"),
"text": MessageLookupByLibrary.simpleMessage(""), "text": MessageLookupByLibrary.simpleMessage("szöveg"),
"underline": MessageLookupByLibrary.simpleMessage("") "underline": MessageLookupByLibrary.simpleMessage("aláhúzás")
}; };
} }

View File

@ -22,21 +22,21 @@ class MessageLookup extends MessageLookupByLibrary {
final messages = _notInlinedMessages(_notInlinedMessages); final messages = _notInlinedMessages(_notInlinedMessages);
static Map<String, Function> _notInlinedMessages(_) => <String, Function>{ static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
"bold": MessageLookupByLibrary.simpleMessage(""), "bold": MessageLookupByLibrary.simpleMessage("berani"),
"bulletedList": MessageLookupByLibrary.simpleMessage(""), "bulletedList": MessageLookupByLibrary.simpleMessage("daftar berpoin"),
"checkbox": MessageLookupByLibrary.simpleMessage(""), "checkbox": MessageLookupByLibrary.simpleMessage("kotak centang"),
"embedCode": MessageLookupByLibrary.simpleMessage(""), "embedCode": MessageLookupByLibrary.simpleMessage("menyematkan Kode"),
"heading1": MessageLookupByLibrary.simpleMessage(""), "heading1": MessageLookupByLibrary.simpleMessage("pos1"),
"heading2": MessageLookupByLibrary.simpleMessage(""), "heading2": MessageLookupByLibrary.simpleMessage("pos2"),
"heading3": MessageLookupByLibrary.simpleMessage(""), "heading3": MessageLookupByLibrary.simpleMessage("pos3"),
"highlight": MessageLookupByLibrary.simpleMessage(""), "highlight": MessageLookupByLibrary.simpleMessage("menyorot"),
"image": MessageLookupByLibrary.simpleMessage(""), "image": MessageLookupByLibrary.simpleMessage("gambar"),
"italic": MessageLookupByLibrary.simpleMessage(""), "italic": MessageLookupByLibrary.simpleMessage("miring"),
"link": MessageLookupByLibrary.simpleMessage(""), "link": MessageLookupByLibrary.simpleMessage("tautan"),
"numberedList": MessageLookupByLibrary.simpleMessage(""), "numberedList": MessageLookupByLibrary.simpleMessage("daftar bernomor"),
"quote": MessageLookupByLibrary.simpleMessage(""), "quote": MessageLookupByLibrary.simpleMessage("mengutip"),
"strikethrough": MessageLookupByLibrary.simpleMessage(""), "strikethrough": MessageLookupByLibrary.simpleMessage("coret"),
"text": MessageLookupByLibrary.simpleMessage(""), "text": MessageLookupByLibrary.simpleMessage("teks"),
"underline": MessageLookupByLibrary.simpleMessage("") "underline": MessageLookupByLibrary.simpleMessage("menggarisbawahi")
}; };
} }

View File

@ -22,21 +22,21 @@ class MessageLookup extends MessageLookupByLibrary {
final messages = _notInlinedMessages(_notInlinedMessages); final messages = _notInlinedMessages(_notInlinedMessages);
static Map<String, Function> _notInlinedMessages(_) => <String, Function>{ static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
"bold": MessageLookupByLibrary.simpleMessage(""), "bold": MessageLookupByLibrary.simpleMessage("Grassetto"),
"bulletedList": MessageLookupByLibrary.simpleMessage(""), "bulletedList": MessageLookupByLibrary.simpleMessage("Elenco puntato"),
"checkbox": MessageLookupByLibrary.simpleMessage(""), "checkbox": MessageLookupByLibrary.simpleMessage("Casella di spunta"),
"embedCode": MessageLookupByLibrary.simpleMessage(""), "embedCode": MessageLookupByLibrary.simpleMessage("Incorpora codice"),
"heading1": MessageLookupByLibrary.simpleMessage(""), "heading1": MessageLookupByLibrary.simpleMessage("H1"),
"heading2": MessageLookupByLibrary.simpleMessage(""), "heading2": MessageLookupByLibrary.simpleMessage("H2"),
"heading3": MessageLookupByLibrary.simpleMessage(""), "heading3": MessageLookupByLibrary.simpleMessage("H3"),
"highlight": MessageLookupByLibrary.simpleMessage(""), "highlight": MessageLookupByLibrary.simpleMessage("Evidenzia"),
"image": MessageLookupByLibrary.simpleMessage(""), "image": MessageLookupByLibrary.simpleMessage("Immagine"),
"italic": MessageLookupByLibrary.simpleMessage(""), "italic": MessageLookupByLibrary.simpleMessage("Corsivo"),
"link": MessageLookupByLibrary.simpleMessage(""), "link": MessageLookupByLibrary.simpleMessage("Collegamento"),
"numberedList": MessageLookupByLibrary.simpleMessage(""), "numberedList": MessageLookupByLibrary.simpleMessage("Elenco numerato"),
"quote": MessageLookupByLibrary.simpleMessage(""), "quote": MessageLookupByLibrary.simpleMessage("Cita"),
"strikethrough": MessageLookupByLibrary.simpleMessage(""), "strikethrough": MessageLookupByLibrary.simpleMessage("Barrato"),
"text": MessageLookupByLibrary.simpleMessage(""), "text": MessageLookupByLibrary.simpleMessage("Testo"),
"underline": MessageLookupByLibrary.simpleMessage("") "underline": MessageLookupByLibrary.simpleMessage("Sottolineato")
}; };
} }

View File

@ -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<dynamic> args);
class MessageLookup extends MessageLookupByLibrary {
String get localeName => 'nl_NL';
final messages = _notInlinedMessages(_notInlinedMessages);
static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
"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")
};
}

View File

@ -22,21 +22,22 @@ class MessageLookup extends MessageLookupByLibrary {
final messages = _notInlinedMessages(_notInlinedMessages); final messages = _notInlinedMessages(_notInlinedMessages);
static Map<String, Function> _notInlinedMessages(_) => <String, Function>{ static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
"bold": MessageLookupByLibrary.simpleMessage(""), "bold": MessageLookupByLibrary.simpleMessage("Negrito"),
"bulletedList": MessageLookupByLibrary.simpleMessage(""), "bulletedList":
"checkbox": MessageLookupByLibrary.simpleMessage(""), MessageLookupByLibrary.simpleMessage("Lista de marcadores"),
"embedCode": MessageLookupByLibrary.simpleMessage(""), "checkbox": MessageLookupByLibrary.simpleMessage("Caixa de seleção"),
"heading1": MessageLookupByLibrary.simpleMessage(""), "embedCode": MessageLookupByLibrary.simpleMessage("Código incorporado"),
"heading2": MessageLookupByLibrary.simpleMessage(""), "heading1": MessageLookupByLibrary.simpleMessage("H1"),
"heading3": MessageLookupByLibrary.simpleMessage(""), "heading2": MessageLookupByLibrary.simpleMessage("H2"),
"highlight": MessageLookupByLibrary.simpleMessage(""), "heading3": MessageLookupByLibrary.simpleMessage("H3"),
"image": MessageLookupByLibrary.simpleMessage(""), "highlight": MessageLookupByLibrary.simpleMessage("Destacar"),
"italic": MessageLookupByLibrary.simpleMessage(""), "image": MessageLookupByLibrary.simpleMessage("Imagem"),
"link": MessageLookupByLibrary.simpleMessage(""), "italic": MessageLookupByLibrary.simpleMessage("Itálico"),
"numberedList": MessageLookupByLibrary.simpleMessage(""), "link": MessageLookupByLibrary.simpleMessage("Link"),
"quote": MessageLookupByLibrary.simpleMessage(""), "numberedList": MessageLookupByLibrary.simpleMessage("Lista numerada"),
"strikethrough": MessageLookupByLibrary.simpleMessage(""), "quote": MessageLookupByLibrary.simpleMessage("Citar"),
"text": MessageLookupByLibrary.simpleMessage(""), "strikethrough": MessageLookupByLibrary.simpleMessage("Rasurar"),
"underline": MessageLookupByLibrary.simpleMessage("") "text": MessageLookupByLibrary.simpleMessage("Texto"),
"underline": MessageLookupByLibrary.simpleMessage("Sublinhar")
}; };
} }

View File

@ -22,21 +22,22 @@ class MessageLookup extends MessageLookupByLibrary {
final messages = _notInlinedMessages(_notInlinedMessages); final messages = _notInlinedMessages(_notInlinedMessages);
static Map<String, Function> _notInlinedMessages(_) => <String, Function>{ static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
"bold": MessageLookupByLibrary.simpleMessage(""), "bold": MessageLookupByLibrary.simpleMessage("negrito"),
"bulletedList": MessageLookupByLibrary.simpleMessage(""), "bulletedList":
"checkbox": MessageLookupByLibrary.simpleMessage(""), MessageLookupByLibrary.simpleMessage("lista com marcadores"),
"embedCode": MessageLookupByLibrary.simpleMessage(""), "checkbox": MessageLookupByLibrary.simpleMessage("caixa de seleção"),
"heading1": MessageLookupByLibrary.simpleMessage(""), "embedCode": MessageLookupByLibrary.simpleMessage("Código embutido"),
"heading2": MessageLookupByLibrary.simpleMessage(""), "heading1": MessageLookupByLibrary.simpleMessage("Cabeçallho 1"),
"heading3": MessageLookupByLibrary.simpleMessage(""), "heading2": MessageLookupByLibrary.simpleMessage("Cabeçallho 2"),
"highlight": MessageLookupByLibrary.simpleMessage(""), "heading3": MessageLookupByLibrary.simpleMessage("Cabeçallho 3"),
"image": MessageLookupByLibrary.simpleMessage(""), "highlight": MessageLookupByLibrary.simpleMessage("realçar"),
"italic": MessageLookupByLibrary.simpleMessage(""), "image": MessageLookupByLibrary.simpleMessage("imagem"),
"link": MessageLookupByLibrary.simpleMessage(""), "italic": MessageLookupByLibrary.simpleMessage("itálico"),
"numberedList": MessageLookupByLibrary.simpleMessage(""), "link": MessageLookupByLibrary.simpleMessage("link"),
"quote": MessageLookupByLibrary.simpleMessage(""), "numberedList": MessageLookupByLibrary.simpleMessage("lista numerada"),
"strikethrough": MessageLookupByLibrary.simpleMessage(""), "quote": MessageLookupByLibrary.simpleMessage("citar"),
"text": MessageLookupByLibrary.simpleMessage(""), "strikethrough": MessageLookupByLibrary.simpleMessage("tachado"),
"underline": MessageLookupByLibrary.simpleMessage("") "text": MessageLookupByLibrary.simpleMessage("texto"),
"underline": MessageLookupByLibrary.simpleMessage("sublinhado")
}; };
} }

View File

@ -220,6 +220,7 @@ class AppLocalizationDelegate
return const <Locale>[ return const <Locale>[
Locale.fromSubtags(languageCode: 'en'), Locale.fromSubtags(languageCode: 'en'),
Locale.fromSubtags(languageCode: 'ca'), Locale.fromSubtags(languageCode: 'ca'),
Locale.fromSubtags(languageCode: 'cs', countryCode: 'CZ'),
Locale.fromSubtags(languageCode: 'de', countryCode: 'DE'), Locale.fromSubtags(languageCode: 'de', countryCode: 'DE'),
Locale.fromSubtags(languageCode: 'es', countryCode: 'VE'), Locale.fromSubtags(languageCode: 'es', countryCode: 'VE'),
Locale.fromSubtags(languageCode: 'fr', countryCode: 'CA'), Locale.fromSubtags(languageCode: 'fr', countryCode: 'CA'),
@ -228,6 +229,7 @@ class AppLocalizationDelegate
Locale.fromSubtags(languageCode: 'id', countryCode: 'ID'), Locale.fromSubtags(languageCode: 'id', countryCode: 'ID'),
Locale.fromSubtags(languageCode: 'it', countryCode: 'IT'), Locale.fromSubtags(languageCode: 'it', countryCode: 'IT'),
Locale.fromSubtags(languageCode: 'ja', countryCode: 'JP'), Locale.fromSubtags(languageCode: 'ja', countryCode: 'JP'),
Locale.fromSubtags(languageCode: 'nl', countryCode: 'NL'),
Locale.fromSubtags(languageCode: 'pl', countryCode: 'PL'), Locale.fromSubtags(languageCode: 'pl', countryCode: 'PL'),
Locale.fromSubtags(languageCode: 'pt', countryCode: 'BR'), Locale.fromSubtags(languageCode: 'pt', countryCode: 'BR'),
Locale.fromSubtags(languageCode: 'pt', countryCode: 'PT'), Locale.fromSubtags(languageCode: 'pt', countryCode: 'PT'),

View File

@ -31,6 +31,7 @@ class _LinkMenuState extends State<LinkMenu> {
void initState() { void initState() {
super.initState(); super.initState();
_textEditingController.text = widget.linkText ?? ''; _textEditingController.text = widget.linkText ?? '';
_focusNode.requestFocus();
_focusNode.addListener(_onFocusChange); _focusNode.addListener(_onFocusChange);
} }

View File

@ -1,15 +1,8 @@
import 'package:appflowy_editor/src/document/built_in_attribute_keys.dart'; import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor/src/document/node.dart'; import 'package:appflowy_editor/src/commands/format_built_in_text.dart';
import 'package:appflowy_editor/src/editor_state.dart';
import 'package:appflowy_editor/src/infra/flowy_svg.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/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:appflowy_editor/src/extensions/text_style_extension.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -81,8 +74,12 @@ class _CheckboxNodeWidgetState extends State<CheckboxNodeWidget>
padding: iconPadding, padding: iconPadding,
name: check ? 'check' : 'uncheck', name: check ? 'check' : 'uncheck',
), ),
onTap: () { onTap: () async {
formatCheckbox(widget.editorState, !check); await formatTextToCheckbox(
widget.editorState,
!check,
textNode: widget.textNode,
);
}, },
), ),
Flexible( Flexible(

View File

@ -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/selection/selectable.dart';
import 'package:appflowy_editor/src/render/toolbar/toolbar_item.dart'; import 'package:appflowy_editor/src/render/toolbar/toolbar_item.dart';
const _kRichTextDebugMode = false;
typedef FlowyTextSpanDecorator = TextSpan Function(TextSpan textSpan); typedef FlowyTextSpanDecorator = TextSpan Function(TextSpan textSpan);
class FlowyRichText extends StatefulWidget { class FlowyRichText extends StatefulWidget {
@ -261,6 +263,17 @@ class _FlowyRichTextState extends State<FlowyRichText> with SelectableMixin {
), ),
); );
} }
if (_kRichTextDebugMode) {
textSpans.add(
TextSpan(
text: '${widget.textNode.path}',
style: const TextStyle(
backgroundColor: Colors.red,
fontSize: 16.0,
),
),
);
}
return TextSpan( return TextSpan(
children: textSpans, children: textSpans,
); );

View File

@ -37,7 +37,7 @@ class SelectionMenuItemWidget extends StatelessWidget {
: MaterialStateProperty.all(Colors.transparent), : MaterialStateProperty.all(Colors.transparent),
), ),
label: Text( label: Text(
item.name, item.name(),
textAlign: TextAlign.left, textAlign: TextAlign.left,
style: const TextStyle( style: const TextStyle(
color: Colors.black, color: Colors.black,

View File

@ -124,7 +124,7 @@ List<SelectionMenuItem> get defaultSelectionMenuItems =>
_defaultSelectionMenuItems; _defaultSelectionMenuItems;
final List<SelectionMenuItem> _defaultSelectionMenuItems = [ final List<SelectionMenuItem> _defaultSelectionMenuItems = [
SelectionMenuItem( SelectionMenuItem(
name: AppFlowyEditorLocalizations.current.text, name: () => AppFlowyEditorLocalizations.current.text,
icon: _selectionMenuIcon('text'), icon: _selectionMenuIcon('text'),
keywords: ['text'], keywords: ['text'],
handler: (editorState, _, __) { handler: (editorState, _, __) {
@ -132,7 +132,7 @@ final List<SelectionMenuItem> _defaultSelectionMenuItems = [
}, },
), ),
SelectionMenuItem( SelectionMenuItem(
name: AppFlowyEditorLocalizations.current.heading1, name: () => AppFlowyEditorLocalizations.current.heading1,
icon: _selectionMenuIcon('h1'), icon: _selectionMenuIcon('h1'),
keywords: ['heading 1, h1'], keywords: ['heading 1, h1'],
handler: (editorState, _, __) { handler: (editorState, _, __) {
@ -140,7 +140,7 @@ final List<SelectionMenuItem> _defaultSelectionMenuItems = [
}, },
), ),
SelectionMenuItem( SelectionMenuItem(
name: AppFlowyEditorLocalizations.current.heading2, name: () => AppFlowyEditorLocalizations.current.heading2,
icon: _selectionMenuIcon('h2'), icon: _selectionMenuIcon('h2'),
keywords: ['heading 2, h2'], keywords: ['heading 2, h2'],
handler: (editorState, _, __) { handler: (editorState, _, __) {
@ -148,7 +148,7 @@ final List<SelectionMenuItem> _defaultSelectionMenuItems = [
}, },
), ),
SelectionMenuItem( SelectionMenuItem(
name: AppFlowyEditorLocalizations.current.heading3, name: () => AppFlowyEditorLocalizations.current.heading3,
icon: _selectionMenuIcon('h3'), icon: _selectionMenuIcon('h3'),
keywords: ['heading 3, h3'], keywords: ['heading 3, h3'],
handler: (editorState, _, __) { handler: (editorState, _, __) {
@ -156,13 +156,13 @@ final List<SelectionMenuItem> _defaultSelectionMenuItems = [
}, },
), ),
SelectionMenuItem( SelectionMenuItem(
name: AppFlowyEditorLocalizations.current.image, name: () => AppFlowyEditorLocalizations.current.image,
icon: _selectionMenuIcon('image'), icon: _selectionMenuIcon('image'),
keywords: ['image'], keywords: ['image'],
handler: showImageUploadMenu, handler: showImageUploadMenu,
), ),
SelectionMenuItem( SelectionMenuItem(
name: AppFlowyEditorLocalizations.current.bulletedList, name: () => AppFlowyEditorLocalizations.current.bulletedList,
icon: _selectionMenuIcon('bulleted_list'), icon: _selectionMenuIcon('bulleted_list'),
keywords: ['bulleted list', 'list', 'unordered list'], keywords: ['bulleted list', 'list', 'unordered list'],
handler: (editorState, _, __) { handler: (editorState, _, __) {
@ -170,7 +170,15 @@ final List<SelectionMenuItem> _defaultSelectionMenuItems = [
}, },
), ),
SelectionMenuItem( 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'), icon: _selectionMenuIcon('checkbox'),
keywords: ['todo list', 'list', 'checkbox list'], keywords: ['todo list', 'list', 'checkbox list'],
handler: (editorState, _, __) { handler: (editorState, _, __) {
@ -178,7 +186,7 @@ final List<SelectionMenuItem> _defaultSelectionMenuItems = [
}, },
), ),
SelectionMenuItem( SelectionMenuItem(
name: AppFlowyEditorLocalizations.current.quote, name: () => AppFlowyEditorLocalizations.current.quote,
icon: _selectionMenuIcon('quote'), icon: _selectionMenuIcon('quote'),
keywords: ['quote', 'refer'], keywords: ['quote', 'refer'],
handler: (editorState, _, __) { handler: (editorState, _, __) {

View File

@ -6,27 +6,54 @@ import 'package:appflowy_editor/src/render/selection_menu/selection_menu_service
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
typedef SelectionMenuItemHandler = void Function(
EditorState editorState,
SelectionMenuService menuService,
BuildContext context,
);
/// Selection Menu Item /// Selection Menu Item
class SelectionMenuItem { class SelectionMenuItem {
SelectionMenuItem({ SelectionMenuItem({
required this.name, required this.name,
required this.icon, required this.icon,
required this.keywords, 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; final Widget icon;
/// Customizes keywords for item. /// Customizes keywords for item.
/// ///
/// The keywords are used to quickly retrieve items. /// The keywords are used to quickly retrieve items.
final List<String> keywords; final List<String> keywords;
final void Function( late final SelectionMenuItemHandler handler;
EditorState editorState,
SelectionMenuService menuService, void _deleteToSlash(EditorState editorState) {
BuildContext context, final selectionService = editorState.service.selectionService;
) handler; 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 { class SelectionMenuWidget extends StatefulWidget {
@ -204,11 +231,8 @@ class _SelectionMenuWidgetState extends State<SelectionMenuWidget> {
if (event.logicalKey == LogicalKeyboardKey.enter) { if (event.logicalKey == LogicalKeyboardKey.enter) {
if (0 <= _selectedIndex && _selectedIndex < _showingItems.length) { if (0 <= _selectedIndex && _selectedIndex < _showingItems.length) {
_deleteLastCharacters(length: keyword.length + 1); _showingItems[_selectedIndex]
WidgetsBinding.instance.addPostFrameCallback((timeStamp) { .handler(widget.editorState, widget.menuService, context);
_showingItems[_selectedIndex]
.handler(widget.editorState, widget.menuService, context);
});
return KeyEventResult.handled; return KeyEventResult.handled;
} }
} else if (event.logicalKey == LogicalKeyboardKey.escape) { } else if (event.logicalKey == LogicalKeyboardKey.escape) {

View File

@ -1,4 +1,5 @@
import 'package:appflowy_editor/appflowy_editor.dart'; 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/extensions/url_launcher_extension.dart';
import 'package:appflowy_editor/src/infra/flowy_svg.dart'; import 'package:appflowy_editor/src/infra/flowy_svg.dart';
import 'package:appflowy_editor/src/render/link_menu/link_menu.dart'; import 'package:appflowy_editor/src/render/link_menu/link_menu.dart';
@ -345,11 +346,8 @@ void showLinkMenu(
onOpenLink: () async { onOpenLink: () async {
await safeLaunchUrl(linkText); await safeLaunchUrl(linkText);
}, },
onSubmitted: (text) { onSubmitted: (text) async {
TransactionBuilder(editorState) await formatLinkInText(editorState, text, textNode: textNode);
..formatText(
textNode, index, length, {BuiltInAttributeKey.href: text})
..commit();
_dismissLinkMenu(); _dismissLinkMenu();
}, },
onCopyLink: () { onCopyLink: () {
@ -377,6 +375,7 @@ void showLinkMenu(
Overlay.of(context)?.insert(_linkMenuOverlay!); Overlay.of(context)?.insert(_linkMenuOverlay!);
editorState.service.scrollService?.disable(); editorState.service.scrollService?.disable();
editorState.service.keyboardService?.disable();
editorState.service.selectionService.currentSelection editorState.service.selectionService.currentSelection
.addListener(_dismissLinkMenu); .addListener(_dismissLinkMenu);
} }

View File

@ -34,6 +34,13 @@ void insertBulletedListAfterSelection(EditorState editorState) {
}); });
} }
void insertNumberedListAfterSelection(EditorState editorState) {
insertTextNodeAfterSelection(editorState, {
BuiltInAttributeKey.subtype: BuiltInAttributeKey.numberList,
BuiltInAttributeKey.number: 1,
});
}
bool insertTextNodeAfterSelection( bool insertTextNodeAfterSelection(
EditorState editorState, Attributes attributes) { EditorState editorState, Attributes attributes) {
final selection = editorState.service.selectionService.currentSelection.value; final selection = editorState.service.selectionService.currentSelection.value;
@ -103,13 +110,17 @@ bool formatTextNodes(EditorState editorState, Attributes attributes) {
final builder = TransactionBuilder(editorState); final builder = TransactionBuilder(editorState);
for (final textNode in textNodes) { 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 builder
..updateNode( ..updateNode(
textNode, textNode,
Attributes.fromIterable( newAttributes,
BuiltInAttributeKey.globalStyleKeys,
value: (_) => null,
)..addAll(attributes),
) )
..afterSelection = Selection.collapsed( ..afterSelection = Selection.collapsed(
Position( Position(

View File

@ -72,6 +72,7 @@ class _AppFlowyEditorState extends State<AppFlowyEditor> {
editorState.selectionMenuItems = widget.selectionMenuItems; editorState.selectionMenuItems = widget.selectionMenuItems;
editorState.editorStyle = widget.editorStyle; editorState.editorStyle = widget.editorStyle;
editorState.service.renderPluginService = _createRenderPlugin(); editorState.service.renderPluginService = _createRenderPlugin();
editorState.editable = widget.editable;
} }
@override @override
@ -84,6 +85,7 @@ class _AppFlowyEditorState extends State<AppFlowyEditor> {
} }
editorState.editorStyle = widget.editorStyle; editorState.editorStyle = widget.editorStyle;
editorState.editable = widget.editable;
services = null; services = null;
} }

View File

@ -297,7 +297,11 @@ class _AppFlowyInputState extends State<AppFlowyInput>
_updateCaretPosition(textNodes.first, selection); _updateCaretPosition(textNodes.first, selection);
} }
} else { } else {
// close(); // https://github.com/flutter/flutter/issues/104944
// Disable IME for the Web.
if (kIsWeb) {
close();
}
} }
} }

View File

@ -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<int> 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<int> _findBackquoteIndexes(String text, TextNode textNode) {
final backquoteIndexes = <int>[];
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<TextNode>();
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<TextNode>();
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 = <int>[];
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<TextNode>();
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;
};

View File

@ -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<TextNode>()
.toList(growable: false);
if (selection == null ||
!selection.isCollapsed ||
!kIsWeb ||
textNodes.length != 1) {
return KeyEventResult.ignored;
}
insertContextInText(editorState, selection.startIndex, ' ');
return KeyEventResult.handled;
};

View File

@ -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/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/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/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/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/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/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/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/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/tab_handler.dart';
import 'package:appflowy_editor/src/service/internal_key_event_handlers/whitespace_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:appflowy_editor/src/service/shortcut_event/shortcut_event.dart';
import 'package:flutter/foundation.dart';
// //
List<ShortcutEvent> builtInShortcutEvents = [ List<ShortcutEvent> builtInShortcutEvents = [
@ -249,4 +252,29 @@ List<ShortcutEvent> builtInShortcutEvents = [
command: 'tab', command: 'tab',
handler: tabHandler, 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,
),
]; ];

View File

@ -139,6 +139,9 @@ extension on LogicalKeyboardKey {
if (this == LogicalKeyboardKey.keyZ) { if (this == LogicalKeyboardKey.keyZ) {
return PhysicalKeyboardKey.keyZ; return PhysicalKeyboardKey.keyZ;
} }
if (this == LogicalKeyboardKey.tilde) {
return PhysicalKeyboardKey.backquote;
}
throw UnimplementedError(); throw UnimplementedError();
} }
} }

View File

@ -1,7 +1,6 @@
import 'package:appflowy_editor/appflowy_editor.dart'; 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_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_service.dart';
import 'package:appflowy_editor/src/render/selection_menu/selection_menu_widget.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import '../../infra/test_editor.dart'; import '../../infra/test_editor.dart';
@ -12,29 +11,40 @@ void main() async {
}); });
group('selection_menu_widget.dart', () { group('selection_menu_widget.dart', () {
// const i = defaultSelectionMenuItems.length; for (var i = 0; i < defaultSelectionMenuItems.length; i += 1) {
// testWidgets('Selects number.$i item in selection menu with enter',
// Because the `defaultSelectionMenuItems` uses localization, (tester) async {
// and the MaterialApp has not been initialized at the time of getting the value, final editor = await _prepare(tester);
// it will crash. for (var j = 0; j < i; j++) {
// await editor.pressLogicKey(LogicalKeyboardKey.arrowDown);
// 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);
}
await editor.pressLogicKey(LogicalKeyboardKey.enter); await editor.pressLogicKey(LogicalKeyboardKey.enter);
expect( expect(
find.byType(SelectionMenuWidget, skipOffstage: false), find.byType(SelectionMenuWidget, skipOffstage: false),
findsNothing, findsNothing,
); );
if (defaultSelectionMenuItems[i].name != 'Image') { if (defaultSelectionMenuItems[i].name() != 'Image') {
await _testDefaultSelectionMenuItems(i, editor); 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', testWidgets('Search item in selection menu util no results',
(tester) async { (tester) async {
@ -48,7 +58,7 @@ void main() async {
await editor.pressLogicKey(LogicalKeyboardKey.backspace); await editor.pressLogicKey(LogicalKeyboardKey.backspace);
expect( expect(
find.byType(SelectionMenuItemWidget, skipOffstage: false), find.byType(SelectionMenuItemWidget, skipOffstage: false),
findsNWidgets(4), findsNWidgets(5),
); );
await editor.pressLogicKey(LogicalKeyboardKey.keyE); await editor.pressLogicKey(LogicalKeyboardKey.keyE);
expect( expect(
@ -137,23 +147,28 @@ Future<void> _testDefaultSelectionMenuItems(
int index, EditorWidgetTester editor) async { int index, EditorWidgetTester editor) async {
expect(editor.documentLength, 4); expect(editor.documentLength, 4);
expect(editor.documentSelection, Selection.single(path: [2], startOffset: 0)); 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 node = editor.nodeAtPath([2]);
final item = defaultSelectionMenuItems[index]; final item = defaultSelectionMenuItems[index];
if (item.name == 'Text') { final itemName = item.name();
if (itemName == 'Text') {
expect(node?.subtype == null, true); expect(node?.subtype == null, true);
} else if (item.name == 'Heading 1') { } else if (itemName == 'Heading 1') {
expect(node?.subtype, BuiltInAttributeKey.heading); expect(node?.subtype, BuiltInAttributeKey.heading);
expect(node?.attributes.heading, BuiltInAttributeKey.h1); expect(node?.attributes.heading, BuiltInAttributeKey.h1);
} else if (item.name == 'Heading 2') { } else if (itemName == 'Heading 2') {
expect(node?.subtype, BuiltInAttributeKey.heading); expect(node?.subtype, BuiltInAttributeKey.heading);
expect(node?.attributes.heading, BuiltInAttributeKey.h2); expect(node?.attributes.heading, BuiltInAttributeKey.h2);
} else if (item.name == 'Heading 3') { } else if (itemName == 'Heading 3') {
expect(node?.subtype, BuiltInAttributeKey.heading); expect(node?.subtype, BuiltInAttributeKey.heading);
expect(node?.attributes.heading, BuiltInAttributeKey.h3); expect(node?.attributes.heading, BuiltInAttributeKey.h3);
} else if (item.name == 'Bulleted list') { } else if (itemName == 'Bulleted list') {
expect(node?.subtype, BuiltInAttributeKey.bulletedList); expect(node?.subtype, BuiltInAttributeKey.bulletedList);
} else if (item.name == 'Checkbox') { } else if (itemName == 'Checkbox') {
expect(node?.subtype, BuiltInAttributeKey.checkbox); expect(node?.subtype, BuiltInAttributeKey.checkbox);
expect(node?.attributes.check, false); expect(node?.attributes.check, false);
} else if (itemName == 'Quote') {
expect(node?.subtype, BuiltInAttributeKey.quote);
} }
} }

View File

@ -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<void> 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<void> 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<void> 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);
});
});
});
}

View File

@ -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 😁 ',
);
}
});
});
}

View File

@ -1444,9 +1444,9 @@ checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7"
[[package]] [[package]]
name = "hashbrown" name = "hashbrown"
version = "0.11.2" version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
[[package]] [[package]]
name = "heck" name = "heck"
@ -1610,9 +1610,9 @@ checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683"
[[package]] [[package]]
name = "indexmap" name = "indexmap"
version = "1.8.1" version = "1.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f647032dfaa1f8b6dc29bd3edb7bbef4861b8b8007ebb118d6db284fd59f6ee" checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e"
dependencies = [ dependencies = [
"autocfg", "autocfg",
"hashbrown", "hashbrown",

View File

@ -34,7 +34,7 @@ rayon = "1.5.2"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = {version = "1.0"} serde_json = {version = "1.0"}
serde_repr = "0.1" serde_repr = "0.1"
indexmap = {version = "1.8.1", features = ["serde"]} indexmap = {version = "1.9.1", features = ["serde"]}
fancy-regex = "0.10.0" fancy-regex = "0.10.0"
regex = "1.5.6" regex = "1.5.6"
url = { version = "2"} url = { version = "2"}

View File

@ -1,3 +1,4 @@
use crate::entities::FieldType;
use flowy_derive::ProtoBuf; use flowy_derive::ProtoBuf;
use flowy_error::ErrorCode; use flowy_error::ErrorCode;
use flowy_grid_data_model::parser::NotEmptyStr; use flowy_grid_data_model::parser::NotEmptyStr;
@ -74,15 +75,20 @@ pub struct GridCellPB {
#[pb(index = 1)] #[pb(index = 1)]
pub field_id: String, pub field_id: String,
// The data was encoded in field_type's data type
#[pb(index = 2)] #[pb(index = 2)]
pub data: Vec<u8>, pub data: Vec<u8>,
#[pb(index = 3, one_of)]
pub field_type: Option<FieldType>,
} }
impl GridCellPB { impl GridCellPB {
pub fn new(field_id: &str, data: Vec<u8>) -> Self { pub fn new(field_id: &str, field_type: FieldType, data: Vec<u8>) -> Self {
Self { Self {
field_id: field_id.to_owned(), field_id: field_id.to_owned(),
data, data,
field_type: Some(field_type),
} }
} }
@ -90,6 +96,7 @@ impl GridCellPB {
Self { Self {
field_id: field_id.to_owned(), field_id: field_id.to_owned(),
data: vec![], data: vec![],
field_type: None,
} }
} }
} }

View File

@ -98,6 +98,7 @@ pub struct MoveGroupPayloadPB {
pub to_group_id: String, pub to_group_id: String,
} }
#[derive(Debug)]
pub struct MoveGroupParams { pub struct MoveGroupParams {
pub view_id: String, pub view_id: String,
pub from_group_id: String, pub from_group_id: String,

View File

@ -24,6 +24,13 @@ pub trait CellDisplayable<CD> {
decoded_field_type: &FieldType, decoded_field_type: &FieldType,
field_rev: &FieldRevision, field_rev: &FieldRevision,
) -> FlowyResult<CellBytes>; ) -> FlowyResult<CellBytes>;
fn display_string(
&self,
cell_data: CellData<CD>,
decoded_field_type: &FieldType,
field_rev: &FieldRevision,
) -> FlowyResult<String>;
} }
// CD: Short for CellData. This type is the type return by apply_changeset function. // CD: Short for CellData. This type is the type return by apply_changeset function.
@ -84,16 +91,16 @@ pub fn apply_cell_data_changeset<C: ToString, T: AsRef<FieldRevision>>(
pub fn decode_any_cell_data<T: TryInto<AnyCellData, Error = FlowyError> + Debug>( pub fn decode_any_cell_data<T: TryInto<AnyCellData, Error = FlowyError> + Debug>(
data: T, data: T,
field_rev: &FieldRevision, field_rev: &FieldRevision,
) -> CellBytes { ) -> (FieldType, CellBytes) {
let to_field_type = field_rev.ty.into();
match data.try_into() { match data.try_into() {
Ok(any_cell_data) => { Ok(any_cell_data) => {
let AnyCellData { data, field_type } = 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) { 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) => { Err(e) => {
tracing::error!("Decode cell data failed, {:?}", e); tracing::error!("Decode cell data failed, {:?}", e);
CellBytes::default() (field_type, CellBytes::default())
} }
} }
} }
@ -101,12 +108,58 @@ pub fn decode_any_cell_data<T: TryInto<AnyCellData, Error = FlowyError> + Debug>
// It's okay to ignore this error, because it's okay that the current cell can't // 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 // 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. // 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<String>,
from_field_type: &FieldType,
to_field_type: &FieldType,
field_rev: &FieldRevision,
) -> FlowyResult<String> {
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::<RichTextTypeOptionPB>(field_type)?
.display_string(cell_data.into(), from_field_type, field_rev),
FieldType::Number => field_rev
.get_type_option::<NumberTypeOptionPB>(field_type)?
.display_string(cell_data.into(), from_field_type, field_rev),
FieldType::DateTime => field_rev
.get_type_option::<DateTypeOptionPB>(field_type)?
.display_string(cell_data.into(), from_field_type, field_rev),
FieldType::SingleSelect => field_rev
.get_type_option::<SingleSelectTypeOptionPB>(field_type)?
.display_string(cell_data.into(), from_field_type, field_rev),
FieldType::MultiSelect => field_rev
.get_type_option::<MultiSelectTypeOptionPB>(field_type)?
.display_string(cell_data.into(), from_field_type, field_rev),
FieldType::Checkbox => field_rev
.get_type_option::<CheckboxTypeOptionPB>(field_type)?
.display_string(cell_data.into(), from_field_type, field_rev),
FieldType::URL => field_rev
.get_type_option::<URLTypeOptionPB>(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` /// Each `FieldType` has its corresponding `TypeOption` that implements the `CellDisplayable`
/// and `CellDataOperation` traits. /// and `CellDataOperation` traits.

View File

@ -48,6 +48,16 @@ impl CellDisplayable<CheckboxCellData> for CheckboxTypeOptionPB {
let cell_data = cell_data.try_into_inner()?; let cell_data = cell_data.try_into_inner()?;
Ok(CellBytes::new(cell_data)) Ok(CellBytes::new(cell_data))
} }
fn display_string(
&self,
cell_data: CellData<CheckboxCellData>,
_decoded_field_type: &FieldType,
_field_rev: &FieldRevision,
) -> FlowyResult<String> {
let cell_data = cell_data.try_into_inner()?;
Ok(cell_data.to_string())
}
} }
impl CellDataOperation<CheckboxCellData, String> for CheckboxTypeOptionPB { impl CellDataOperation<CheckboxCellData, String> for CheckboxTypeOptionPB {

View File

@ -127,6 +127,17 @@ impl CellDisplayable<DateTimestamp> for DateTypeOptionPB {
let date_cell_data = self.today_desc_from_timestamp(timestamp); let date_cell_data = self.today_desc_from_timestamp(timestamp);
CellBytes::from(date_cell_data) CellBytes::from(date_cell_data)
} }
fn display_string(
&self,
cell_data: CellData<DateTimestamp>,
_decoded_field_type: &FieldType,
_field_rev: &FieldRevision,
) -> FlowyResult<String> {
let timestamp = cell_data.try_into_inner()?;
let date_cell_data = self.today_desc_from_timestamp(timestamp);
Ok(date_cell_data.date)
}
} }
impl CellDataOperation<DateTimestamp, DateCellChangesetPB> for DateTypeOptionPB { impl CellDataOperation<DateTimestamp, DateCellChangesetPB> for DateTypeOptionPB {

View File

@ -1,6 +1,6 @@
use crate::entities::FieldType; use crate::entities::FieldType;
use crate::impl_type_option; 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::type_options::number_type_option::format::*;
use crate::services::field::{BoxTypeOptionBuilder, NumberCellData, TypeOptionBuilder}; use crate::services::field::{BoxTypeOptionBuilder, NumberCellData, TypeOptionBuilder};
use bytes::Bytes; use bytes::Bytes;
@ -102,17 +102,13 @@ pub(crate) fn strip_currency_symbol<T: ToString>(s: T) -> String {
s s
} }
impl CellDataOperation<String, String> for NumberTypeOptionPB { impl CellDisplayable<String> for NumberTypeOptionPB {
fn decode_cell_data( fn display_data(
&self, &self,
cell_data: CellData<String>, cell_data: CellData<String>,
decoded_field_type: &FieldType, _decoded_field_type: &FieldType,
_field_rev: &FieldRevision, _field_rev: &FieldRevision,
) -> FlowyResult<CellBytes> { ) -> FlowyResult<CellBytes> {
if decoded_field_type.is_date() {
return Ok(CellBytes::default());
}
let cell_data: String = cell_data.try_into_inner()?; let cell_data: String = cell_data.try_into_inner()?;
match self.format_cell_data(&cell_data) { match self.format_cell_data(&cell_data) {
Ok(num) => Ok(CellBytes::new(num.to_string())), Ok(num) => Ok(CellBytes::new(num.to_string())),
@ -120,6 +116,31 @@ impl CellDataOperation<String, String> for NumberTypeOptionPB {
} }
} }
fn display_string(
&self,
cell_data: CellData<String>,
_decoded_field_type: &FieldType,
_field_rev: &FieldRevision,
) -> FlowyResult<String> {
let cell_data: String = cell_data.try_into_inner()?;
Ok(cell_data)
}
}
impl CellDataOperation<String, String> for NumberTypeOptionPB {
fn decode_cell_data(
&self,
cell_data: CellData<String>,
decoded_field_type: &FieldType,
field_rev: &FieldRevision,
) -> FlowyResult<CellBytes> {
if decoded_field_type.is_date() {
return Ok(CellBytes::default());
}
self.display_data(cell_data, decoded_field_type, field_rev)
}
fn apply_changeset( fn apply_changeset(
&self, &self,
changeset: CellDataChangeset<String>, changeset: CellDataChangeset<String>,

View File

@ -120,6 +120,21 @@ where
) -> FlowyResult<CellBytes> { ) -> FlowyResult<CellBytes> {
CellBytes::from(self.selected_select_option(cell_data)) CellBytes::from(self.selected_select_option(cell_data))
} }
fn display_string(
&self,
cell_data: CellData<SelectOptionIds>,
_decoded_field_type: &FieldType,
_field_rev: &FieldRevision,
) -> FlowyResult<String> {
Ok(self
.selected_select_option(cell_data)
.select_options
.into_iter()
.map(|option| option.name)
.collect::<Vec<String>>()
.join(SELECTION_IDS_SEPARATOR))
}
} }
pub fn select_option_operation(field_rev: &FieldRevision) -> FlowyResult<Box<dyn SelectOptionOperation>> { pub fn select_option_operation(field_rev: &FieldRevision) -> FlowyResult<Box<dyn SelectOptionOperation>> {

View File

@ -17,10 +17,10 @@ mod tests {
type_option type_option
.decode_cell_data(1647251762.into(), &field_type, &field_rev) .decode_cell_data(1647251762.into(), &field_type, &field_rev)
.unwrap() .unwrap()
.parser::<DateCellDataParser>() .parser::<TextCellDataParser>()
.unwrap() .unwrap()
.date, .as_ref(),
"Mar 14,2022".to_owned() "Mar 14,2022"
); );
} }
@ -40,10 +40,10 @@ mod tests {
type_option type_option
.decode_cell_data(option_id.into(), &field_type, &field_rev) .decode_cell_data(option_id.into(), &field_type, &field_rev)
.unwrap() .unwrap()
.parser::<SelectOptionCellDataParser>() .parser::<TextCellDataParser>()
.unwrap() .unwrap()
.select_options, .to_string(),
vec![done_option], done_option.name,
); );
} }
} }

View File

@ -1,8 +1,8 @@
use crate::entities::FieldType; use crate::entities::FieldType;
use crate::impl_type_option; use crate::impl_type_option;
use crate::services::cell::{ use crate::services::cell::{
try_decode_cell_data, CellBytes, CellBytesParser, CellData, CellDataChangeset, CellDataOperation, CellDisplayable, decode_cell_data_to_string, CellBytes, CellBytesParser, CellData, CellDataChangeset, CellDataOperation,
FromCellString, CellDisplayable, FromCellString,
}; };
use crate::services::field::{BoxTypeOptionBuilder, TypeOptionBuilder}; use crate::services::field::{BoxTypeOptionBuilder, TypeOptionBuilder};
use bytes::Bytes; use bytes::Bytes;
@ -44,6 +44,16 @@ impl CellDisplayable<String> for RichTextTypeOptionPB {
let cell_str: String = cell_data.try_into_inner()?; let cell_str: String = cell_data.try_into_inner()?;
Ok(CellBytes::new(cell_str)) Ok(CellBytes::new(cell_str))
} }
fn display_string(
&self,
cell_data: CellData<String>,
_decoded_field_type: &FieldType,
_field_rev: &FieldRevision,
) -> FlowyResult<String> {
let cell_str: String = cell_data.try_into_inner()?;
Ok(cell_str)
}
} }
impl CellDataOperation<String, String> for RichTextTypeOptionPB { impl CellDataOperation<String, String> for RichTextTypeOptionPB {
@ -57,8 +67,10 @@ impl CellDataOperation<String, String> for RichTextTypeOptionPB {
|| decoded_field_type.is_single_select() || decoded_field_type.is_single_select()
|| decoded_field_type.is_multi_select() || decoded_field_type.is_multi_select()
|| decoded_field_type.is_number() || 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 { } else {
self.display_data(cell_data, decoded_field_type, field_rev) self.display_data(cell_data, decoded_field_type, field_rev)
} }
@ -85,6 +97,14 @@ impl AsRef<str> for TextCellData {
} }
} }
impl std::ops::Deref for TextCellData {
type Target = String;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl FromCellString for TextCellData { impl FromCellString for TextCellData {
fn from_cell_str(s: &str) -> FlowyResult<Self> fn from_cell_str(s: &str) -> FlowyResult<Self>
where where
@ -94,6 +114,12 @@ impl FromCellString for TextCellData {
} }
} }
impl ToString for TextCellData {
fn to_string(&self) -> String {
self.0.clone()
}
}
pub struct TextCellDataParser(); pub struct TextCellDataParser();
impl CellBytesParser for TextCellDataParser { impl CellBytesParser for TextCellDataParser {
type Object = TextCellData; type Object = TextCellData;

View File

@ -42,6 +42,16 @@ impl CellDisplayable<URLCellDataPB> for URLTypeOptionPB {
let cell_data: URLCellDataPB = cell_data.try_into_inner()?; let cell_data: URLCellDataPB = cell_data.try_into_inner()?;
CellBytes::from(cell_data) CellBytes::from(cell_data)
} }
fn display_string(
&self,
cell_data: CellData<URLCellDataPB>,
_decoded_field_type: &FieldType,
_field_rev: &FieldRevision,
) -> FlowyResult<String> {
let cell_data: URLCellDataPB = cell_data.try_into_inner()?;
Ok(cell_data.content)
}
} }
impl CellDataOperation<URLCellDataPB, String> for URLTypeOptionPB { impl CellDataOperation<URLCellDataPB, String> for URLTypeOptionPB {

View File

@ -368,6 +368,7 @@ impl GridRevisionEditor {
Ok(row_pb) Ok(row_pb)
} }
#[tracing::instrument(level = "trace", skip_all, err)]
pub async fn move_group(&self, params: MoveGroupParams) -> FlowyResult<()> { pub async fn move_group(&self, params: MoveGroupParams) -> FlowyResult<()> {
let _ = self.view_manager.move_group(params).await?; let _ = self.view_manager.move_group(params).await?;
Ok(()) Ok(())
@ -435,14 +436,18 @@ impl GridRevisionEditor {
} }
pub async fn get_cell(&self, params: &GridCellIdParams) -> Option<GridCellPB> { pub async fn get_cell(&self, params: &GridCellIdParams) -> Option<GridCellPB> {
let cell_bytes = self.get_cell_bytes(params).await?; let (field_type, cell_bytes) = self.decode_any_cell_data(params).await?;
Some(GridCellPB::new(&params.field_id, cell_bytes.to_vec())) Some(GridCellPB::new(&params.field_id, field_type, cell_bytes.to_vec()))
} }
pub async fn get_cell_bytes(&self, params: &GridCellIdParams) -> Option<CellBytes> { pub async fn get_cell_bytes(&self, params: &GridCellIdParams) -> Option<CellBytes> {
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(&params.field_id).await?; let field_rev = self.get_field_rev(&params.field_id).await?;
let row_rev = self.block_manager.get_row_rev(&params.row_id).await.ok()??; let row_rev = self.block_manager.get_row_rev(&params.row_id).await.ok()??;
let cell_rev = row_rev.cells.get(&params.field_id)?.clone(); let cell_rev = row_rev.cells.get(&params.field_id)?.clone();
Some(decode_any_cell_data(cell_rev.data, &field_rev)) Some(decode_any_cell_data(cell_rev.data, &field_rev))
} }

View File

@ -173,6 +173,7 @@ impl GridViewRevisionEditor {
Ok(groups.into_iter().map(GroupPB::from).collect()) 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<()> { pub(crate) async fn move_group(&self, params: MoveGroupParams) -> FlowyResult<()> {
let _ = self let _ = self
.group_controller .group_controller
@ -180,7 +181,7 @@ impl GridViewRevisionEditor {
.await .await
.move_group(&params.from_group_id, &params.to_group_id)?; .move_group(&params.from_group_id, &params.to_group_id)?;
match self.group_controller.read().await.get_group(&params.from_group_id) { match self.group_controller.read().await.get_group(&params.from_group_id) {
None => {} None => tracing::warn!("Can not find the group with id: {}", params.from_group_id),
Some((index, group)) => { Some((index, group)) => {
let inserted_group = InsertedGroupPB { let inserted_group = InsertedGroupPB {
group: GroupPB::from(group), group: GroupPB::from(group),
@ -228,7 +229,11 @@ impl GridViewRevisionEditor {
let _ = self let _ = self
.modify(|pad| { .modify(|pad| {
let configuration = default_group_configuration(&field_rev); let configuration = default_group_configuration(&field_rev);
let changeset = pad.insert_group(&params.field_id, &params.field_type_rev, configuration)?; let changeset = pad.insert_or_update_group_configuration(
&params.field_id,
&params.field_type_rev,
configuration,
)?;
Ok(changeset) Ok(changeset)
}) })
.await?; .await?;
@ -496,10 +501,11 @@ impl GroupConfigurationWriter for GroupConfigurationWriterImpl {
let field_id = field_id.to_owned(); let field_id = field_id.to_owned();
wrap_future(async move { wrap_future(async move {
let changeset = view_pad let changeset = view_pad.write().await.insert_or_update_group_configuration(
.write() &field_id,
.await &field_type,
.insert_group(&field_id, &field_type, group_configuration)?; group_configuration,
)?;
if let Some(changeset) = changeset { if let Some(changeset) = changeset {
let _ = apply_change(&user_id, rev_manager, changeset).await?; let _ = apply_change(&user_id, rev_manager, changeset).await?;

View File

@ -1,5 +1,5 @@
use crate::entities::{GroupPB, GroupViewChangesetPB}; 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_error::{FlowyError, FlowyResult};
use flowy_grid_data_model::revision::{ use flowy_grid_data_model::revision::{
FieldRevision, FieldTypeRevision, GroupConfigurationContentSerde, GroupConfigurationRevision, GroupRevision, FieldRevision, FieldTypeRevision, GroupConfigurationContentSerde, GroupConfigurationRevision, GroupRevision,
@ -29,10 +29,7 @@ impl<T> std::fmt::Display for GroupContext<T> {
self.groups_map.iter().for_each(|(_, group)| { 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!("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(()) Ok(())
} }
} }
@ -44,7 +41,7 @@ pub struct GroupContext<C> {
field_rev: Arc<FieldRevision>, field_rev: Arc<FieldRevision>,
groups_map: IndexMap<String, Group>, groups_map: IndexMap<String, Group>,
/// default_group is used to store the rows that don't belong to any groups. /// default_group is used to store the rows that don't belong to any groups.
default_group: Group, // default_group: Group,
writer: Arc<dyn GroupConfigurationWriter>, writer: Arc<dyn GroupConfigurationWriter>,
} }
@ -59,16 +56,6 @@ where
reader: Arc<dyn GroupConfigurationReader>, reader: Arc<dyn GroupConfigurationReader>,
writer: Arc<dyn GroupConfigurationWriter>, writer: Arc<dyn GroupConfigurationWriter>,
) -> FlowyResult<Self> { ) -> FlowyResult<Self> {
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 { let configuration = match reader.get_configuration().await {
None => { None => {
let default_configuration = default_group_configuration(&field_rev); let default_configuration = default_group_configuration(&field_rev);
@ -80,24 +67,22 @@ where
Some(configuration) => configuration, Some(configuration) => configuration,
}; };
// let configuration = C::from_configuration_content(&configuration_rev.content)?;
Ok(Self { Ok(Self {
view_id, view_id,
field_rev, field_rev,
groups_map: IndexMap::new(), groups_map: IndexMap::new(),
default_group,
writer, writer,
configuration, configuration,
configuration_content: PhantomData, configuration_content: PhantomData,
}) })
} }
pub(crate) fn get_default_group(&self) -> &Group { pub(crate) fn get_default_group(&self) -> Option<&Group> {
&self.default_group self.groups_map.get(&self.field_rev.id)
} }
pub(crate) fn get_mut_default_group(&mut self) -> &mut Group { pub(crate) fn get_mut_default_group(&mut self) -> Option<&mut Group> {
&mut self.default_group self.groups_map.get_mut(&self.field_rev.id)
} }
/// Returns the groups without the default group /// Returns the groups without the default group
@ -122,8 +107,6 @@ where
self.groups_map.iter_mut().for_each(|(_, group)| { self.groups_map.iter_mut().for_each(|(_, group)| {
each(group); each(group);
}); });
each(&mut self.default_group);
} }
pub(crate) fn move_group(&mut self, from_id: &str, to_id: &str) -> FlowyResult<()> { 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); let to_index = self.groups_map.get_index_of(to_id);
match (from_index, to_index) { match (from_index, to_index) {
(Some(from_index), Some(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| { self.mut_configuration(|configuration| {
let from_index = configuration.groups.iter().position(|group| group.id == from_id); let from_index = configuration.groups.iter().position(|group| group.id == from_id);
let to_index = configuration.groups.iter().position(|group| group.id == to_id); let to_index = configuration.groups.iter().position(|group| group.id == to_id);
if let (Some(from), Some(to)) = (from_index, to_index) { tracing::info!("Configuration groups: {:?} ", configuration.groups);
configuration.groups.swap(from, to); 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(()) 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( pub(crate) fn init_groups(
&mut self, &mut self,
generated_groups: Vec<GeneratedGroup>, generated_groups: Vec<GeneratedGroup>,
reset: bool,
) -> FlowyResult<Option<GroupViewChangesetPB>> { ) -> FlowyResult<Option<GroupViewChangesetPB>> {
let mut new_groups = vec![]; let mut new_groups = vec![];
let mut filter_content_map = HashMap::new(); let mut filter_content_map = HashMap::new();
@ -159,16 +146,17 @@ where
new_groups.push(generate_group.group_rev); 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 { let MergeGroupResult {
mut all_group_revs, mut all_group_revs,
new_group_revs, new_group_revs,
updated_group_revs: _, updated_group_revs: _,
deleted_group_revs, deleted_group_revs,
} = if reset { } = merge_groups(old_groups, new_groups);
merge_groups(&[], new_groups)
} else {
merge_groups(&self.configuration.groups, new_groups)
};
let deleted_group_ids = deleted_group_revs let deleted_group_ids = deleted_group_revs
.into_iter() .into_iter()
@ -197,31 +185,23 @@ where
Some(pos) => { Some(pos) => {
let mut old_group = configuration.groups.remove(pos); let mut old_group = configuration.groups.remove(pos);
group_rev.update_with_other(&old_group); group_rev.update_with_other(&old_group);
is_changed = is_group_changed(group_rev, &old_group);
// Take the GroupRevision if the name has changed old_group.name = group_rev.name.clone();
if is_group_changed(group_rev, &old_group) { configuration.groups.insert(pos, old_group);
old_group.name = group_rev.name.clone();
is_changed = true;
configuration.groups.insert(pos, old_group);
}
} }
} }
} }
is_changed 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| { all_group_revs.into_iter().for_each(|group_rev| {
if let Some(filter_content) = filter_content_map.get(&group_rev.id) { let filter_content = filter_content_map
let group = Group::new( .get(&group_rev.id)
group_rev.id, .cloned()
self.field_rev.id.clone(), .unwrap_or_else(|| "".to_owned());
group_rev.name, let group = Group::new(group_rev.id, self.field_rev.id.clone(), group_rev.name, filter_content);
filter_content.clone(), self.groups_map.insert(group.id.clone(), group);
);
self.groups_map.insert(group.id.clone(), group);
}
}); });
let new_groups = new_group_revs let new_groups = new_group_revs
@ -269,6 +249,7 @@ where
Ok(()) Ok(())
} }
#[tracing::instrument(level = "trace", skip_all, err)]
pub fn save_configuration(&self) -> FlowyResult<()> { pub fn save_configuration(&self) -> FlowyResult<()> {
let configuration = (&*self.configuration).clone(); let configuration = (&*self.configuration).clone();
let writer = self.writer.clone(); let writer = self.writer.clone();
@ -311,13 +292,14 @@ where
} }
} }
fn merge_groups(old_groups: &[GroupRevision], new_groups: Vec<GroupRevision>) -> MergeGroupResult { fn merge_groups(old_groups: Vec<GroupRevision>, new_groups: Vec<GroupRevision>) -> MergeGroupResult {
let mut merge_result = MergeGroupResult::new(); let mut merge_result = MergeGroupResult::new();
if old_groups.is_empty() { // if old_groups.is_empty() {
merge_result.all_group_revs = new_groups.clone(); // merge_result.all_group_revs.extend(new_groups.clone());
merge_result.new_group_revs = new_groups; // merge_result.all_group_revs.push(default_group);
return merge_result; // merge_result.new_group_revs = new_groups;
} // return merge_result;
// }
// group_map is a helper map is used to filter out the new groups. // group_map is a helper map is used to filter out the new groups.
let mut new_group_map: IndexMap<String, GroupRevision> = IndexMap::new(); let mut new_group_map: IndexMap<String, GroupRevision> = IndexMap::new();
@ -329,19 +311,20 @@ fn merge_groups(old_groups: &[GroupRevision], new_groups: Vec<GroupRevision>) ->
for old in old_groups { for old in old_groups {
if let Some(new) = new_group_map.remove(&old.id) { if let Some(new) = new_group_map.remove(&old.id) {
merge_result.all_group_revs.push(new.clone()); 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); merge_result.updated_group_revs.push(new);
} }
} else { } else {
merge_result.deleted_group_revs.push(old.clone()); merge_result.all_group_revs.push(old);
} }
} }
// Find out the new groups // Find out the new groups
new_group_map.reverse();
let new_groups = new_group_map.into_values(); let new_groups = new_group_map.into_values();
for (_, group) in new_groups.into_iter().enumerate() { for (_, group) in new_groups.into_iter().enumerate() {
merge_result.all_group_revs.push(group.clone()); merge_result.all_group_revs.insert(0, group.clone());
merge_result.new_group_revs.push(group); merge_result.new_group_revs.insert(0, group);
} }
merge_result merge_result
} }

View File

@ -88,7 +88,7 @@ where
pub async fn new(field_rev: &Arc<FieldRevision>, mut configuration: GroupContext<C>) -> FlowyResult<Self> { pub async fn new(field_rev: &Arc<FieldRevision>, mut configuration: GroupContext<C>) -> FlowyResult<Self> {
let type_option = field_rev.get_type_option::<T>(field_rev.ty); let type_option = field_rev.get_type_option::<T>(field_rev.ty);
let groups = G::generate_groups(&field_rev.id, &configuration, &type_option); let groups = G::generate_groups(&field_rev.id, &configuration, &type_option);
let _ = configuration.init_groups(groups, true)?; let _ = configuration.init_groups(groups)?;
Ok(Self { Ok(Self {
field_id: field_rev.id.clone(), field_id: field_rev.id.clone(),
@ -105,8 +105,8 @@ where
&mut self, &mut self,
row_rev: &RowRevision, row_rev: &RowRevision,
other_group_changesets: &[GroupChangesetPB], other_group_changesets: &[GroupChangesetPB],
) -> GroupChangesetPB { ) -> Option<GroupChangesetPB> {
let default_group = self.group_ctx.get_mut_default_group(); let default_group = self.group_ctx.get_mut_default_group()?;
// [other_group_inserted_row] contains all the inserted rows except the default group. // [other_group_inserted_row] contains all the inserted rows except the default group.
let other_group_inserted_row = other_group_changesets let other_group_inserted_row = other_group_changesets
@ -163,7 +163,7 @@ where
} }
default_group.rows.retain(|row| !deleted_row_ids.contains(&row.id)); default_group.rows.retain(|row| !deleted_row_ids.contains(&row.id));
changeset.deleted_rows.extend(deleted_row_ids); changeset.deleted_rows.extend(deleted_row_ids);
changeset Some(changeset)
} }
} }
@ -182,11 +182,14 @@ where
fn groups(&self) -> Vec<Group> { fn groups(&self) -> Vec<Group> {
if self.use_default_group() { if self.use_default_group() {
let mut groups: Vec<Group> = 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() self.group_ctx.groups().into_iter().cloned().collect()
} else {
self.group_ctx
.groups()
.into_iter()
.filter(|group| group.id != self.field_id)
.cloned()
.collect::<Vec<_>>()
} }
} }
@ -205,7 +208,7 @@ where
if let Some(cell_rev) = cell_rev { if let Some(cell_rev) = cell_rev {
let mut grouped_rows: Vec<GroupedRow> = vec![]; let mut grouped_rows: Vec<GroupedRow> = 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::<P>()?; let cell_data = cell_bytes.parser::<P>()?;
for group in self.group_ctx.groups() { for group in self.group_ctx.groups() {
if self.can_group(&group.filter_content, &cell_data) { if self.can_group(&group.filter_content, &cell_data) {
@ -216,17 +219,18 @@ where
} }
} }
if grouped_rows.is_empty() { if !grouped_rows.is_empty() {
self.group_ctx.get_mut_default_group().add_row(row_rev.into());
} else {
for group_row in grouped_rows { for group_row in grouped_rows {
if let Some(group) = self.group_ctx.get_mut_group(&group_row.group_id) { if let Some(group) = self.group_ctx.get_mut_group(&group_row.group_id) {
group.add_row(group_row.row); 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, field_rev: &FieldRevision,
) -> FlowyResult<Vec<GroupChangesetPB>> { ) -> FlowyResult<Vec<GroupChangesetPB>> {
if let Some(cell_rev) = row_rev.cells.get(&self.field_id) { 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::<P>()?; let cell_data = cell_bytes.parser::<P>()?;
let mut changesets = self.add_row_if_match(row_rev, &cell_data); let mut changesets = self.add_row_if_match(row_rev, &cell_data);
let default_group_changeset = self.update_default_group(row_rev, &changesets); if let Some(default_group_changeset) = self.update_default_group(row_rev, &changesets) {
tracing::trace!("default_group_changeset: {}", default_group_changeset); tracing::trace!("default_group_changeset: {}", default_group_changeset);
if !default_group_changeset.is_empty() { if !default_group_changeset.is_empty() {
changesets.push(default_group_changeset); changesets.push(default_group_changeset);
}
} }
Ok(changesets) Ok(changesets)
} else { } else {
@ -265,15 +270,16 @@ where
) -> FlowyResult<Vec<GroupChangesetPB>> { ) -> FlowyResult<Vec<GroupChangesetPB>> {
// if the cell_rev is none, then the row must be crated from the default group. // 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) { 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::<P>()?; let cell_data = cell_bytes.parser::<P>()?;
Ok(self.remove_row_if_match(row_rev, &cell_data)) Ok(self.remove_row_if_match(row_rev, &cell_data))
} else { } else if let Some(group) = self.group_ctx.get_default_group() {
let group = self.group_ctx.get_default_group();
Ok(vec![GroupChangesetPB::delete( Ok(vec![GroupChangesetPB::delete(
group.id.clone(), group.id.clone(),
vec![row_rev.id.clone()], vec![row_rev.id.clone()],
)]) )])
} else {
Ok(vec![])
} }
} }
@ -285,7 +291,7 @@ where
}; };
if let Some(cell_rev) = cell_rev { 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::<P>()?; let cell_data = cell_bytes.parser::<P>()?;
Ok(self.move_row(&cell_data, context)) Ok(self.move_row(&cell_data, context))
} else { } else {
@ -297,7 +303,7 @@ where
fn did_update_field(&mut self, field_rev: &FieldRevision) -> FlowyResult<Option<GroupViewChangesetPB>> { fn did_update_field(&mut self, field_rev: &FieldRevision) -> FlowyResult<Option<GroupViewChangesetPB>> {
let type_option = field_rev.get_type_option::<T>(field_rev.ty); let type_option = field_rev.get_type_option::<T>(field_rev.ty);
let groups = G::generate_groups(&field_rev.id, &self.group_ctx, &type_option); 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) Ok(changeset)
} }
} }

View File

@ -9,16 +9,17 @@ pub struct Group {
pub is_visible: bool, pub is_visible: bool,
pub(crate) rows: Vec<RowPB>, pub(crate) rows: Vec<RowPB>,
/// [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, pub filter_content: String,
} }
impl Group { impl Group {
pub fn new(id: String, field_id: String, name: String, filter_content: String) -> Self { pub fn new(id: String, field_id: String, name: String, filter_content: String) -> Self {
let is_default = id == field_id;
Self { Self {
id, id,
field_id, field_id,
is_default: false, is_default,
is_visible: true, is_visible: true,
name, name,
rows: vec![], rows: vec![],

View File

@ -8,8 +8,8 @@ use crate::services::group::{
use flowy_error::FlowyResult; use flowy_error::FlowyResult;
use flowy_grid_data_model::revision::{ use flowy_grid_data_model::revision::{
CheckboxGroupConfigurationRevision, DateGroupConfigurationRevision, FieldRevision, GroupConfigurationRevision, CheckboxGroupConfigurationRevision, DateGroupConfigurationRevision, FieldRevision, GroupConfigurationRevision,
LayoutRevision, NumberGroupConfigurationRevision, RowRevision, SelectOptionGroupConfigurationRevision, GroupRevision, LayoutRevision, NumberGroupConfigurationRevision, RowRevision,
TextGroupConfigurationRevision, UrlGroupConfigurationRevision, SelectOptionGroupConfigurationRevision, TextGroupConfigurationRevision, UrlGroupConfigurationRevision,
}; };
use std::sync::Arc; 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_id = field_rev.id.clone();
let field_type_rev = field_rev.ty; let field_type_rev = field_rev.ty;
let field_type: FieldType = field_rev.ty.into(); let field_type: FieldType = field_rev.ty.into();
match field_type { let mut group_configuration_rev = match field_type {
FieldType::RichText => { FieldType::RichText => {
GroupConfigurationRevision::new(field_id, field_type_rev, TextGroupConfigurationRevision::default()) GroupConfigurationRevision::new(field_id, field_type_rev, TextGroupConfigurationRevision::default())
.unwrap() .unwrap()
@ -112,5 +112,23 @@ pub fn default_group_configuration(field_rev: &FieldRevision) -> GroupConfigurat
FieldType::URL => { FieldType::URL => {
GroupConfigurationRevision::new(field_id, field_type_rev, UrlGroupConfigurationRevision::default()).unwrap() 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,
} }
} }

Some files were not shown because too many files have changed in this diff Show More