mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
Merge branch 'main' into latex
This commit is contained in:
commit
c9fc9e7497
@ -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",
|
||||||
|
@ -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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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": "看板"
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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,
|
||||||
|
@ -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);
|
||||||
|
@ -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(
|
||||||
|
@ -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,
|
||||||
),
|
),
|
||||||
|
@ -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),
|
||||||
|
@ -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,
|
||||||
|
@ -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),
|
||||||
)
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -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
|
||||||
|
@ -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),
|
||||||
|
@ -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,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@ -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),
|
||||||
|
@ -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,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -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),
|
||||||
|
@ -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,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
@ -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) {
|
||||||
|
@ -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(),
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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(
|
||||||
|
@ -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
|
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
@ -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) {
|
||||||
|
@ -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 {}
|
||||||
|
@ -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,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -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.
|
||||||
|
@ -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 {
|
||||||
|
@ -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,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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, _, __) {
|
||||||
|
@ -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
|
||||||
|
@ -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": {}
|
||||||
|
}
|
@ -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": "l’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": "souligner",
|
||||||
"@underline": {}
|
"@underline": {}
|
||||||
}
|
}
|
@ -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": {}
|
||||||
}
|
}
|
@ -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": {}
|
||||||
}
|
}
|
@ -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": {}
|
||||||
}
|
}
|
@ -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": {}
|
||||||
}
|
}
|
@ -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": {}
|
||||||
|
}
|
@ -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": {}
|
||||||
}
|
}
|
@ -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": {}
|
||||||
}
|
}
|
@ -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;
|
||||||
|
}
|
@ -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,
|
||||||
|
);
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
@ -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);
|
||||||
|
@ -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);
|
||||||
|
@ -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,
|
||||||
|
@ -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':
|
||||||
|
@ -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í")
|
||||||
|
};
|
||||||
|
}
|
@ -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("l’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("souligner")
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -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é")
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -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")
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -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")
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -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")
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -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")
|
||||||
|
};
|
||||||
|
}
|
@ -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")
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -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")
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -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'),
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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(
|
||||||
|
@ -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,
|
||||||
);
|
);
|
||||||
|
@ -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,
|
||||||
|
@ -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, _, __) {
|
||||||
|
@ -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) {
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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(
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
};
|
@ -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;
|
||||||
|
};
|
@ -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,
|
||||||
|
),
|
||||||
];
|
];
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
@ -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 😁 ',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
8
frontend/rust-lib/Cargo.lock
generated
8
frontend/rust-lib/Cargo.lock
generated
@ -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",
|
||||||
|
@ -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"}
|
||||||
|
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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.
|
||||||
|
@ -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 {
|
||||||
|
@ -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 {
|
||||||
|
@ -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>,
|
||||||
|
@ -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>> {
|
||||||
|
@ -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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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 {
|
||||||
|
@ -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(¶ms.field_id, cell_bytes.to_vec()))
|
Some(GridCellPB::new(¶ms.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(¶ms.field_id).await?;
|
let field_rev = self.get_field_rev(¶ms.field_id).await?;
|
||||||
let row_rev = self.block_manager.get_row_rev(¶ms.row_id).await.ok()??;
|
let row_rev = self.block_manager.get_row_rev(¶ms.row_id).await.ok()??;
|
||||||
|
|
||||||
let cell_rev = row_rev.cells.get(¶ms.field_id)?.clone();
|
let cell_rev = row_rev.cells.get(¶ms.field_id)?.clone();
|
||||||
Some(decode_any_cell_data(cell_rev.data, &field_rev))
|
Some(decode_any_cell_data(cell_rev.data, &field_rev))
|
||||||
}
|
}
|
||||||
|
@ -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(¶ms.from_group_id, ¶ms.to_group_id)?;
|
.move_group(¶ms.from_group_id, ¶ms.to_group_id)?;
|
||||||
match self.group_controller.read().await.get_group(¶ms.from_group_id) {
|
match self.group_controller.read().await.get_group(¶ms.from_group_id) {
|
||||||
None => {}
|
None => tracing::warn!("Can not find the group with id: {}", params.from_group_id),
|
||||||
Some((index, group)) => {
|
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(¶ms.field_id, ¶ms.field_type_rev, configuration)?;
|
let changeset = pad.insert_or_update_group_configuration(
|
||||||
|
¶ms.field_id,
|
||||||
|
¶ms.field_type_rev,
|
||||||
|
configuration,
|
||||||
|
)?;
|
||||||
Ok(changeset)
|
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?;
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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![],
|
||||||
|
@ -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
Loading…
x
Reference in New Issue
Block a user