mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
Merge branch 'main' into main
This commit is contained in:
commit
b8cbbf7454
25
CHANGELOG.md
25
CHANGELOG.md
@ -1,5 +1,30 @@
|
|||||||
# Release Notes
|
# Release Notes
|
||||||
|
|
||||||
|
## Version 0.0.5.3 - 09/26/2022
|
||||||
|
|
||||||
|
New features
|
||||||
|
- Open the next page automatically after deleting the current page
|
||||||
|
- Refresh the Kanban board after altering a property type
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
- Fix switch board bug
|
||||||
|
- Fix delete the Kanban board's row error
|
||||||
|
- Remove duplicate time format
|
||||||
|
- Fix can't delete field in property edit panel
|
||||||
|
- Adjust some display UI issues
|
||||||
|
|
||||||
|
|
||||||
|
## Version 0.0.5.2 - 09/16/2022
|
||||||
|
|
||||||
|
New features
|
||||||
|
- Enable adding a new card to the "No Status" group
|
||||||
|
- Fix some bugs
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
- Fix cannot open AppFlowy error
|
||||||
|
- Fix delete the Kanban board's row error
|
||||||
|
|
||||||
|
|
||||||
## Version 0.0.5.1 - 09/14/2022
|
## Version 0.0.5.1 - 09/14/2022
|
||||||
|
|
||||||
New features
|
New features
|
||||||
|
@ -181,7 +181,7 @@
|
|||||||
"includeTime": " Include time",
|
"includeTime": " Include time",
|
||||||
"dateFormatFriendly": "Month Day,Year",
|
"dateFormatFriendly": "Month Day,Year",
|
||||||
"dateFormatISO": "Year-Month-Day",
|
"dateFormatISO": "Year-Month-Day",
|
||||||
"dateFormatLocal": "Year/Month/Day",
|
"dateFormatLocal": "Month/Day/Year",
|
||||||
"dateFormatUS": "Year/Month/Day",
|
"dateFormatUS": "Year/Month/Day",
|
||||||
"timeFormat": " Time format",
|
"timeFormat": " Time format",
|
||||||
"invalidTimeFormat": "Invalid format",
|
"invalidTimeFormat": "Invalid format",
|
||||||
@ -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",
|
||||||
@ -231,4 +232,4 @@
|
|||||||
"create_new_card": "New"
|
"create_new_card": "New"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -172,7 +172,7 @@
|
|||||||
"includeTime": " Incluir tiempo",
|
"includeTime": " Incluir tiempo",
|
||||||
"dateFormatFriendly": "Mes Día, Año",
|
"dateFormatFriendly": "Mes Día, Año",
|
||||||
"dateFormatISO": "Año-Mes-Día",
|
"dateFormatISO": "Año-Mes-Día",
|
||||||
"dateFormatLocal": "Año/Mes/Día",
|
"dateFormatLocal": "Mes/Día/Año",
|
||||||
"dateFormatUS": "Año/Mes/Día",
|
"dateFormatUS": "Año/Mes/Día",
|
||||||
"timeFormat": " Time format",
|
"timeFormat": " Time format",
|
||||||
"invalidTimeFormat": "Formato de tiempo",
|
"invalidTimeFormat": "Formato de tiempo",
|
||||||
|
@ -149,7 +149,7 @@
|
|||||||
"grid": {
|
"grid": {
|
||||||
"settings": {
|
"settings": {
|
||||||
"filter": "Filtrer",
|
"filter": "Filtrer",
|
||||||
"sortBy": "Trier par",
|
"sortBy": "Filtrer par",
|
||||||
"Properties": "Propriétés"
|
"Properties": "Propriétés"
|
||||||
},
|
},
|
||||||
"field": {
|
"field": {
|
||||||
@ -170,7 +170,7 @@
|
|||||||
"includeTime": " Inclure l'heure",
|
"includeTime": " Inclure l'heure",
|
||||||
"dateFormatFriendly": "Mois Jour, Année",
|
"dateFormatFriendly": "Mois Jour, Année",
|
||||||
"dateFormatISO": "Année-Mois-Jour",
|
"dateFormatISO": "Année-Mois-Jour",
|
||||||
"dateFormatLocal": "Année/Mois/Jour",
|
"dateFormatLocal": "Mois/Jour/Année",
|
||||||
"dateFormatUS": "Année/Mois/Jour",
|
"dateFormatUS": "Année/Mois/Jour",
|
||||||
"timeFormat": " Format du temps",
|
"timeFormat": " Format du temps",
|
||||||
"invalidTimeFormat": "Format invalide",
|
"invalidTimeFormat": "Format invalide",
|
||||||
|
@ -173,7 +173,7 @@
|
|||||||
"includeTime": " Sertakan waktu",
|
"includeTime": " Sertakan waktu",
|
||||||
"dateFormatFriendly": "Bulan Hari,Tahun",
|
"dateFormatFriendly": "Bulan Hari,Tahun",
|
||||||
"dateFormatISO": "Tahun-Bulan-Hari",
|
"dateFormatISO": "Tahun-Bulan-Hari",
|
||||||
"dateFormatLocal": "Tahun/Bulan/Hari",
|
"dateFormatLocal": "Bulan/Hari/Tahun",
|
||||||
"dateFormatUS": "Tahun/Bulan/Hari",
|
"dateFormatUS": "Tahun/Bulan/Hari",
|
||||||
"timeFormat": " Format waktu",
|
"timeFormat": " Format waktu",
|
||||||
"invalidTimeFormat": "Format yang tidak valid",
|
"invalidTimeFormat": "Format yang tidak valid",
|
||||||
|
@ -165,7 +165,7 @@
|
|||||||
"includeTime": " 時刻を含める",
|
"includeTime": " 時刻を含める",
|
||||||
"dateFormatFriendly": "月 日,年",
|
"dateFormatFriendly": "月 日,年",
|
||||||
"dateFormatISO": "年-月-日",
|
"dateFormatISO": "年-月-日",
|
||||||
"dateFormatLocal": "年/月/日",
|
"dateFormatLocal": "月/日/年",
|
||||||
"dateFormatUS": "年/月/日",
|
"dateFormatUS": "年/月/日",
|
||||||
"timeFormat": " 時刻書式",
|
"timeFormat": " 時刻書式",
|
||||||
"timeFormatTwelveHour": "12 時間表記",
|
"timeFormatTwelveHour": "12 時間表記",
|
||||||
|
@ -63,13 +63,13 @@
|
|||||||
"deletePermanent": "Apagar permanentemente"
|
"deletePermanent": "Apagar permanentemente"
|
||||||
},
|
},
|
||||||
"dialogCreatePageNameHint": "Nome da página",
|
"dialogCreatePageNameHint": "Nome da página",
|
||||||
"questionBubble": {
|
"questionBubble": {
|
||||||
"whatsNew": "O que há de novo?",
|
"whatsNew": "O que há de novo?",
|
||||||
"help": "Ajuda e Suporte",
|
"help": "Ajuda e Suporte",
|
||||||
"debug": {
|
"debug": {
|
||||||
"name": "Informação de depuração",
|
"name": "Informação de depuração",
|
||||||
"success": "Informação de depuração copiada para a área de transferência!",
|
"success": "Informação de depuração copiada para a área de transferência!",
|
||||||
"fail": "Falha ao copiar a informação de depuração para a área de transferência"
|
"fail": "Falha ao copiar a informação de depuração para a área de transferência"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"menuAppHeader": {
|
"menuAppHeader": {
|
||||||
@ -148,7 +148,7 @@
|
|||||||
"menu": {
|
"menu": {
|
||||||
"appearance": "Aparência",
|
"appearance": "Aparência",
|
||||||
"language": "Idioma",
|
"language": "Idioma",
|
||||||
"user":"Usuário",
|
"user": "Usuário",
|
||||||
"open": "Abrir as Configurações"
|
"open": "Abrir as Configurações"
|
||||||
},
|
},
|
||||||
"appearance": {
|
"appearance": {
|
||||||
@ -181,7 +181,7 @@
|
|||||||
"includeTime": "Incluir horário",
|
"includeTime": "Incluir horário",
|
||||||
"dateFormatFriendly": "Mês/Dia/Ano",
|
"dateFormatFriendly": "Mês/Dia/Ano",
|
||||||
"dateFormatISO": "Ano/Mês/Dia",
|
"dateFormatISO": "Ano/Mês/Dia",
|
||||||
"dateFormatLocal": "Ano/Mês/Dia",
|
"dateFormatLocal": "Mês/Dia/Ano",
|
||||||
"dateFormatUS": "Ano/Mês/Dia",
|
"dateFormatUS": "Ano/Mês/Dia",
|
||||||
"timeFormat": "Formato de hora",
|
"timeFormat": "Formato de hora",
|
||||||
"invalidTimeFormat": "Formato Inválido",
|
"invalidTimeFormat": "Formato Inválido",
|
||||||
@ -231,4 +231,4 @@
|
|||||||
"create_new_card": "Novo"
|
"create_new_card": "Novo"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -94,9 +94,9 @@
|
|||||||
},
|
},
|
||||||
"tooltip": {
|
"tooltip": {
|
||||||
"darkMode": "Переключиться в тёмную тему",
|
"darkMode": "Переключиться в тёмную тему",
|
||||||
"openAsPage": "Открыть как страницу",
|
"openAsPage": "Открыть как страницу",
|
||||||
"addNewRow": "Добавить новую строку",
|
"addNewRow": "Добавить новую строку",
|
||||||
"openMenu": "Открыть меню"
|
"openMenu": "Открыть меню"
|
||||||
},
|
},
|
||||||
"sideBar": {
|
"sideBar": {
|
||||||
"closeSidebar": "Закрыть боковое меню",
|
"closeSidebar": "Закрыть боковое меню",
|
||||||
@ -180,7 +180,7 @@
|
|||||||
"includeTime": " Время",
|
"includeTime": " Время",
|
||||||
"dateFormatFriendly": "День Месяц, Год",
|
"dateFormatFriendly": "День Месяц, Год",
|
||||||
"dateFormatISO": "Год-Месяц-День",
|
"dateFormatISO": "Год-Месяц-День",
|
||||||
"dateFormatLocal": "Год/Месяц/День",
|
"dateFormatLocal": "Месяц/День/Год",
|
||||||
"dateFormatUS": "Год/Месяц/День",
|
"dateFormatUS": "Год/Месяц/День",
|
||||||
"timeFormat": " Форматировать время",
|
"timeFormat": " Форматировать время",
|
||||||
"invalidTimeFormat": "Неверный формат",
|
"invalidTimeFormat": "Неверный формат",
|
||||||
|
@ -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": "隐藏",
|
||||||
@ -177,7 +181,7 @@
|
|||||||
"includeTime": " 包含时间",
|
"includeTime": " 包含时间",
|
||||||
"dateFormatFriendly": "月 日,年",
|
"dateFormatFriendly": "月 日,年",
|
||||||
"dateFormatISO": "年-月-日",
|
"dateFormatISO": "年-月-日",
|
||||||
"dateFormatLocal": "年/月/日",
|
"dateFormatLocal": "月/日/年",
|
||||||
"dateFormatUS": "年/月/日",
|
"dateFormatUS": "年/月/日",
|
||||||
"timeFormat": " 时间格式",
|
"timeFormat": " 时间格式",
|
||||||
"invalidTimeFormat": "时间格式错误",
|
"invalidTimeFormat": "时间格式错误",
|
||||||
@ -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": "看板"
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -173,7 +173,7 @@
|
|||||||
"includeTime": " 包含時間",
|
"includeTime": " 包含時間",
|
||||||
"dateFormatFriendly": "月 日,年",
|
"dateFormatFriendly": "月 日,年",
|
||||||
"dateFormatISO": "年-月-日",
|
"dateFormatISO": "年-月-日",
|
||||||
"dateFormatLocal": "年/月/日",
|
"dateFormatLocal": "月/日/年",
|
||||||
"dateFormatUS": "年/月/日",
|
"dateFormatUS": "年/月/日",
|
||||||
"timeFormat": " 時間格式",
|
"timeFormat": " 時間格式",
|
||||||
"invalidTimeFormat": "格式無效",
|
"invalidTimeFormat": "格式無效",
|
||||||
|
@ -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,
|
||||||
|
@ -68,9 +68,11 @@ class GridPluginDisplay extends PluginDisplay {
|
|||||||
@override
|
@override
|
||||||
Widget buildWidget(PluginContext context) {
|
Widget buildWidget(PluginContext context) {
|
||||||
notifier.isDeleted.addListener(() {
|
notifier.isDeleted.addListener(() {
|
||||||
if (notifier.isDeleted.value) {
|
notifier.isDeleted.value.fold(() => null, (deletedView) {
|
||||||
context.onDeleted(view);
|
if (deletedView.hasIndex()) {
|
||||||
}
|
context.onDeleted(view, deletedView.index);
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return BoardPage(key: ValueKey(view.id), view: view);
|
return BoardPage(key: ValueKey(view.id), view: view);
|
||||||
|
@ -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);
|
||||||
|
@ -106,8 +106,11 @@ class _SettingItem extends StatelessWidget {
|
|||||||
height: 30,
|
height: 30,
|
||||||
child: FlowyButton(
|
child: FlowyButton(
|
||||||
isSelected: isSelected,
|
isSelected: isSelected,
|
||||||
text: FlowyText.medium(action.title(),
|
text: FlowyText.medium(
|
||||||
fontSize: 12, color: theme.textColor),
|
action.title(),
|
||||||
|
fontSize: 12,
|
||||||
|
color: theme.textColor,
|
||||||
|
),
|
||||||
hoverColor: theme.hover,
|
hoverColor: theme.hover,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
context
|
context
|
||||||
|
@ -74,15 +74,26 @@ class DocumentPlugin extends Plugin<int> {
|
|||||||
class DocumentPluginDisplay extends PluginDisplay with NavigationItem {
|
class DocumentPluginDisplay extends PluginDisplay with NavigationItem {
|
||||||
final ViewPluginNotifier notifier;
|
final ViewPluginNotifier notifier;
|
||||||
ViewPB get view => notifier.view;
|
ViewPB get view => notifier.view;
|
||||||
|
int? deletedViewIndex;
|
||||||
|
|
||||||
DocumentPluginDisplay({required this.notifier, Key? key});
|
DocumentPluginDisplay({required this.notifier, Key? key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget buildWidget(PluginContext context) => DocumentPage(
|
Widget buildWidget(PluginContext context) {
|
||||||
view: view,
|
notifier.isDeleted.addListener(() {
|
||||||
onDeleted: () => context.onDeleted(view),
|
notifier.isDeleted.value.fold(() => null, (deletedView) {
|
||||||
key: ValueKey(view.id),
|
if (deletedView.hasIndex()) {
|
||||||
);
|
deletedViewIndex = deletedView.index;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return DocumentPage(
|
||||||
|
view: view,
|
||||||
|
onDeleted: () => context.onDeleted(view, deletedViewIndex),
|
||||||
|
key: ValueKey(view.id),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget get leftBarItem => ViewLeftBarItem(view: view);
|
Widget get leftBarItem => ViewLeftBarItem(view: view);
|
||||||
@ -120,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(
|
||||||
@ -195,9 +206,6 @@ class ShareActions with ActionList<ShareActionWrapper>, FlowyOverlayDelegate {
|
|||||||
|
|
||||||
ShareActions({required this.onSelected});
|
ShareActions({required this.onSelected});
|
||||||
|
|
||||||
@override
|
|
||||||
double get maxWidth => 130;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
double get itemHeight => 22;
|
double get itemHeight => 22;
|
||||||
|
|
||||||
|
@ -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,
|
||||||
),
|
),
|
||||||
|
@ -54,6 +54,9 @@ class SelectOptionCellEditorBloc
|
|||||||
selectOption: (_SelectOption value) {
|
selectOption: (_SelectOption value) {
|
||||||
_onSelectOption(value.optionId);
|
_onSelectOption(value.optionId);
|
||||||
},
|
},
|
||||||
|
trySelectOption: (_TrySelectOption value) {
|
||||||
|
_trySelectOption(value.optionName, emit);
|
||||||
|
},
|
||||||
filterOption: (_SelectOptionFilter value) {
|
filterOption: (_SelectOptionFilter value) {
|
||||||
_filterOption(value.optionName, emit);
|
_filterOption(value.optionName, emit);
|
||||||
},
|
},
|
||||||
@ -100,6 +103,36 @@ class SelectOptionCellEditorBloc
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _trySelectOption(
|
||||||
|
String optionName, Emitter<SelectOptionEditorState> emit) async {
|
||||||
|
SelectOptionPB? matchingOption;
|
||||||
|
bool optionExistsButSelected = false;
|
||||||
|
|
||||||
|
for (final option in state.options) {
|
||||||
|
if (option.name.toLowerCase() == optionName.toLowerCase()) {
|
||||||
|
if (!state.selectedOptions.contains(option)) {
|
||||||
|
matchingOption = option;
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
optionExistsButSelected = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if there isn't a matching option at all, then create it
|
||||||
|
if (matchingOption == null && !optionExistsButSelected) {
|
||||||
|
_createOption(optionName);
|
||||||
|
}
|
||||||
|
|
||||||
|
// if there is an unselected matching option, select it
|
||||||
|
if (matchingOption != null) {
|
||||||
|
_selectOptionService.select(optionId: matchingOption.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// clear the filter
|
||||||
|
emit(state.copyWith(filter: none()));
|
||||||
|
}
|
||||||
|
|
||||||
void _filterOption(String optionName, Emitter<SelectOptionEditorState> emit) {
|
void _filterOption(String optionName, Emitter<SelectOptionEditorState> emit) {
|
||||||
final _MakeOptionResult result =
|
final _MakeOptionResult result =
|
||||||
_makeOptions(Some(optionName), state.allOptions);
|
_makeOptions(Some(optionName), state.allOptions);
|
||||||
@ -187,6 +220,8 @@ class SelectOptionEditorEvent with _$SelectOptionEditorEvent {
|
|||||||
_DeleteOption;
|
_DeleteOption;
|
||||||
const factory SelectOptionEditorEvent.filterOption(String optionName) =
|
const factory SelectOptionEditorEvent.filterOption(String optionName) =
|
||||||
_SelectOptionFilter;
|
_SelectOptionFilter;
|
||||||
|
const factory SelectOptionEditorEvent.trySelectOption(String optionName) =
|
||||||
|
_TrySelectOption;
|
||||||
}
|
}
|
||||||
|
|
||||||
@freezed
|
@freezed
|
||||||
|
@ -11,7 +11,10 @@ class NumberFormatBloc extends Bloc<NumberFormatEvent, NumberFormatState> {
|
|||||||
event.map(setFilter: (_SetFilter value) {
|
event.map(setFilter: (_SetFilter value) {
|
||||||
final List<NumberFormat> formats = List.from(NumberFormat.values);
|
final List<NumberFormat> formats = List.from(NumberFormat.values);
|
||||||
if (value.filter.isNotEmpty) {
|
if (value.filter.isNotEmpty) {
|
||||||
formats.retainWhere((element) => element.title().toLowerCase().contains(value.filter.toLowerCase()));
|
formats.retainWhere((element) => element
|
||||||
|
.title()
|
||||||
|
.toLowerCase()
|
||||||
|
.contains(value.filter.toLowerCase()));
|
||||||
}
|
}
|
||||||
emit(state.copyWith(formats: formats, filter: value.filter));
|
emit(state.copyWith(formats: formats, filter: value.filter));
|
||||||
});
|
});
|
||||||
@ -91,7 +94,7 @@ extension NumberFormatExtension on NumberFormat {
|
|||||||
case NumberFormat.Percent:
|
case NumberFormat.Percent:
|
||||||
return "Percent";
|
return "Percent";
|
||||||
case NumberFormat.PhilippinePeso:
|
case NumberFormat.PhilippinePeso:
|
||||||
return "Percent";
|
return "PhilippinePeso";
|
||||||
case NumberFormat.Pound:
|
case NumberFormat.Pound:
|
||||||
return "Pound";
|
return "Pound";
|
||||||
case NumberFormat.Rand:
|
case NumberFormat.Rand:
|
||||||
|
@ -70,9 +70,11 @@ class GridPluginDisplay extends PluginDisplay {
|
|||||||
@override
|
@override
|
||||||
Widget buildWidget(PluginContext context) {
|
Widget buildWidget(PluginContext context) {
|
||||||
notifier.isDeleted.addListener(() {
|
notifier.isDeleted.addListener(() {
|
||||||
if (notifier.isDeleted.value) {
|
notifier.isDeleted.value.fold(() => null, (deletedView) {
|
||||||
context.onDeleted(view);
|
if (deletedView.hasIndex()) {
|
||||||
}
|
context.onDeleted(view, deletedView.index);
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return GridPage(key: ValueKey(view.id), view: view);
|
return GridPage(key: ValueKey(view.id), view: view);
|
||||||
|
@ -154,10 +154,10 @@ class _TextField extends StatelessWidget {
|
|||||||
.read<SelectOptionCellEditorBloc>()
|
.read<SelectOptionCellEditorBloc>()
|
||||||
.add(SelectOptionEditorEvent.filterOption(text));
|
.add(SelectOptionEditorEvent.filterOption(text));
|
||||||
},
|
},
|
||||||
onNewTag: (tagName) {
|
onSubmitted: (tagName) {
|
||||||
context
|
context
|
||||||
.read<SelectOptionCellEditorBloc>()
|
.read<SelectOptionCellEditorBloc>()
|
||||||
.add(SelectOptionEditorEvent.newOption(tagName));
|
.add(SelectOptionEditorEvent.trySelectOption(tagName));
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -17,7 +17,7 @@ class SelectOptionTextField extends StatefulWidget {
|
|||||||
final LinkedHashMap<String, SelectOptionPB> selectedOptionMap;
|
final LinkedHashMap<String, SelectOptionPB> selectedOptionMap;
|
||||||
final double distanceToText;
|
final double distanceToText;
|
||||||
|
|
||||||
final Function(String) onNewTag;
|
final Function(String) onSubmitted;
|
||||||
final Function(String) newText;
|
final Function(String) newText;
|
||||||
final VoidCallback? onClick;
|
final VoidCallback? onClick;
|
||||||
|
|
||||||
@ -26,7 +26,7 @@ class SelectOptionTextField extends StatefulWidget {
|
|||||||
required this.selectedOptionMap,
|
required this.selectedOptionMap,
|
||||||
required this.distanceToText,
|
required this.distanceToText,
|
||||||
required this.tagController,
|
required this.tagController,
|
||||||
required this.onNewTag,
|
required this.onSubmitted,
|
||||||
required this.newText,
|
required this.newText,
|
||||||
this.onClick,
|
this.onClick,
|
||||||
TextEditingController? textController,
|
TextEditingController? textController,
|
||||||
@ -88,7 +88,7 @@ class _SelectOptionTextFieldState extends State<SelectOptionTextField> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (text.isNotEmpty) {
|
if (text.isNotEmpty) {
|
||||||
widget.onNewTag(text);
|
widget.onSubmitted(text);
|
||||||
focusNode.requestFocus();
|
focusNode.requestFocus();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -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),
|
||||||
|
@ -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),
|
||||||
|
@ -1,15 +1,16 @@
|
|||||||
import 'package:app_flowy/startup/plugin/plugin.dart';
|
import 'package:app_flowy/startup/plugin/plugin.dart';
|
||||||
import 'package:app_flowy/workspace/application/view/view_listener.dart';
|
import 'package:app_flowy/workspace/application/view/view_listener.dart';
|
||||||
|
import 'package:dartz/dartz.dart';
|
||||||
import 'package:flowy_sdk/log.dart';
|
import 'package:flowy_sdk/log.dart';
|
||||||
import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart';
|
import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class ViewPluginNotifier extends PluginNotifier {
|
class ViewPluginNotifier extends PluginNotifier<Option<DeletedViewPB>> {
|
||||||
final ViewListener? _viewListener;
|
final ViewListener? _viewListener;
|
||||||
ViewPB view;
|
ViewPB view;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
final ValueNotifier<bool> isDeleted = ValueNotifier(false);
|
final ValueNotifier<Option<DeletedViewPB>> isDeleted = ValueNotifier(none());
|
||||||
|
|
||||||
@override
|
@override
|
||||||
final ValueNotifier<int> isDisplayChanged = ValueNotifier(0);
|
final ValueNotifier<int> isDisplayChanged = ValueNotifier(0);
|
||||||
@ -27,9 +28,7 @@ class ViewPluginNotifier extends PluginNotifier {
|
|||||||
);
|
);
|
||||||
}, onViewMoveToTrash: (result) {
|
}, onViewMoveToTrash: (result) {
|
||||||
result.fold(
|
result.fold(
|
||||||
(deletedView) {
|
(deletedView) => isDeleted.value = some(deletedView),
|
||||||
isDeleted.value = true;
|
|
||||||
},
|
|
||||||
(err) => Log.error(err),
|
(err) => Log.error(err),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -32,9 +32,9 @@ abstract class Plugin<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class PluginNotifier {
|
abstract class PluginNotifier<T> {
|
||||||
/// Notify if the plugin get deleted
|
/// Notify if the plugin get deleted
|
||||||
ValueNotifier<bool> get isDeleted;
|
ValueNotifier<T> get isDeleted;
|
||||||
|
|
||||||
/// Notify if the [PluginDisplay]'s content was changed
|
/// Notify if the [PluginDisplay]'s content was changed
|
||||||
ValueNotifier<int> get isDisplayChanged;
|
ValueNotifier<int> get isDisplayChanged;
|
||||||
@ -67,7 +67,7 @@ abstract class PluginDisplay with NavigationItem {
|
|||||||
|
|
||||||
class PluginContext {
|
class PluginContext {
|
||||||
// calls when widget of the plugin get deleted
|
// calls when widget of the plugin get deleted
|
||||||
final Function(ViewPB) onDeleted;
|
final Function(ViewPB, int?) onDeleted;
|
||||||
|
|
||||||
PluginContext({required this.onDeleted});
|
PluginContext({required this.onDeleted});
|
||||||
}
|
}
|
||||||
|
@ -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];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,7 +16,7 @@ typedef UpdateViewNotifiedValue = Either<ViewPB, FlowyError>;
|
|||||||
// Restore the view from trash
|
// Restore the view from trash
|
||||||
typedef RestoreViewNotifiedValue = Either<ViewPB, FlowyError>;
|
typedef RestoreViewNotifiedValue = Either<ViewPB, FlowyError>;
|
||||||
// Move the view to trash
|
// Move the view to trash
|
||||||
typedef MoveToTrashNotifiedValue = Either<ViewIdPB, FlowyError>;
|
typedef MoveToTrashNotifiedValue = Either<DeletedViewPB, FlowyError>;
|
||||||
|
|
||||||
class ViewListener {
|
class ViewListener {
|
||||||
StreamSubscription<SubscribeObject>? _subscription;
|
StreamSubscription<SubscribeObject>? _subscription;
|
||||||
@ -98,8 +98,8 @@ class ViewListener {
|
|||||||
break;
|
break;
|
||||||
case FolderNotification.ViewMoveToTrash:
|
case FolderNotification.ViewMoveToTrash:
|
||||||
result.fold(
|
result.fold(
|
||||||
(payload) =>
|
(payload) => _moveToTrashNotifier.value =
|
||||||
_moveToTrashNotifier.value = left(ViewIdPB.fromBuffer(payload)),
|
left(DeletedViewPB.fromBuffer(payload)),
|
||||||
(error) => _moveToTrashNotifier.value = right(error),
|
(error) => _moveToTrashNotifier.value = right(error),
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
@ -34,19 +34,6 @@ class HomeScreen extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _HomeScreenState extends State<HomeScreen> {
|
class _HomeScreenState extends State<HomeScreen> {
|
||||||
ViewPB? initialView;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void didUpdateWidget(covariant HomeScreen oldWidget) {
|
|
||||||
initialView = null;
|
|
||||||
super.didUpdateWidget(oldWidget);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return MultiBlocProvider(
|
return MultiBlocProvider(
|
||||||
@ -129,26 +116,29 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
required BuildContext context,
|
required BuildContext context,
|
||||||
required HomeState state}) {
|
required HomeState state}) {
|
||||||
final workspaceSetting = state.workspaceSetting;
|
final workspaceSetting = state.workspaceSetting;
|
||||||
if (initialView == null && workspaceSetting.hasLatestView()) {
|
|
||||||
initialView = workspaceSetting.latestView;
|
|
||||||
final plugin = makePlugin(
|
|
||||||
pluginType: initialView!.pluginType,
|
|
||||||
data: initialView,
|
|
||||||
);
|
|
||||||
getIt<HomeStackManager>().setPlugin(plugin);
|
|
||||||
}
|
|
||||||
|
|
||||||
final homeMenu = HomeMenu(
|
final homeMenu = HomeMenu(
|
||||||
user: widget.user,
|
user: widget.user,
|
||||||
workspaceSetting: workspaceSetting,
|
workspaceSetting: workspaceSetting,
|
||||||
collapsedNotifier: getIt<HomeStackManager>().collapsedNotifier,
|
collapsedNotifier: getIt<HomeStackManager>().collapsedNotifier,
|
||||||
);
|
);
|
||||||
|
|
||||||
final latestView =
|
// Only open the last opened view if the [HomeStackManager] current opened
|
||||||
workspaceSetting.hasLatestView() ? workspaceSetting.latestView : null;
|
// plugin is blank and the last opened view is not null.
|
||||||
if (getIt<MenuSharedState>().latestOpenView == null) {
|
//
|
||||||
/// AppFlowy will open the view that the last time the user opened it. The _buildHomeMenu will get called when AppFlowy's screen resizes. So we only set the latestOpenView when it's null.
|
// All opened widgets that display on the home screen are in the form
|
||||||
getIt<MenuSharedState>().latestOpenView = latestView;
|
// of plugins. There is a list of built-in plugins defined in the
|
||||||
|
// [PluginType] enum, including board, grid and trash.
|
||||||
|
if (getIt<HomeStackManager>().plugin.ty == PluginType.blank) {
|
||||||
|
// Open the last opened view.
|
||||||
|
if (workspaceSetting.hasLatestView()) {
|
||||||
|
final view = workspaceSetting.latestView;
|
||||||
|
final plugin = makePlugin(
|
||||||
|
pluginType: view.pluginType,
|
||||||
|
data: view,
|
||||||
|
);
|
||||||
|
getIt<HomeStackManager>().setPlugin(plugin);
|
||||||
|
getIt<MenuSharedState>().latestOpenView = view;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return FocusTraversalGroup(child: RepaintBoundary(child: homeMenu));
|
return FocusTraversalGroup(child: RepaintBoundary(child: homeMenu));
|
||||||
@ -261,14 +251,18 @@ class HomeScreenStackAdaptor extends HomeStackDelegate {
|
|||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void didDeleteStackWidget(ViewPB view) {
|
void didDeleteStackWidget(ViewPB view, int? index) {
|
||||||
final homeService = HomeService();
|
final homeService = HomeService();
|
||||||
homeService.readApp(appId: view.appId).then((result) {
|
homeService.readApp(appId: view.appId).then((result) {
|
||||||
result.fold(
|
result.fold(
|
||||||
(appPB) {
|
(appPB) {
|
||||||
final List<ViewPB> views = appPB.belongings.items;
|
final List<ViewPB> views = appPB.belongings.items;
|
||||||
if (views.isNotEmpty) {
|
if (views.isNotEmpty) {
|
||||||
final lastView = views.last;
|
var lastView = views.last;
|
||||||
|
if (index != null && index != 0 && views.length > index - 1) {
|
||||||
|
lastView = views[index - 1];
|
||||||
|
}
|
||||||
|
|
||||||
final plugin = makePlugin(
|
final plugin = makePlugin(
|
||||||
pluginType: lastView.pluginType,
|
pluginType: lastView.pluginType,
|
||||||
data: lastView,
|
data: lastView,
|
||||||
|
@ -18,7 +18,7 @@ import 'home_layout.dart';
|
|||||||
typedef NavigationCallback = void Function(String id);
|
typedef NavigationCallback = void Function(String id);
|
||||||
|
|
||||||
abstract class HomeStackDelegate {
|
abstract class HomeStackDelegate {
|
||||||
void didDeleteStackWidget(ViewPB view);
|
void didDeleteStackWidget(ViewPB view, int? index);
|
||||||
}
|
}
|
||||||
|
|
||||||
class HomeStack extends StatelessWidget {
|
class HomeStack extends StatelessWidget {
|
||||||
@ -41,9 +41,11 @@ class HomeStack extends StatelessWidget {
|
|||||||
child: Container(
|
child: Container(
|
||||||
color: theme.surface,
|
color: theme.surface,
|
||||||
child: FocusTraversalGroup(
|
child: FocusTraversalGroup(
|
||||||
child: getIt<HomeStackManager>().stackWidget(onDeleted: (view) {
|
child: getIt<HomeStackManager>().stackWidget(
|
||||||
delegate.didDeleteStackWidget(view);
|
onDeleted: (view, index) {
|
||||||
}),
|
delegate.didDeleteStackWidget(view, index);
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -144,6 +146,7 @@ class HomeStackManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
PublishNotifier<bool> get collapsedNotifier => _notifier.collapsedNotifier;
|
PublishNotifier<bool> get collapsedNotifier => _notifier.collapsedNotifier;
|
||||||
|
Plugin get plugin => _notifier.plugin;
|
||||||
|
|
||||||
void setPlugin(Plugin newPlugin) {
|
void setPlugin(Plugin newPlugin) {
|
||||||
_notifier.plugin = newPlugin;
|
_notifier.plugin = newPlugin;
|
||||||
@ -167,7 +170,7 @@ class HomeStackManager {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget stackWidget({required Function(ViewPB) onDeleted}) {
|
Widget stackWidget({required Function(ViewPB, int?) onDeleted}) {
|
||||||
return MultiProvider(
|
return MultiProvider(
|
||||||
providers: [ChangeNotifierProvider.value(value: _notifier)],
|
providers: [ChangeNotifierProvider.value(value: _notifier)],
|
||||||
child: Consumer(builder: (ctx, HomeStackNotifier notifier, child) {
|
child: Consumer(builder: (ctx, HomeStackNotifier notifier, child) {
|
||||||
|
@ -61,13 +61,20 @@ class ActionList {
|
|||||||
itemBuilder: (context, index) => items[index],
|
itemBuilder: (context, index) => items[index],
|
||||||
anchorContext: anchorContext,
|
anchorContext: anchorContext,
|
||||||
anchorDirection: AnchorDirection.bottomRight,
|
anchorDirection: AnchorDirection.bottomRight,
|
||||||
width: 120,
|
constraints: BoxConstraints(
|
||||||
height: 80,
|
minWidth: 120,
|
||||||
|
maxWidth: 280,
|
||||||
|
minHeight: items.length * (CreateItem.height),
|
||||||
|
maxHeight: items.length * (CreateItem.height),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class CreateItem extends StatelessWidget {
|
class CreateItem extends StatelessWidget {
|
||||||
|
static const double height = 30;
|
||||||
|
static const double verticalPadding = 6;
|
||||||
|
|
||||||
final PluginBuilder pluginBuilder;
|
final PluginBuilder pluginBuilder;
|
||||||
final Function(PluginBuilder) onSelected;
|
final Function(PluginBuilder) onSelected;
|
||||||
const CreateItem({
|
const CreateItem({
|
||||||
@ -86,11 +93,20 @@ class CreateItem extends StatelessWidget {
|
|||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
behavior: HitTestBehavior.opaque,
|
behavior: HitTestBehavior.opaque,
|
||||||
onTap: () => onSelected(pluginBuilder),
|
onTap: () => onSelected(pluginBuilder),
|
||||||
child: FlowyText.medium(
|
child: ConstrainedBox(
|
||||||
pluginBuilder.menuName,
|
constraints: const BoxConstraints(
|
||||||
color: theme.textColor,
|
minWidth: 120,
|
||||||
fontSize: 12,
|
minHeight: CreateItem.height,
|
||||||
).padding(horizontal: 10, vertical: 6),
|
),
|
||||||
|
child: Align(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: FlowyText.medium(
|
||||||
|
pluginBuilder.menuName,
|
||||||
|
color: theme.textColor,
|
||||||
|
fontSize: 12,
|
||||||
|
).padding(horizontal: 10),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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),
|
||||||
|
@ -53,7 +53,7 @@ class ViewSectionItem extends StatelessWidget {
|
|||||||
_handleAction(context, action);
|
_handleAction(context, action);
|
||||||
},
|
},
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
padding: const EdgeInsets.symmetric(vertical: 2),
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: () => onSelected(context.read<ViewBloc>().state.view),
|
onTap: () => onSelected(context.read<ViewBloc>().state.view),
|
||||||
child: FlowyHover(
|
child: FlowyHover(
|
||||||
@ -73,13 +73,18 @@ class ViewSectionItem extends StatelessWidget {
|
|||||||
BuildContext context, bool onHover, ViewState state, Color iconColor) {
|
BuildContext context, bool onHover, ViewState state, Color iconColor) {
|
||||||
List<Widget> children = [
|
List<Widget> children = [
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: 16,
|
width: 16,
|
||||||
height: 16,
|
height: 16,
|
||||||
child: state.view.renderThumbnail(iconColor: iconColor)),
|
child: state.view.renderThumbnail(iconColor: iconColor),
|
||||||
|
),
|
||||||
const HSpace(2),
|
const HSpace(2),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: FlowyText.regular(state.view.name,
|
child: FlowyText.regular(
|
||||||
fontSize: 12, overflow: TextOverflow.clip)),
|
state.view.name,
|
||||||
|
fontSize: 12,
|
||||||
|
overflow: TextOverflow.clip,
|
||||||
|
),
|
||||||
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
if (onHover || state.isEditing) {
|
if (onHover || state.isEditing) {
|
||||||
|
@ -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(),
|
||||||
|
@ -13,7 +13,7 @@ class SettingsAppearanceView extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = context.watch<AppTheme>();
|
final theme = context.read<AppTheme>();
|
||||||
|
|
||||||
return SingleChildScrollView(
|
return SingleChildScrollView(
|
||||||
child: Column(
|
child: Column(
|
||||||
@ -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(
|
||||||
|
@ -111,9 +111,6 @@ class QuestionBubbleActionSheet
|
|||||||
required this.onSelected,
|
required this.onSelected,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
|
||||||
double get maxWidth => 170;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
double get itemHeight => 22;
|
double get itemHeight => 22;
|
||||||
|
|
||||||
@ -142,7 +139,7 @@ class QuestionBubbleActionSheet
|
|||||||
@override
|
@override
|
||||||
ListOverlayFooter? get footer => ListOverlayFooter(
|
ListOverlayFooter? get footer => ListOverlayFooter(
|
||||||
widget: const FlowyVersionDescription(),
|
widget: const FlowyVersionDescription(),
|
||||||
height: 30,
|
height: 40,
|
||||||
padding: const EdgeInsets.only(top: 6),
|
padding: const EdgeInsets.only(top: 6),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -174,11 +171,14 @@ class FlowyVersionDescription extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
Divider(height: 1, color: theme.shader6, thickness: 1.0),
|
Divider(height: 1, color: theme.shader6, thickness: 1.0),
|
||||||
const VSpace(6),
|
const VSpace(6),
|
||||||
FlowyText("$appName $version.$buildNumber",
|
FlowyText(
|
||||||
fontSize: 12, color: theme.shader4),
|
"$appName $version.$buildNumber",
|
||||||
|
fontSize: 12,
|
||||||
|
color: theme.shader4,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
).padding(
|
).padding(
|
||||||
horizontal: ActionListSizes.itemHPadding + ActionListSizes.padding,
|
horizontal: ActionListSizes.itemHPadding + ActionListSizes.hPadding,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return const CircularProgressIndicator();
|
return const CircularProgressIndicator();
|
||||||
|
@ -13,7 +13,9 @@ abstract class ActionList<T extends ActionItem> {
|
|||||||
|
|
||||||
String get identifier => toString();
|
String get identifier => toString();
|
||||||
|
|
||||||
double get maxWidth => 162;
|
double get maxWidth => 300;
|
||||||
|
|
||||||
|
double get minWidth => 120;
|
||||||
|
|
||||||
double get itemHeight => ActionListSizes.itemHeight;
|
double get itemHeight => ActionListSizes.itemHeight;
|
||||||
|
|
||||||
@ -29,28 +31,29 @@ abstract class ActionList<T extends ActionItem> {
|
|||||||
AnchorDirection anchorDirection = AnchorDirection.bottomRight,
|
AnchorDirection anchorDirection = AnchorDirection.bottomRight,
|
||||||
Offset? anchorOffset,
|
Offset? anchorOffset,
|
||||||
}) {
|
}) {
|
||||||
final widgets = items
|
|
||||||
.map(
|
|
||||||
(action) => ActionCell<T>(
|
|
||||||
action: action,
|
|
||||||
itemHeight: itemHeight,
|
|
||||||
onSelected: (action) {
|
|
||||||
FlowyOverlay.of(buildContext).remove(identifier);
|
|
||||||
selectCallback(dartz.some(action));
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
ListOverlay.showWithAnchor(
|
ListOverlay.showWithAnchor(
|
||||||
buildContext,
|
buildContext,
|
||||||
identifier: identifier,
|
identifier: identifier,
|
||||||
itemCount: widgets.length,
|
itemCount: items.length,
|
||||||
itemBuilder: (context, index) => widgets[index],
|
itemBuilder: (context, index) {
|
||||||
|
final action = items[index];
|
||||||
|
return ActionCell<T>(
|
||||||
|
action: action,
|
||||||
|
itemHeight: itemHeight,
|
||||||
|
onSelected: (action) {
|
||||||
|
FlowyOverlay.of(buildContext).remove(identifier);
|
||||||
|
selectCallback(dartz.some(action));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
anchorContext: anchorContext ?? buildContext,
|
anchorContext: anchorContext ?? buildContext,
|
||||||
anchorDirection: anchorDirection,
|
anchorDirection: anchorDirection,
|
||||||
width: maxWidth,
|
constraints: BoxConstraints(
|
||||||
height: widgets.length * (itemHeight + ActionListSizes.padding * 2),
|
minHeight: items.length * (itemHeight + ActionListSizes.vPadding * 2),
|
||||||
|
maxHeight: items.length * (itemHeight + ActionListSizes.vPadding * 2),
|
||||||
|
maxWidth: maxWidth,
|
||||||
|
minWidth: minWidth,
|
||||||
|
),
|
||||||
delegate: delegate,
|
delegate: delegate,
|
||||||
anchorOffset: anchorOffset,
|
anchorOffset: anchorOffset,
|
||||||
footer: footer,
|
footer: footer,
|
||||||
@ -66,7 +69,8 @@ abstract class ActionItem {
|
|||||||
class ActionListSizes {
|
class ActionListSizes {
|
||||||
static double itemHPadding = 10;
|
static double itemHPadding = 10;
|
||||||
static double itemHeight = 20;
|
static double itemHeight = 20;
|
||||||
static double padding = 6;
|
static double vPadding = 6;
|
||||||
|
static double hPadding = 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
class ActionCell<T extends ActionItem> extends StatelessWidget {
|
class ActionCell<T extends ActionItem> extends StatelessWidget {
|
||||||
@ -93,7 +97,7 @@ class ActionCell<T extends ActionItem> extends StatelessWidget {
|
|||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
height: itemHeight,
|
height: itemHeight,
|
||||||
child: Row(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
if (icon != null) icon,
|
if (icon != null) icon,
|
||||||
HSpace(ActionListSizes.itemHPadding),
|
HSpace(ActionListSizes.itemHPadding),
|
||||||
@ -101,8 +105,8 @@ class ActionCell<T extends ActionItem> extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
).padding(
|
).padding(
|
||||||
horizontal: ActionListSizes.padding,
|
horizontal: ActionListSizes.hPadding,
|
||||||
vertical: ActionListSizes.padding,
|
vertical: ActionListSizes.vPadding,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -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) {
|
||||||
|
@ -8,25 +8,28 @@ class Log {
|
|||||||
|
|
||||||
static void info(String? message) {
|
static void info(String? message) {
|
||||||
if (enableLog) {
|
if (enableLog) {
|
||||||
debugPrint('ℹ️[Info]=> $message');
|
debugPrint('AppFlowyBoard: ℹ️[Info]=> $message');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void debug(String? message) {
|
static void debug(String? message) {
|
||||||
if (enableLog) {
|
if (enableLog) {
|
||||||
debugPrint('🐛[Debug] - ${DateTime.now().second}=> $message');
|
debugPrint(
|
||||||
|
'AppFlowyBoard: 🐛[Debug] - ${DateTime.now().second}=> $message');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void warn(String? message) {
|
static void warn(String? message) {
|
||||||
if (enableLog) {
|
if (enableLog) {
|
||||||
debugPrint('🐛[Warn] - ${DateTime.now().second} => $message');
|
debugPrint(
|
||||||
|
'AppFlowyBoard: 🐛[Warn] - ${DateTime.now().second} => $message');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void trace(String? message) {
|
static void trace(String? message) {
|
||||||
if (enableLog) {
|
if (enableLog) {
|
||||||
debugPrint('❗️[Trace] - ${DateTime.now().second}=> $message');
|
debugPrint(
|
||||||
|
'AppFlowyBoard: ❗️[Trace] - ${DateTime.now().second}=> $message');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 {}
|
||||||
|
@ -217,15 +217,15 @@ class AppFlowyBoardController extends ChangeNotifier
|
|||||||
final fromGroupItem = fromGroupController.removeAt(fromGroupIndex);
|
final fromGroupItem = fromGroupController.removeAt(fromGroupIndex);
|
||||||
if (toGroupController.items.length > toGroupIndex) {
|
if (toGroupController.items.length > toGroupIndex) {
|
||||||
assert(toGroupController.items[toGroupIndex] is PhantomGroupItem);
|
assert(toGroupController.items[toGroupIndex] is PhantomGroupItem);
|
||||||
}
|
|
||||||
|
|
||||||
toGroupController.replace(toGroupIndex, fromGroupItem);
|
toGroupController.replace(toGroupIndex, fromGroupItem);
|
||||||
onMoveGroupItemToGroup?.call(
|
onMoveGroupItemToGroup?.call(
|
||||||
fromGroupId,
|
fromGroupId,
|
||||||
fromGroupIndex,
|
fromGroupIndex,
|
||||||
toGroupId,
|
toGroupId,
|
||||||
toGroupIndex,
|
toGroupIndex,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -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,
|
||||||
@ -112,7 +112,7 @@ class AppFlowyBoardGroup extends StatefulWidget {
|
|||||||
this.itemMargin = EdgeInsets.zero,
|
this.itemMargin = EdgeInsets.zero,
|
||||||
this.cornerRadius = 0.0,
|
this.cornerRadius = 0.0,
|
||||||
this.backgroundColor = Colors.transparent,
|
this.backgroundColor = Colors.transparent,
|
||||||
}) : config = const ReorderFlexConfig(setStateWhenEndDrag: false),
|
}) : config = const ReorderFlexConfig(),
|
||||||
super(key: key);
|
super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -124,6 +124,8 @@ class AppFlowyGroupController extends ChangeNotifier with EquatableMixin {
|
|||||||
Log.debug('[$AppFlowyGroupController] $groupData add $newItem');
|
Log.debug('[$AppFlowyGroupController] $groupData add $newItem');
|
||||||
} else {
|
} else {
|
||||||
if (index >= groupData._items.length) {
|
if (index >= groupData._items.length) {
|
||||||
|
Log.warn(
|
||||||
|
'[$AppFlowyGroupController] unexpected items length, index should less than the count of the items. Index: $index, items count: ${items.length}');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -133,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) {
|
||||||
@ -141,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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -152,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.
|
||||||
|
@ -22,6 +22,8 @@ class FlexDragTargetData extends DragTargetData {
|
|||||||
|
|
||||||
Size? get feedbackSize => _state.feedbackSize;
|
Size? get feedbackSize => _state.feedbackSize;
|
||||||
|
|
||||||
|
bool get isDragging => _state.isDragging();
|
||||||
|
|
||||||
final String dragTargetId;
|
final String dragTargetId;
|
||||||
|
|
||||||
Offset dragTargetOffset = Offset.zero;
|
Offset dragTargetOffset = Offset.zero;
|
||||||
@ -48,47 +50,28 @@ class FlexDragTargetData extends DragTargetData {
|
|||||||
|
|
||||||
bool isOverlapWithWidgets(List<GlobalObjectKey> widgetKeys) {
|
bool isOverlapWithWidgets(List<GlobalObjectKey> widgetKeys) {
|
||||||
final renderBox = dragTargetIndexKey.currentContext?.findRenderObject();
|
final renderBox = dragTargetIndexKey.currentContext?.findRenderObject();
|
||||||
|
|
||||||
if (renderBox == null) return false;
|
if (renderBox == null) return false;
|
||||||
if (renderBox is! RenderBox) return false;
|
if (renderBox is! RenderBox) return false;
|
||||||
final size = feedbackSize ?? Size.zero;
|
final size = feedbackSize ?? Size.zero;
|
||||||
final Rect rect = dragTargetOffset & size;
|
|
||||||
|
|
||||||
|
final Rect dragTargetRect = renderBox.localToGlobal(Offset.zero) & size;
|
||||||
for (final widgetKey in widgetKeys) {
|
for (final widgetKey in widgetKeys) {
|
||||||
final renderObject = widgetKey.currentContext?.findRenderObject();
|
final renderObject = widgetKey.currentContext?.findRenderObject();
|
||||||
if (renderObject != null && renderObject is RenderBox) {
|
if (renderObject != null && renderObject is RenderBox) {
|
||||||
Rect widgetRect =
|
Rect widgetRect =
|
||||||
renderObject.localToGlobal(Offset.zero) & renderObject.size;
|
renderObject.localToGlobal(Offset.zero) & renderObject.size;
|
||||||
// return rect.overlaps(widgetRect);
|
return dragTargetRect.overlaps(widgetRect);
|
||||||
if (rect.right <= widgetRect.left || widgetRect.right <= rect.left) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rect.bottom <= widgetRect.top || widgetRect.bottom <= rect.top) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// final HitTestResult result = HitTestResult();
|
|
||||||
// WidgetsBinding.instance.hitTest(result, position);
|
|
||||||
// for (final HitTestEntry entry in result.path) {
|
|
||||||
// final HitTestTarget target = entry.target;
|
|
||||||
// if (target is RenderMetaData) {
|
|
||||||
// print(target.metaData);
|
|
||||||
// }
|
|
||||||
// print(target);
|
|
||||||
// }
|
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
||||||
@ -113,7 +96,7 @@ class DraggingState {
|
|||||||
int currentIndex = -1;
|
int currentIndex = -1;
|
||||||
|
|
||||||
/// The widget to move the dragging widget too after the current index.
|
/// The widget to move the dragging widget too after the current index.
|
||||||
int nextIndex = 0;
|
int nextIndex = -1;
|
||||||
|
|
||||||
/// Whether or not we are currently scrolling this view to show a widget.
|
/// Whether or not we are currently scrolling this view to show a widget.
|
||||||
bool scrolling = false;
|
bool scrolling = false;
|
||||||
@ -149,6 +132,7 @@ class DraggingState {
|
|||||||
dragStartIndex = -1;
|
dragStartIndex = -1;
|
||||||
phantomIndex = -1;
|
phantomIndex = -1;
|
||||||
currentIndex = -1;
|
currentIndex = -1;
|
||||||
|
nextIndex = -1;
|
||||||
_draggingWidget = null;
|
_draggingWidget = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import 'package:appflowy_board/src/utils/log.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/scheduler.dart';
|
import 'package:flutter/scheduler.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
@ -72,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,
|
||||||
@ -93,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
|
||||||
@ -163,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: () {
|
||||||
@ -184,8 +191,9 @@ class _ReorderDragTargetState<T extends DragTargetData>
|
|||||||
/// When the drag does not end inside a DragTarget widget, the
|
/// When the drag does not end inside a DragTarget widget, the
|
||||||
/// drag fails, but we still reorder the widget to the last position it
|
/// drag fails, but we still reorder the widget to the last position it
|
||||||
/// had been dragged to.
|
/// had been dragged to.
|
||||||
onDraggableCanceled: (Velocity velocity, Offset offset) =>
|
onDraggableCanceled: (Velocity velocity, Offset offset) {
|
||||||
widget.onDragEnded(widget.dragTargetData),
|
widget.onDragEnded(widget.dragTargetData);
|
||||||
|
},
|
||||||
child: widget.child,
|
child: widget.child,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -193,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,
|
||||||
@ -203,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),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -221,8 +232,11 @@ class DragTargetAnimation {
|
|||||||
// where the widget used to be.
|
// where the widget used to be.
|
||||||
late AnimationController phantomController;
|
late AnimationController phantomController;
|
||||||
|
|
||||||
|
// Uses to simulate the insert animation when card was moved from on group to
|
||||||
|
// another group. Check out the [FakeDragTarget].
|
||||||
late AnimationController insertController;
|
late AnimationController insertController;
|
||||||
|
|
||||||
|
// Used to remove the phantom
|
||||||
late AnimationController deleteController;
|
late AnimationController deleteController;
|
||||||
|
|
||||||
DragTargetAnimation({
|
DragTargetAnimation({
|
||||||
@ -238,7 +252,7 @@ class DragTargetAnimation {
|
|||||||
value: 0, vsync: vsync, duration: reorderAnimationDuration);
|
value: 0, vsync: vsync, duration: reorderAnimationDuration);
|
||||||
|
|
||||||
insertController = AnimationController(
|
insertController = AnimationController(
|
||||||
value: 0.0, vsync: vsync, duration: const Duration(milliseconds: 200));
|
value: 0.0, vsync: vsync, duration: const Duration(milliseconds: 100));
|
||||||
|
|
||||||
deleteController = AnimationController(
|
deleteController = AnimationController(
|
||||||
value: 0.0, vsync: vsync, duration: const Duration(milliseconds: 1));
|
value: 0.0, vsync: vsync, duration: const Duration(milliseconds: 1));
|
||||||
@ -269,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);
|
||||||
@ -281,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,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -295,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);
|
||||||
@ -307,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,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -423,7 +440,6 @@ abstract class FakeDragTargetEventData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class FakeDragTarget<T extends DragTargetData> extends StatefulWidget {
|
class FakeDragTarget<T extends DragTargetData> extends StatefulWidget {
|
||||||
final Duration animationDuration;
|
|
||||||
final FakeDragTargetEventTrigger eventTrigger;
|
final FakeDragTargetEventTrigger eventTrigger;
|
||||||
final FakeDragTargetEventData eventData;
|
final FakeDragTargetEventData eventData;
|
||||||
final DragTargetOnStarted onDragStarted;
|
final DragTargetOnStarted onDragStarted;
|
||||||
@ -442,7 +458,6 @@ class FakeDragTarget<T extends DragTargetData> extends StatefulWidget {
|
|||||||
required this.insertAnimationController,
|
required this.insertAnimationController,
|
||||||
required this.deleteAnimationController,
|
required this.deleteAnimationController,
|
||||||
required this.child,
|
required this.child,
|
||||||
this.animationDuration = const Duration(milliseconds: 250),
|
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -468,6 +483,7 @@ class _FakeDragTargetState<T extends DragTargetData>
|
|||||||
// });
|
// });
|
||||||
|
|
||||||
widget.eventTrigger.fakeOnDragEnded(() {
|
widget.eventTrigger.fakeOnDragEnded(() {
|
||||||
|
Log.trace("[$FakeDragTarget] on drag end");
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
widget.onDragEnded(widget.eventData.dragTargetData as T);
|
widget.onDragEnded(widget.eventData.dragTargetData as T);
|
||||||
});
|
});
|
||||||
@ -476,6 +492,13 @@ class _FakeDragTargetState<T extends DragTargetData>
|
|||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
widget.insertAnimationController
|
||||||
|
.removeStatusListener(_onInsertedAnimationStatusChanged);
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (simulateDragging) {
|
if (simulateDragging) {
|
||||||
@ -483,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,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -492,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,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -503,14 +528,18 @@ class _FakeDragTargetState<T extends DragTargetData>
|
|||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() {
|
setState(() {
|
||||||
simulateDragging = true;
|
if (widget.onWillAccept(widget.eventData.dragTargetData as T)) {
|
||||||
widget.deleteAnimationController.reverse(from: 1.0);
|
Log.trace("[$FakeDragTarget] on drag start");
|
||||||
widget.onWillAccept(widget.eventData.dragTargetData as T);
|
simulateDragging = true;
|
||||||
widget.onDragStarted(
|
widget.deleteAnimationController.reverse(from: 1.0);
|
||||||
widget.child,
|
widget.onDragStarted(
|
||||||
widget.eventData.index,
|
widget.child,
|
||||||
widget.eventData.feedbackSize,
|
widget.eventData.index,
|
||||||
);
|
widget.eventData.feedbackSize,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
Log.trace("[$FakeDragTarget] cancel start drag");
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -35,7 +35,7 @@ abstract class DragTargetInterceptor {
|
|||||||
|
|
||||||
abstract class OverlapDragTargetDelegate {
|
abstract class OverlapDragTargetDelegate {
|
||||||
void cancel();
|
void cancel();
|
||||||
void moveTo(
|
void dragTargetDidMoveToReorderFlex(
|
||||||
String reorderFlexId,
|
String reorderFlexId,
|
||||||
FlexDragTargetData dragTargetData,
|
FlexDragTargetData dragTargetData,
|
||||||
int dragTargetIndex,
|
int dragTargetIndex,
|
||||||
@ -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)) {
|
||||||
@ -99,10 +99,10 @@ class OverlappingDragTargetInterceptor extends DragTargetInterceptor {
|
|||||||
if (index != -1) {
|
if (index != -1) {
|
||||||
Log.trace(
|
Log.trace(
|
||||||
'[$OverlappingDragTargetInterceptor] move to $dragTargetId at $index');
|
'[$OverlappingDragTargetInterceptor] move to $dragTargetId at $index');
|
||||||
delegate.moveTo(dragTargetId, dragTargetData, index);
|
delegate.dragTargetDidMoveToReorderFlex(
|
||||||
|
dragTargetId, dragTargetData, index);
|
||||||
|
|
||||||
columnsState
|
columnsState.reorderFlexActionMap[dragTargetId]
|
||||||
.getReorderFlexState(groupId: dragTargetId)
|
|
||||||
?.resetDragTargetIndex(index);
|
?.resetDragTargetIndex(index);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -153,7 +153,7 @@ class CrossReorderFlexDragTargetInterceptor extends DragTargetInterceptor {
|
|||||||
/// it means the dragTarget is dragging on the top of its own list.
|
/// it means the dragTarget is dragging on the top of its own list.
|
||||||
/// Otherwise, it means the dargTarget was moved to another list.
|
/// Otherwise, it means the dargTarget was moved to another list.
|
||||||
Log.trace(
|
Log.trace(
|
||||||
"[$CrossReorderFlexDragTargetInterceptor] $reorderFlexId accept ${dragTargetData.reorderFlexId} ${reorderFlexId != dragTargetData.reorderFlexId}");
|
"[$CrossReorderFlexDragTargetInterceptor] $reorderFlexId should accept ${dragTargetData.reorderFlexId} : ${reorderFlexId != dragTargetData.reorderFlexId}");
|
||||||
return reorderFlexId != dragTargetData.reorderFlexId;
|
return reorderFlexId != dragTargetData.reorderFlexId;
|
||||||
} else {
|
} else {
|
||||||
Log.trace(
|
Log.trace(
|
||||||
|
@ -31,27 +31,43 @@ 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 {
|
||||||
/// The opacity of the dragging widget
|
/// The opacity of the dragging widget
|
||||||
final double draggingWidgetOpacity = 0.3;
|
final double draggingWidgetOpacity = 0.4;
|
||||||
|
|
||||||
// How long an animation to reorder an element
|
// How long an animation to reorder an element
|
||||||
final Duration reorderAnimationDuration = const Duration(milliseconds: 300);
|
final Duration reorderAnimationDuration = const Duration(milliseconds: 200);
|
||||||
|
|
||||||
// How long an animation to scroll to an off-screen element
|
// How long an animation to scroll to an off-screen element
|
||||||
final Duration scrollAnimationDuration = const Duration(milliseconds: 300);
|
final Duration scrollAnimationDuration = const Duration(milliseconds: 200);
|
||||||
|
|
||||||
/// Determines if setSatte method needs to be called when the drag is complete.
|
|
||||||
/// Default value is [true].
|
|
||||||
///
|
|
||||||
/// If the [ReorderFlex] needs to be rebuild after the [ReorderFlex] end dragging,
|
|
||||||
/// the [setStateWhenEndDrag] should set to [true].
|
|
||||||
final bool setStateWhenEndDrag;
|
|
||||||
|
|
||||||
final bool useMoveAnimation;
|
final bool useMoveAnimation;
|
||||||
|
|
||||||
@ -59,7 +75,6 @@ class ReorderFlexConfig {
|
|||||||
|
|
||||||
const ReorderFlexConfig({
|
const ReorderFlexConfig({
|
||||||
this.useMoveAnimation = true,
|
this.useMoveAnimation = true,
|
||||||
this.setStateWhenEndDrag = true,
|
|
||||||
}) : useMovePlaceholder = !useMoveAnimation;
|
}) : useMovePlaceholder = !useMoveAnimation;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -86,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;
|
||||||
|
|
||||||
@ -101,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.'),
|
||||||
@ -117,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;
|
||||||
|
|
||||||
@ -139,22 +158,31 @@ 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,
|
||||||
entranceAnimateStatusChanged: (status) {
|
entranceAnimateStatusChanged: (status) {
|
||||||
if (status == AnimationStatus.completed) {
|
if (status == AnimationStatus.completed) {
|
||||||
|
if (dragState.nextIndex == -1) return;
|
||||||
setState(() => _requestAnimationToNextIndex());
|
setState(() => _requestAnimationToNextIndex());
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
vsync: this,
|
vsync: this,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
widget.reorderFlexAction?._scrollToBottom = (fn) {
|
||||||
|
scrollToBottom(fn);
|
||||||
|
};
|
||||||
|
|
||||||
|
widget.reorderFlexAction?._resetDragTargetIndex = (index) {
|
||||||
|
resetDragTargetIndex(index);
|
||||||
|
};
|
||||||
|
|
||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -191,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,
|
||||||
@ -243,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()) {
|
||||||
@ -349,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,
|
||||||
@ -371,18 +412,24 @@ 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;
|
||||||
},
|
},
|
||||||
onDragEnded: (dragTargetData) {
|
onDragEnded: (dragTargetData) {
|
||||||
if (!mounted) return;
|
if (!mounted) {
|
||||||
|
Log.warn(
|
||||||
|
"[DragTarget]: Group:[${widget.dataSource.identifier}] end dragging but current widget was unmounted");
|
||||||
|
return;
|
||||||
|
}
|
||||||
Log.debug(
|
Log.debug(
|
||||||
"[DragTarget]: Group:[${widget.dataSource.identifier}] end dragging");
|
"[DragTarget]: Group:[${widget.dataSource.identifier}] end dragging");
|
||||||
_notifier.updateDragTargetIndex(-1);
|
|
||||||
|
|
||||||
onDragEnded() {
|
_notifier.updateDragTargetIndex(-1);
|
||||||
|
_animation.insertController.stop();
|
||||||
|
|
||||||
|
setState(() {
|
||||||
if (dragTargetData.reorderFlexId == widget.reorderFlexId) {
|
if (dragTargetData.reorderFlexId == widget.reorderFlexId) {
|
||||||
_onReordered(
|
_onReordered(
|
||||||
dragState.dragStartIndex,
|
dragState.dragStartIndex,
|
||||||
@ -391,13 +438,7 @@ class ReorderFlexState extends State<ReorderFlex>
|
|||||||
}
|
}
|
||||||
dragState.endDragging();
|
dragState.endDragging();
|
||||||
widget.onDragEnded?.call();
|
widget.onDragEnded?.call();
|
||||||
}
|
});
|
||||||
|
|
||||||
if (widget.config.setStateWhenEndDrag) {
|
|
||||||
setState(() => onDragEnded());
|
|
||||||
} else {
|
|
||||||
onDragEnded();
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
onWillAccept: (FlexDragTargetData dragTargetData) {
|
onWillAccept: (FlexDragTargetData dragTargetData) {
|
||||||
// Do not receive any events if the Insert item is animating.
|
// Do not receive any events if the Insert item is animating.
|
||||||
@ -405,19 +446,23 @@ class ReorderFlexState extends State<ReorderFlex>
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
assert(widget.dataSource.items.length > dragTargetIndex);
|
if (dragTargetData.isDragging) {
|
||||||
if (_interceptDragTarget(dragTargetData, (interceptor) {
|
assert(widget.dataSource.items.length > dragTargetIndex);
|
||||||
interceptor.onWillAccept(
|
if (_interceptDragTarget(dragTargetData, (interceptor) {
|
||||||
context: builderContext,
|
interceptor.onWillAccept(
|
||||||
reorderFlexState: this,
|
context: builderContext,
|
||||||
dragTargetData: dragTargetData,
|
reorderFlexState: this,
|
||||||
dragTargetId: reorderFlexItem.id,
|
dragTargetData: dragTargetData,
|
||||||
dragTargetIndex: dragTargetIndex,
|
dragTargetId: reorderFlexItem.id,
|
||||||
);
|
dragTargetIndex: dragTargetIndex,
|
||||||
})) {
|
);
|
||||||
return true;
|
})) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return handleOnWillAccept(builderContext, dragTargetIndex);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
return handleOnWillAccept(builderContext, dragTargetIndex);
|
return false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onAccept: (dragTargetData) {
|
onAccept: (dragTargetData) {
|
||||||
@ -438,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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -485,8 +531,12 @@ class ReorderFlexState extends State<ReorderFlex>
|
|||||||
}
|
}
|
||||||
|
|
||||||
void resetDragTargetIndex(int dragTargetIndex) {
|
void resetDragTargetIndex(int dragTargetIndex) {
|
||||||
|
if (dragTargetIndex > widget.dataSource.items.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
dragState.setStartDraggingIndex(dragTargetIndex);
|
dragState.setStartDraggingIndex(dragTargetIndex);
|
||||||
widget.dragStateStorage?.write(
|
widget.dragStateStorage?.insertState(
|
||||||
widget.reorderFlexId,
|
widget.reorderFlexId,
|
||||||
dragState,
|
dragState,
|
||||||
);
|
);
|
||||||
@ -521,6 +571,9 @@ class ReorderFlexState extends State<ReorderFlex>
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _onReordered(int fromIndex, int toIndex) {
|
void _onReordered(int fromIndex, int toIndex) {
|
||||||
|
if (toIndex == -1) return;
|
||||||
|
if (fromIndex == -1) return;
|
||||||
|
|
||||||
if (fromIndex != toIndex) {
|
if (fromIndex != toIndex) {
|
||||||
widget.onReorder.call(fromIndex, toIndex);
|
widget.onReorder.call(fromIndex, toIndex);
|
||||||
}
|
}
|
||||||
@ -577,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,
|
||||||
|
@ -94,7 +94,8 @@ class BoardPhantomController extends OverlapDragTargetDelegate
|
|||||||
|
|
||||||
/// Remove the phantom in the group if it contains phantom
|
/// Remove the phantom in the group if it contains phantom
|
||||||
void _removePhantom(String groupId) {
|
void _removePhantom(String groupId) {
|
||||||
if (delegate.removePhantom(groupId)) {
|
final didRemove = delegate.removePhantom(groupId);
|
||||||
|
if (didRemove) {
|
||||||
phantomState.notifyDidRemovePhantom(groupId);
|
phantomState.notifyDidRemovePhantom(groupId);
|
||||||
phantomState.removeGroupListener(groupId);
|
phantomState.removeGroupListener(groupId);
|
||||||
}
|
}
|
||||||
@ -195,7 +196,7 @@ class BoardPhantomController extends OverlapDragTargetDelegate
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void moveTo(
|
void dragTargetDidMoveToReorderFlex(
|
||||||
String reorderFlexId,
|
String reorderFlexId,
|
||||||
FlexDragTargetData dragTargetData,
|
FlexDragTargetData dragTargetData,
|
||||||
int dragTargetIndex,
|
int dragTargetIndex,
|
||||||
|
@ -1,50 +1,54 @@
|
|||||||
|
import 'package:appflowy_board/src/utils/log.dart';
|
||||||
|
|
||||||
import 'phantom_controller.dart';
|
import 'phantom_controller.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class GroupPhantomState {
|
class GroupPhantomState {
|
||||||
final _states = <String, GroupState>{};
|
final _groupStates = <String, GroupState>{};
|
||||||
|
final _groupIsDragging = <String, bool>{};
|
||||||
|
|
||||||
void setGroupIsDragging(String groupId, bool isDragging) {
|
void setGroupIsDragging(String groupId, bool isDragging) {
|
||||||
_stateWithId(groupId).isDragging = isDragging;
|
_groupIsDragging[groupId] = isDragging;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isDragging(String groupId) {
|
bool isDragging(String groupId) {
|
||||||
return _stateWithId(groupId).isDragging;
|
return _groupIsDragging[groupId] ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void addGroupListener(String groupId, PassthroughPhantomListener listener) {
|
void addGroupListener(String groupId, PassthroughPhantomListener listener) {
|
||||||
_stateWithId(groupId).notifier.addListener(
|
if (_groupStates[groupId] == null) {
|
||||||
onInserted: (index) => listener.onInserted?.call(index),
|
Log.debug("[$GroupPhantomState] add group listener: $groupId");
|
||||||
onDeleted: () => listener.onDragEnded?.call(),
|
_groupStates[groupId] = GroupState();
|
||||||
);
|
_groupStates[groupId]?.notifier.addListener(
|
||||||
|
onInserted: (index) => listener.onInserted?.call(index),
|
||||||
|
onDeleted: () => listener.onDragEnded?.call(),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void removeGroupListener(String groupId) {
|
void removeGroupListener(String groupId) {
|
||||||
_stateWithId(groupId).notifier.dispose();
|
Log.debug("[$GroupPhantomState] remove group listener: $groupId");
|
||||||
_states.remove(groupId);
|
final groupState = _groupStates.remove(groupId);
|
||||||
|
groupState?.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
void notifyDidInsertPhantom(String groupId, int index) {
|
void notifyDidInsertPhantom(String groupId, int index) {
|
||||||
_stateWithId(groupId).notifier.insert(index);
|
_groupStates[groupId]?.notifier.insert(index);
|
||||||
}
|
}
|
||||||
|
|
||||||
void notifyDidRemovePhantom(String groupId) {
|
void notifyDidRemovePhantom(String groupId) {
|
||||||
_stateWithId(groupId).notifier.remove();
|
Log.debug("[$GroupPhantomState] $groupId remove phantom");
|
||||||
}
|
_groupStates[groupId]?.notifier.remove();
|
||||||
|
|
||||||
GroupState _stateWithId(String groupId) {
|
|
||||||
var state = _states[groupId];
|
|
||||||
if (state == null) {
|
|
||||||
state = GroupState();
|
|
||||||
_states[groupId] = state;
|
|
||||||
}
|
|
||||||
return state;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class GroupState {
|
class GroupState {
|
||||||
bool isDragging = false;
|
bool isDragging = false;
|
||||||
final notifier = PassthroughPhantomNotifier();
|
final notifier = PassthroughPhantomNotifier();
|
||||||
|
|
||||||
|
void dispose() {
|
||||||
|
notifier.dispose();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class PassthroughPhantomListener {
|
abstract class PassthroughPhantomListener {
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:example/plugin/code_block_node_widget.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
@ -116,9 +117,17 @@ class _MyHomePageState extends State<MyHomePage> {
|
|||||||
editorState: _editorState!,
|
editorState: _editorState!,
|
||||||
editorStyle: _editorStyle,
|
editorStyle: _editorStyle,
|
||||||
editable: true,
|
editable: true,
|
||||||
|
customBuilders: {
|
||||||
|
'text/code_block': CodeBlockNodeWidgetBuilder(),
|
||||||
|
},
|
||||||
shortcutEvents: [
|
shortcutEvents: [
|
||||||
|
enterInCodeBlock,
|
||||||
|
ignoreKeysInCodeBlock,
|
||||||
underscoreToItalic,
|
underscoreToItalic,
|
||||||
],
|
],
|
||||||
|
selectionMenuItems: [
|
||||||
|
codeBlockItem,
|
||||||
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
@ -0,0 +1,277 @@
|
|||||||
|
import 'dart:collection';
|
||||||
|
|
||||||
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:highlight/highlight.dart' as highlight;
|
||||||
|
import 'package:highlight/languages/all.dart';
|
||||||
|
|
||||||
|
ShortcutEvent enterInCodeBlock = ShortcutEvent(
|
||||||
|
key: 'Enter in code block',
|
||||||
|
command: 'enter',
|
||||||
|
handler: _enterInCodeBlockHandler,
|
||||||
|
);
|
||||||
|
|
||||||
|
ShortcutEvent ignoreKeysInCodeBlock = ShortcutEvent(
|
||||||
|
key: 'White space in code block',
|
||||||
|
command: 'space,slash,shift+underscore',
|
||||||
|
handler: _ignorekHandler,
|
||||||
|
);
|
||||||
|
|
||||||
|
ShortcutEventHandler _enterInCodeBlockHandler = (editorState, event) {
|
||||||
|
final selection = editorState.service.selectionService.currentSelection.value;
|
||||||
|
final nodes = editorState.service.selectionService.currentSelectedNodes;
|
||||||
|
final codeBlockNode =
|
||||||
|
nodes.whereType<TextNode>().where((node) => node.id == 'text/code_block');
|
||||||
|
if (codeBlockNode.length != 1 || selection == null) {
|
||||||
|
return KeyEventResult.ignored;
|
||||||
|
}
|
||||||
|
if (selection.isCollapsed) {
|
||||||
|
TransactionBuilder(editorState)
|
||||||
|
..insertText(codeBlockNode.first, selection.end.offset, '\n')
|
||||||
|
..commit();
|
||||||
|
return KeyEventResult.handled;
|
||||||
|
}
|
||||||
|
return KeyEventResult.ignored;
|
||||||
|
};
|
||||||
|
|
||||||
|
ShortcutEventHandler _ignorekHandler = (editorState, event) {
|
||||||
|
final nodes = editorState.service.selectionService.currentSelectedNodes;
|
||||||
|
final codeBlockNodes =
|
||||||
|
nodes.whereType<TextNode>().where((node) => node.id == 'text/code_block');
|
||||||
|
if (codeBlockNodes.length == 1) {
|
||||||
|
return KeyEventResult.skipRemainingHandlers;
|
||||||
|
}
|
||||||
|
return KeyEventResult.ignored;
|
||||||
|
};
|
||||||
|
|
||||||
|
SelectionMenuItem codeBlockItem = SelectionMenuItem(
|
||||||
|
name: 'Code Block',
|
||||||
|
icon: const Icon(Icons.abc),
|
||||||
|
keywords: ['code block'],
|
||||||
|
handler: (editorState, _, __) {
|
||||||
|
final selection =
|
||||||
|
editorState.service.selectionService.currentSelection.value;
|
||||||
|
final textNodes = editorState.service.selectionService.currentSelectedNodes
|
||||||
|
.whereType<TextNode>();
|
||||||
|
if (selection == null || textNodes.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (textNodes.first.toRawString().isEmpty) {
|
||||||
|
TransactionBuilder(editorState)
|
||||||
|
..updateNode(textNodes.first, {
|
||||||
|
'subtype': 'code_block',
|
||||||
|
'theme': 'vs',
|
||||||
|
'language': null,
|
||||||
|
})
|
||||||
|
..afterSelection = selection
|
||||||
|
..commit();
|
||||||
|
} else {
|
||||||
|
TransactionBuilder(editorState)
|
||||||
|
..insertNode(
|
||||||
|
selection.end.path.next,
|
||||||
|
TextNode(
|
||||||
|
type: 'text',
|
||||||
|
children: LinkedList(),
|
||||||
|
attributes: {
|
||||||
|
'subtype': 'code_block',
|
||||||
|
'theme': 'vs',
|
||||||
|
'language': null,
|
||||||
|
},
|
||||||
|
delta: Delta()..insert('\n'),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
..afterSelection = selection
|
||||||
|
..commit();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
class CodeBlockNodeWidgetBuilder extends NodeWidgetBuilder<TextNode> {
|
||||||
|
@override
|
||||||
|
Widget build(NodeWidgetContext<TextNode> context) {
|
||||||
|
return _CodeBlockNodeWidge(
|
||||||
|
key: context.node.key,
|
||||||
|
textNode: context.node,
|
||||||
|
editorState: context.editorState,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
NodeValidator<Node> get nodeValidator => (node) {
|
||||||
|
return node is TextNode && node.attributes['theme'] is String;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CodeBlockNodeWidge extends StatefulWidget {
|
||||||
|
const _CodeBlockNodeWidge({
|
||||||
|
Key? key,
|
||||||
|
required this.textNode,
|
||||||
|
required this.editorState,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
final TextNode textNode;
|
||||||
|
final EditorState editorState;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_CodeBlockNodeWidge> createState() => __CodeBlockNodeWidgeState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class __CodeBlockNodeWidgeState extends State<_CodeBlockNodeWidge>
|
||||||
|
with SelectableMixin, DefaultSelectable {
|
||||||
|
final _richTextKey = GlobalKey(debugLabel: 'code_block_text');
|
||||||
|
final _padding = const EdgeInsets.only(left: 20, top: 20, bottom: 20);
|
||||||
|
String? get _language => widget.textNode.attributes['language'] as String?;
|
||||||
|
String? _detectLanguage;
|
||||||
|
|
||||||
|
@override
|
||||||
|
SelectableMixin<StatefulWidget> get forward =>
|
||||||
|
_richTextKey.currentState as SelectableMixin;
|
||||||
|
|
||||||
|
@override
|
||||||
|
GlobalKey<State<StatefulWidget>>? get iconKey => null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Offset get baseOffset => super.baseOffset + _padding.topLeft;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Stack(
|
||||||
|
children: [
|
||||||
|
_buildCodeBlock(context),
|
||||||
|
_buildSwitchCodeButton(context),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildCodeBlock(BuildContext context) {
|
||||||
|
final result = highlight.highlight.parse(
|
||||||
|
widget.textNode.toRawString(),
|
||||||
|
language: _language,
|
||||||
|
autoDetection: _language == null,
|
||||||
|
);
|
||||||
|
_detectLanguage = _language ?? result.language;
|
||||||
|
final code = result.nodes;
|
||||||
|
final codeTextSpan = _convert(code!);
|
||||||
|
return Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(8.0)),
|
||||||
|
color: Colors.grey.withOpacity(0.1),
|
||||||
|
),
|
||||||
|
padding: _padding,
|
||||||
|
width: MediaQuery.of(context).size.width,
|
||||||
|
child: FlowyRichText(
|
||||||
|
key: _richTextKey,
|
||||||
|
textNode: widget.textNode,
|
||||||
|
editorState: widget.editorState,
|
||||||
|
textSpanDecorator: (textSpan) => TextSpan(
|
||||||
|
style: widget.editorState.editorStyle.textStyle.defaultTextStyle,
|
||||||
|
children: codeTextSpan,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSwitchCodeButton(BuildContext context) {
|
||||||
|
return Positioned(
|
||||||
|
top: -5,
|
||||||
|
right: 0,
|
||||||
|
child: DropdownButton<String>(
|
||||||
|
value: _detectLanguage,
|
||||||
|
onChanged: (value) {
|
||||||
|
TransactionBuilder(widget.editorState)
|
||||||
|
..updateNode(widget.textNode, {
|
||||||
|
'language': value,
|
||||||
|
})
|
||||||
|
..commit();
|
||||||
|
},
|
||||||
|
items: allLanguages.keys.map<DropdownMenuItem<String>>((String value) {
|
||||||
|
return DropdownMenuItem<String>(
|
||||||
|
value: value,
|
||||||
|
child: Text(
|
||||||
|
value,
|
||||||
|
style: const TextStyle(fontSize: 12.0),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList(growable: false),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy from flutter.highlight package.
|
||||||
|
// https://github.com/git-touch/highlight.dart/blob/master/flutter_highlight/lib/flutter_highlight.dart
|
||||||
|
List<TextSpan> _convert(List<highlight.Node> nodes) {
|
||||||
|
List<TextSpan> spans = [];
|
||||||
|
var currentSpans = spans;
|
||||||
|
List<List<TextSpan>> stack = [];
|
||||||
|
|
||||||
|
_traverse(highlight.Node node) {
|
||||||
|
if (node.value != null) {
|
||||||
|
currentSpans.add(node.className == null
|
||||||
|
? TextSpan(text: node.value)
|
||||||
|
: TextSpan(
|
||||||
|
text: node.value,
|
||||||
|
style: _builtInCodeBlockTheme[node.className!]));
|
||||||
|
} else if (node.children != null) {
|
||||||
|
List<TextSpan> tmp = [];
|
||||||
|
currentSpans.add(TextSpan(
|
||||||
|
children: tmp, style: _builtInCodeBlockTheme[node.className!]));
|
||||||
|
stack.add(currentSpans);
|
||||||
|
currentSpans = tmp;
|
||||||
|
|
||||||
|
for (var n in node.children!) {
|
||||||
|
_traverse(n);
|
||||||
|
if (n == node.children!.last) {
|
||||||
|
currentSpans = stack.isEmpty ? spans : stack.removeLast();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var node in nodes) {
|
||||||
|
_traverse(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
return spans;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const _builtInCodeBlockTheme = {
|
||||||
|
'root':
|
||||||
|
TextStyle(backgroundColor: Color(0xffffffff), color: Color(0xff000000)),
|
||||||
|
'comment': TextStyle(color: Color(0xff007400)),
|
||||||
|
'quote': TextStyle(color: Color(0xff007400)),
|
||||||
|
'tag': TextStyle(color: Color(0xffaa0d91)),
|
||||||
|
'attribute': TextStyle(color: Color(0xffaa0d91)),
|
||||||
|
'keyword': TextStyle(color: Color(0xffaa0d91)),
|
||||||
|
'selector-tag': TextStyle(color: Color(0xffaa0d91)),
|
||||||
|
'literal': TextStyle(color: Color(0xffaa0d91)),
|
||||||
|
'name': TextStyle(color: Color(0xffaa0d91)),
|
||||||
|
'variable': TextStyle(color: Color(0xff3F6E74)),
|
||||||
|
'template-variable': TextStyle(color: Color(0xff3F6E74)),
|
||||||
|
'code': TextStyle(color: Color(0xffc41a16)),
|
||||||
|
'string': TextStyle(color: Color(0xffc41a16)),
|
||||||
|
'meta-string': TextStyle(color: Color(0xffc41a16)),
|
||||||
|
'regexp': TextStyle(color: Color(0xff0E0EFF)),
|
||||||
|
'link': TextStyle(color: Color(0xff0E0EFF)),
|
||||||
|
'title': TextStyle(color: Color(0xff1c00cf)),
|
||||||
|
'symbol': TextStyle(color: Color(0xff1c00cf)),
|
||||||
|
'bullet': TextStyle(color: Color(0xff1c00cf)),
|
||||||
|
'number': TextStyle(color: Color(0xff1c00cf)),
|
||||||
|
'section': TextStyle(color: Color(0xff643820)),
|
||||||
|
'meta': TextStyle(color: Color(0xff643820)),
|
||||||
|
'type': TextStyle(color: Color(0xff5c2699)),
|
||||||
|
'built_in': TextStyle(color: Color(0xff5c2699)),
|
||||||
|
'builtin-name': TextStyle(color: Color(0xff5c2699)),
|
||||||
|
'params': TextStyle(color: Color(0xff5c2699)),
|
||||||
|
'attr': TextStyle(color: Color(0xff836C28)),
|
||||||
|
'subst': TextStyle(color: Color(0xff000000)),
|
||||||
|
'formula': TextStyle(
|
||||||
|
backgroundColor: Color(0xffeeeeee), fontStyle: FontStyle.italic),
|
||||||
|
'addition': TextStyle(backgroundColor: Color(0xffbaeeba)),
|
||||||
|
'deletion': TextStyle(backgroundColor: Color(0xffffc8bd)),
|
||||||
|
'selector-id': TextStyle(color: Color(0xff9b703f)),
|
||||||
|
'selector-class': TextStyle(color: Color(0xff9b703f)),
|
||||||
|
'doctag': TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
'strong': TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
'emphasis': TextStyle(fontStyle: FontStyle.italic),
|
||||||
|
};
|
@ -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
|
||||||
|
@ -43,6 +43,7 @@ dependencies:
|
|||||||
sdk: flutter
|
sdk: flutter
|
||||||
file_picker: ^5.0.1
|
file_picker: ^5.0.1
|
||||||
universal_html: ^2.0.8
|
universal_html: ^2.0.8
|
||||||
|
highlight: ^0.7.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
@ -28,4 +28,8 @@ export 'src/service/shortcut_event/keybinding.dart';
|
|||||||
export 'src/service/shortcut_event/shortcut_event.dart';
|
export 'src/service/shortcut_event/shortcut_event.dart';
|
||||||
export 'src/service/shortcut_event/shortcut_event_handler.dart';
|
export 'src/service/shortcut_event/shortcut_event_handler.dart';
|
||||||
export 'src/extensions/attributes_extension.dart';
|
export 'src/extensions/attributes_extension.dart';
|
||||||
|
export 'src/extensions/path_extensions.dart';
|
||||||
|
export 'src/render/rich_text/default_selectable.dart';
|
||||||
|
export 'src/render/rich_text/flowy_rich_text.dart';
|
||||||
|
export 'src/render/selection_menu/selection_menu_widget.dart';
|
||||||
export 'src/l10n/l10n.dart';
|
export 'src/l10n/l10n.dart';
|
||||||
|
@ -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": {}
|
||||||
}
|
}
|
@ -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;
|
||||||
|
}
|
@ -93,12 +93,14 @@ class Node extends ChangeNotifier with LinkedListEntry<Node> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void updateAttributes(Attributes attributes) {
|
void updateAttributes(Attributes attributes) {
|
||||||
bool shouldNotifyParent = _attributes['subtype'] != attributes['subtype'];
|
final oldAttributes = {..._attributes};
|
||||||
|
|
||||||
_attributes = composeAttributes(_attributes, attributes) ?? {};
|
_attributes = composeAttributes(_attributes, attributes) ?? {};
|
||||||
|
|
||||||
// Notifies the new attributes
|
// Notifies the new attributes
|
||||||
// if attributes contains 'subtype', should notify parent to rebuild node
|
// if attributes contains 'subtype', should notify parent to rebuild node
|
||||||
// else, just notify current node.
|
// else, just notify current node.
|
||||||
|
bool shouldNotifyParent =
|
||||||
|
_attributes['subtype'] != oldAttributes['subtype'];
|
||||||
shouldNotifyParent ? parent?.notifyListeners() : notifyListeners();
|
shouldNotifyParent ? parent?.notifyListeners() : notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
@ -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,7 @@ final List<SelectionMenuItem> _defaultSelectionMenuItems = [
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
SelectionMenuItem(
|
SelectionMenuItem(
|
||||||
name: AppFlowyEditorLocalizations.current.checkbox,
|
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 +178,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';
|
||||||
@ -8,7 +9,6 @@ import 'package:appflowy_editor/src/service/default_text_operations/format_rich_
|
|||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:rich_clipboard/rich_clipboard.dart';
|
import 'package:rich_clipboard/rich_clipboard.dart';
|
||||||
import 'package:appflowy_editor/src/document/built_in_attribute_keys.dart';
|
|
||||||
|
|
||||||
typedef ToolbarItemEventHandler = void Function(
|
typedef ToolbarItemEventHandler = void Function(
|
||||||
EditorState editorState, BuildContext context);
|
EditorState editorState, BuildContext context);
|
||||||
@ -120,7 +120,7 @@ List<ToolbarItem> defaultToolbarItems = [
|
|||||||
name: 'toolbar/bold',
|
name: 'toolbar/bold',
|
||||||
color: isHighlight ? Colors.lightBlue : null,
|
color: isHighlight ? Colors.lightBlue : null,
|
||||||
),
|
),
|
||||||
validator: _showInTextSelection,
|
validator: _showInBuiltInTextSelection,
|
||||||
highlightCallback: (editorState) => _allSatisfy(
|
highlightCallback: (editorState) => _allSatisfy(
|
||||||
editorState,
|
editorState,
|
||||||
BuiltInAttributeKey.bold,
|
BuiltInAttributeKey.bold,
|
||||||
@ -136,7 +136,7 @@ List<ToolbarItem> defaultToolbarItems = [
|
|||||||
name: 'toolbar/italic',
|
name: 'toolbar/italic',
|
||||||
color: isHighlight ? Colors.lightBlue : null,
|
color: isHighlight ? Colors.lightBlue : null,
|
||||||
),
|
),
|
||||||
validator: _showInTextSelection,
|
validator: _showInBuiltInTextSelection,
|
||||||
highlightCallback: (editorState) => _allSatisfy(
|
highlightCallback: (editorState) => _allSatisfy(
|
||||||
editorState,
|
editorState,
|
||||||
BuiltInAttributeKey.italic,
|
BuiltInAttributeKey.italic,
|
||||||
@ -152,7 +152,7 @@ List<ToolbarItem> defaultToolbarItems = [
|
|||||||
name: 'toolbar/underline',
|
name: 'toolbar/underline',
|
||||||
color: isHighlight ? Colors.lightBlue : null,
|
color: isHighlight ? Colors.lightBlue : null,
|
||||||
),
|
),
|
||||||
validator: _showInTextSelection,
|
validator: _showInBuiltInTextSelection,
|
||||||
highlightCallback: (editorState) => _allSatisfy(
|
highlightCallback: (editorState) => _allSatisfy(
|
||||||
editorState,
|
editorState,
|
||||||
BuiltInAttributeKey.underline,
|
BuiltInAttributeKey.underline,
|
||||||
@ -168,7 +168,7 @@ List<ToolbarItem> defaultToolbarItems = [
|
|||||||
name: 'toolbar/strikethrough',
|
name: 'toolbar/strikethrough',
|
||||||
color: isHighlight ? Colors.lightBlue : null,
|
color: isHighlight ? Colors.lightBlue : null,
|
||||||
),
|
),
|
||||||
validator: _showInTextSelection,
|
validator: _showInBuiltInTextSelection,
|
||||||
highlightCallback: (editorState) => _allSatisfy(
|
highlightCallback: (editorState) => _allSatisfy(
|
||||||
editorState,
|
editorState,
|
||||||
BuiltInAttributeKey.strikethrough,
|
BuiltInAttributeKey.strikethrough,
|
||||||
@ -184,7 +184,7 @@ List<ToolbarItem> defaultToolbarItems = [
|
|||||||
name: 'toolbar/code',
|
name: 'toolbar/code',
|
||||||
color: isHighlight ? Colors.lightBlue : null,
|
color: isHighlight ? Colors.lightBlue : null,
|
||||||
),
|
),
|
||||||
validator: _showInTextSelection,
|
validator: _showInBuiltInTextSelection,
|
||||||
highlightCallback: (editorState) => _allSatisfy(
|
highlightCallback: (editorState) => _allSatisfy(
|
||||||
editorState,
|
editorState,
|
||||||
BuiltInAttributeKey.code,
|
BuiltInAttributeKey.code,
|
||||||
@ -248,7 +248,7 @@ List<ToolbarItem> defaultToolbarItems = [
|
|||||||
name: 'toolbar/highlight',
|
name: 'toolbar/highlight',
|
||||||
color: isHighlight ? Colors.lightBlue : null,
|
color: isHighlight ? Colors.lightBlue : null,
|
||||||
),
|
),
|
||||||
validator: _showInTextSelection,
|
validator: _showInBuiltInTextSelection,
|
||||||
highlightCallback: (editorState) => _allSatisfy(
|
highlightCallback: (editorState) => _allSatisfy(
|
||||||
editorState,
|
editorState,
|
||||||
BuiltInAttributeKey.backgroundColor,
|
BuiltInAttributeKey.backgroundColor,
|
||||||
@ -262,13 +262,22 @@ List<ToolbarItem> defaultToolbarItems = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
ToolbarItemValidator _onlyShowInSingleTextSelection = (editorState) {
|
ToolbarItemValidator _onlyShowInSingleTextSelection = (editorState) {
|
||||||
|
final result = _showInBuiltInTextSelection(editorState);
|
||||||
|
if (!result) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
final nodes = editorState.service.selectionService.currentSelectedNodes;
|
final nodes = editorState.service.selectionService.currentSelectedNodes;
|
||||||
return (nodes.length == 1 && nodes.first is TextNode);
|
return (nodes.length == 1 && nodes.first is TextNode);
|
||||||
};
|
};
|
||||||
|
|
||||||
ToolbarItemValidator _showInTextSelection = (editorState) {
|
ToolbarItemValidator _showInBuiltInTextSelection = (editorState) {
|
||||||
final nodes = editorState.service.selectionService.currentSelectedNodes
|
final nodes = editorState.service.selectionService.currentSelectedNodes
|
||||||
.whereType<TextNode>();
|
.whereType<TextNode>()
|
||||||
|
.where(
|
||||||
|
(textNode) =>
|
||||||
|
BuiltInAttributeKey.globalStyleKeys.contains(textNode.subtype) ||
|
||||||
|
textNode.subtype == null,
|
||||||
|
);
|
||||||
return nodes.isNotEmpty;
|
return nodes.isNotEmpty;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -337,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: () {
|
||||||
@ -369,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);
|
||||||
}
|
}
|
||||||
|
@ -103,13 +103,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -118,8 +120,8 @@ class _AppFlowyEditorState extends State<AppFlowyEditor> {
|
|||||||
key: editorState.service.keyboardServiceKey,
|
key: editorState.service.keyboardServiceKey,
|
||||||
editable: widget.editable,
|
editable: widget.editable,
|
||||||
shortcutEvents: [
|
shortcutEvents: [
|
||||||
...builtInShortcutEvents,
|
|
||||||
...widget.shortcutEvents,
|
...widget.shortcutEvents,
|
||||||
|
...builtInShortcutEvents,
|
||||||
],
|
],
|
||||||
editorState: editorState,
|
editorState: editorState,
|
||||||
child: FlowyToolbar(
|
child: FlowyToolbar(
|
||||||
|
@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -117,12 +117,17 @@ ShortcutEventHandler enterWithoutShiftInTextNodesHandler =
|
|||||||
makeFollowingNodesIncremental(editorState, insertPath, afterSelection,
|
makeFollowingNodesIncremental(editorState, insertPath, afterSelection,
|
||||||
beginNum: prevNumber);
|
beginNum: prevNumber);
|
||||||
} else {
|
} else {
|
||||||
|
bool needCopyAttributes = ![
|
||||||
|
BuiltInAttributeKey.heading,
|
||||||
|
BuiltInAttributeKey.quote,
|
||||||
|
].contains(subtype);
|
||||||
TransactionBuilder(editorState)
|
TransactionBuilder(editorState)
|
||||||
..insertNode(
|
..insertNode(
|
||||||
textNode.path,
|
textNode.path,
|
||||||
textNode.copyWith(
|
textNode.copyWith(
|
||||||
children: LinkedList(),
|
children: LinkedList(),
|
||||||
delta: Delta(),
|
delta: Delta(),
|
||||||
|
attributes: needCopyAttributes ? null : {},
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
..afterSelection = afterSelection
|
..afterSelection = afterSelection
|
||||||
@ -173,7 +178,9 @@ ShortcutEventHandler enterWithoutShiftInTextNodesHandler =
|
|||||||
Attributes _attributesFromPreviousLine(TextNode textNode) {
|
Attributes _attributesFromPreviousLine(TextNode textNode) {
|
||||||
final prevAttributes = textNode.attributes;
|
final prevAttributes = textNode.attributes;
|
||||||
final subType = textNode.subtype;
|
final subType = textNode.subtype;
|
||||||
if (subType == null || subType == BuiltInAttributeKey.heading) {
|
if (subType == null ||
|
||||||
|
subType == BuiltInAttributeKey.heading ||
|
||||||
|
subType == BuiltInAttributeKey.quote) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,126 @@
|
|||||||
|
import "dart:math";
|
||||||
|
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.length > 0) {
|
||||||
|
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;
|
||||||
|
};
|
@ -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;
|
||||||
|
};
|
@ -13,12 +13,19 @@ ShortcutEventHandler tabHandler = (editorState, event) {
|
|||||||
|
|
||||||
final textNode = textNodes.first;
|
final textNode = textNodes.first;
|
||||||
final previous = textNode.previous;
|
final previous = textNode.previous;
|
||||||
if (textNode.subtype != BuiltInAttributeKey.bulletedList ||
|
|
||||||
previous == null ||
|
if (textNode.subtype != BuiltInAttributeKey.bulletedList) {
|
||||||
previous.subtype != BuiltInAttributeKey.bulletedList) {
|
TransactionBuilder(editorState)
|
||||||
|
..insertText(textNode, selection.end.offset, ' ' * 4)
|
||||||
|
..commit();
|
||||||
return KeyEventResult.handled;
|
return KeyEventResult.handled;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (previous == null ||
|
||||||
|
previous.subtype != BuiltInAttributeKey.bulletedList) {
|
||||||
|
return KeyEventResult.ignored;
|
||||||
|
}
|
||||||
|
|
||||||
final path = previous.path + [previous.children.length];
|
final path = previous.path + [previous.children.length];
|
||||||
final afterSelection = Selection(
|
final afterSelection = Selection(
|
||||||
start: selection.start.copyWith(path: path),
|
start: selection.start.copyWith(path: path),
|
||||||
|
@ -124,6 +124,8 @@ class _AppFlowyKeyboardState extends State<AppFlowyKeyboard>
|
|||||||
final result = shortcutEvent.handler(widget.editorState, event);
|
final result = shortcutEvent.handler(widget.editorState, event);
|
||||||
if (result == KeyEventResult.handled) {
|
if (result == KeyEventResult.handled) {
|
||||||
return KeyEventResult.handled;
|
return KeyEventResult.handled;
|
||||||
|
} else if (result == KeyEventResult.skipRemainingHandlers) {
|
||||||
|
return KeyEventResult.skipRemainingHandlers;
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
@ -5,14 +5,17 @@ import 'package:appflowy_editor/src/service/internal_key_event_handlers/backspac
|
|||||||
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_handler.dart';
|
import 'package:appflowy_editor/src/service/internal_key_event_handlers/markdown_syntax_to_styled_text_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 = [
|
||||||
@ -260,4 +263,18 @@ List<ShortcutEvent> builtInShortcutEvents = [
|
|||||||
command: 'shift+underscore',
|
command: 'shift+underscore',
|
||||||
handler: doubleUnderscoresToBold,
|
handler: doubleUnderscoresToBold,
|
||||||
),
|
),
|
||||||
|
key: 'Backquote to code',
|
||||||
|
command: 'backquote',
|
||||||
|
handler: backquoteToCodeHandler,
|
||||||
|
),
|
||||||
|
// 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,
|
||||||
|
),
|
||||||
];
|
];
|
||||||
|
@ -38,14 +38,17 @@ class _FlowyToolbarState extends State<FlowyToolbar>
|
|||||||
@override
|
@override
|
||||||
void showInOffset(Offset offset, LayerLink layerLink) {
|
void showInOffset(Offset offset, LayerLink layerLink) {
|
||||||
hide();
|
hide();
|
||||||
|
final items = _filterItems(defaultToolbarItems);
|
||||||
|
if (items.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
_toolbarOverlay = OverlayEntry(
|
_toolbarOverlay = OverlayEntry(
|
||||||
builder: (context) => ToolbarWidget(
|
builder: (context) => ToolbarWidget(
|
||||||
key: _toolbarWidgetKey,
|
key: _toolbarWidgetKey,
|
||||||
editorState: widget.editorState,
|
editorState: widget.editorState,
|
||||||
layerLink: layerLink,
|
layerLink: layerLink,
|
||||||
offset: offset,
|
offset: offset,
|
||||||
items: _filterItems(defaultToolbarItems),
|
items: items,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
Overlay.of(context)?.insert(_toolbarOverlay!);
|
Overlay.of(context)?.insert(_toolbarOverlay!);
|
||||||
@ -102,9 +105,4 @@ class _FlowyToolbarState extends State<FlowyToolbar>
|
|||||||
}
|
}
|
||||||
return dividedItems;
|
return dividedItems;
|
||||||
}
|
}
|
||||||
|
|
||||||
// List<ToolbarItem> _highlightItems(
|
|
||||||
// List<ToolbarItem> items,
|
|
||||||
// Selection selection,
|
|
||||||
// ) {}
|
|
||||||
}
|
}
|
||||||
|
@ -1,49 +0,0 @@
|
|||||||
import 'package:appflowy_editor/src/render/selection_menu/selection_menu_item_widget.dart';
|
|
||||||
import 'package:appflowy_editor/src/render/selection_menu/selection_menu_service.dart';
|
|
||||||
import 'package:appflowy_editor/src/render/selection_menu/selection_menu_widget.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
|
||||||
import '../../infra/test_editor.dart';
|
|
||||||
|
|
||||||
void main() async {
|
|
||||||
setUpAll(() {
|
|
||||||
TestWidgetsFlutterBinding.ensureInitialized();
|
|
||||||
});
|
|
||||||
|
|
||||||
group('selection_menu_item_widget.dart', () {
|
|
||||||
testWidgets('test selection menu item widget', (tester) async {
|
|
||||||
bool flag = false;
|
|
||||||
final editorState = tester.editor.editorState;
|
|
||||||
final menuService = _TestSelectionMenuService();
|
|
||||||
const icon = Icon(Icons.abc);
|
|
||||||
final item = SelectionMenuItem(
|
|
||||||
name: 'example',
|
|
||||||
icon: icon,
|
|
||||||
keywords: ['example A', 'example B'],
|
|
||||||
handler: (editorState, menuService, context) {
|
|
||||||
flag = true;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
final widget = SelectionMenuItemWidget(
|
|
||||||
editorState: editorState,
|
|
||||||
menuService: menuService,
|
|
||||||
item: item,
|
|
||||||
isSelected: true,
|
|
||||||
);
|
|
||||||
await tester.pumpWidget(MaterialApp(home: widget));
|
|
||||||
await tester.tap(find.byType(SelectionMenuItemWidget));
|
|
||||||
expect(flag, true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
class _TestSelectionMenuService implements SelectionMenuService {
|
|
||||||
@override
|
|
||||||
void dismiss() {}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void show() {}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Offset get topLeft => throw UnimplementedError();
|
|
||||||
}
|
|
@ -1,5 +1,4 @@
|
|||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
import 'package:appflowy_editor/src/document/built_in_attribute_keys.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:appflowy_editor/src/render/selection_menu/selection_menu_widget.dart';
|
||||||
@ -13,29 +12,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 {
|
||||||
@ -138,23 +148,27 @@ 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,6 @@ import 'package:appflowy_editor/appflowy_editor.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';
|
||||||
import 'package:appflowy_editor/src/document/built_in_attribute_keys.dart';
|
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
setUpAll(() {
|
setUpAll(() {
|
||||||
@ -171,13 +170,27 @@ Future<void> _testStyleNeedToBeCopy(WidgetTester tester, String style) async {
|
|||||||
LogicalKeyboardKey.enter,
|
LogicalKeyboardKey.enter,
|
||||||
);
|
);
|
||||||
expect(editor.documentSelection, Selection.single(path: [4], startOffset: 0));
|
expect(editor.documentSelection, Selection.single(path: [4], startOffset: 0));
|
||||||
expect(editor.nodeAtPath([4])?.subtype, style);
|
|
||||||
|
|
||||||
await editor.pressLogicKey(
|
if ([BuiltInAttributeKey.heading, BuiltInAttributeKey.quote]
|
||||||
LogicalKeyboardKey.enter,
|
.contains(style)) {
|
||||||
);
|
expect(editor.nodeAtPath([4])?.subtype, null);
|
||||||
expect(editor.documentSelection, Selection.single(path: [4], startOffset: 0));
|
|
||||||
expect(editor.nodeAtPath([4])?.subtype, null);
|
await editor.pressLogicKey(
|
||||||
|
LogicalKeyboardKey.enter,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
editor.documentSelection, Selection.single(path: [5], startOffset: 0));
|
||||||
|
expect(editor.nodeAtPath([5])?.subtype, null);
|
||||||
|
} else {
|
||||||
|
expect(editor.nodeAtPath([4])?.subtype, style);
|
||||||
|
|
||||||
|
await editor.pressLogicKey(
|
||||||
|
LogicalKeyboardKey.enter,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
editor.documentSelection, Selection.single(path: [4], startOffset: 0));
|
||||||
|
expect(editor.nodeAtPath([4])?.subtype, null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _testMultipleSelection(
|
Future<void> _testMultipleSelection(
|
||||||
|
@ -0,0 +1,154 @@
|
|||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
@ -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 😁 ',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
@ -15,23 +15,24 @@ void main() async {
|
|||||||
..insertTextNode(text)
|
..insertTextNode(text)
|
||||||
..insertTextNode(text);
|
..insertTextNode(text);
|
||||||
await editor.startTesting();
|
await editor.startTesting();
|
||||||
final document = editor.document;
|
|
||||||
|
|
||||||
var selection = Selection.single(path: [0], startOffset: 0);
|
var selection = Selection.single(path: [0], startOffset: 0);
|
||||||
await editor.updateSelection(selection);
|
await editor.updateSelection(selection);
|
||||||
await editor.pressLogicKey(LogicalKeyboardKey.tab);
|
await editor.pressLogicKey(LogicalKeyboardKey.tab);
|
||||||
|
|
||||||
// nothing happens
|
expect(
|
||||||
expect(editor.documentSelection, selection);
|
editor.documentSelection,
|
||||||
expect(editor.document.toJson(), document.toJson());
|
Selection.single(path: [0], startOffset: 4),
|
||||||
|
);
|
||||||
|
|
||||||
selection = Selection.single(path: [1], startOffset: 0);
|
selection = Selection.single(path: [1], startOffset: 0);
|
||||||
await editor.updateSelection(selection);
|
await editor.updateSelection(selection);
|
||||||
await editor.pressLogicKey(LogicalKeyboardKey.tab);
|
await editor.pressLogicKey(LogicalKeyboardKey.tab);
|
||||||
|
|
||||||
// nothing happens
|
expect(
|
||||||
expect(editor.documentSelection, selection);
|
editor.documentSelection,
|
||||||
expect(editor.document.toJson(), document.toJson());
|
Selection.single(path: [1], startOffset: 4),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('press tab in bulleted list', (tester) async {
|
testWidgets('press tab in bulleted list', (tester) async {
|
||||||
@ -63,7 +64,10 @@ void main() async {
|
|||||||
await editor.pressLogicKey(LogicalKeyboardKey.tab);
|
await editor.pressLogicKey(LogicalKeyboardKey.tab);
|
||||||
|
|
||||||
// nothing happens
|
// nothing happens
|
||||||
expect(editor.documentSelection, selection);
|
expect(
|
||||||
|
editor.documentSelection,
|
||||||
|
Selection.single(path: [0], startOffset: 0),
|
||||||
|
);
|
||||||
expect(editor.document.toJson(), document.toJson());
|
expect(editor.document.toJson(), document.toJson());
|
||||||
|
|
||||||
// Before
|
// Before
|
||||||
|
@ -104,7 +104,7 @@ class AppTheme {
|
|||||||
..tint6 = const Color(0xfff5ffdc)
|
..tint6 = const Color(0xfff5ffdc)
|
||||||
..tint7 = const Color(0xffddffd6)
|
..tint7 = const Color(0xffddffd6)
|
||||||
..tint8 = const Color(0xffdefff1)
|
..tint8 = const Color(0xffdefff1)
|
||||||
..tint9 = const Color(0xffdefff1)
|
..tint9 = const Color(0xffe1fbff)
|
||||||
..main1 = const Color(0xff00bcf0)
|
..main1 = const Color(0xff00bcf0)
|
||||||
..main2 = const Color(0xff00b7ea)
|
..main2 = const Color(0xff00b7ea)
|
||||||
..textColor = _black
|
..textColor = _black
|
||||||
@ -152,7 +152,8 @@ class AppTheme {
|
|||||||
ThemeData get themeData {
|
ThemeData get themeData {
|
||||||
var t = ThemeData(
|
var t = ThemeData(
|
||||||
textTheme: TextTheme(bodyText2: TextStyle(color: textColor)),
|
textTheme: TextTheme(bodyText2: TextStyle(color: textColor)),
|
||||||
textSelectionTheme: TextSelectionThemeData(cursorColor: main2, selectionHandleColor: main2),
|
textSelectionTheme: TextSelectionThemeData(
|
||||||
|
cursorColor: main2, selectionHandleColor: main2),
|
||||||
primaryIconTheme: IconThemeData(color: hover),
|
primaryIconTheme: IconThemeData(color: hover),
|
||||||
iconTheme: IconThemeData(color: shader1),
|
iconTheme: IconThemeData(color: shader1),
|
||||||
canvasColor: shader6,
|
canvasColor: shader6,
|
||||||
@ -179,7 +180,8 @@ class AppTheme {
|
|||||||
toggleableActiveColor: main1);
|
toggleableActiveColor: main1);
|
||||||
}
|
}
|
||||||
|
|
||||||
Color shift(Color c, double d) => ColorUtils.shiftHsl(c, d * (isDark ? -1 : 1));
|
Color shift(Color c, double d) =>
|
||||||
|
ColorUtils.shiftHsl(c, d * (isDark ? -1 : 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
class ColorUtils {
|
class ColorUtils {
|
||||||
@ -188,14 +190,18 @@ class ColorUtils {
|
|||||||
return hslc.withLightness((hslc.lightness + amt).clamp(0.0, 1.0)).toColor();
|
return hslc.withLightness((hslc.lightness + amt).clamp(0.0, 1.0)).toColor();
|
||||||
}
|
}
|
||||||
|
|
||||||
static Color parseHex(String value) => Color(int.parse(value.substring(1, 7), radix: 16) + 0xFF000000);
|
static Color parseHex(String value) =>
|
||||||
|
Color(int.parse(value.substring(1, 7), radix: 16) + 0xFF000000);
|
||||||
|
|
||||||
static Color blend(Color dst, Color src, double opacity) {
|
static Color blend(Color dst, Color src, double opacity) {
|
||||||
return Color.fromARGB(
|
return Color.fromARGB(
|
||||||
255,
|
255,
|
||||||
(dst.red.toDouble() * (1.0 - opacity) + src.red.toDouble() * opacity).toInt(),
|
(dst.red.toDouble() * (1.0 - opacity) + src.red.toDouble() * opacity)
|
||||||
(dst.green.toDouble() * (1.0 - opacity) + src.green.toDouble() * opacity).toInt(),
|
.toInt(),
|
||||||
(dst.blue.toDouble() * (1.0 - opacity) + src.blue.toDouble() * opacity).toInt(),
|
(dst.green.toDouble() * (1.0 - opacity) + src.green.toDouble() * opacity)
|
||||||
|
.toInt(),
|
||||||
|
(dst.blue.toDouble() * (1.0 - opacity) + src.blue.toDouble() * opacity)
|
||||||
|
.toInt(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -218,8 +218,8 @@ class OverlayScreen extends StatelessWidget {
|
|||||||
overlapBehaviour: providerContext
|
overlapBehaviour: providerContext
|
||||||
.read<OverlayDemoConfiguration>()
|
.read<OverlayDemoConfiguration>()
|
||||||
.overlapBehaviour,
|
.overlapBehaviour,
|
||||||
width: 200.0,
|
constraints:
|
||||||
height: 200.0,
|
BoxConstraints.tight(const Size(200, 200)),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
child: const Text('Show List Overlay'),
|
child: const Text('Show List Overlay'),
|
||||||
|
@ -30,7 +30,7 @@ class AppFlowyPopover extends StatelessWidget {
|
|||||||
this.offset,
|
this.offset,
|
||||||
this.controller,
|
this.controller,
|
||||||
this.asBarrier = false,
|
this.asBarrier = false,
|
||||||
this.margin = const EdgeInsets.all(12),
|
this.margin = const EdgeInsets.all(6),
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flowy_infra/theme.dart';
|
import 'package:flowy_infra/theme.dart';
|
||||||
@ -19,46 +21,55 @@ class ListOverlay extends StatelessWidget {
|
|||||||
const ListOverlay({
|
const ListOverlay({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.itemBuilder,
|
required this.itemBuilder,
|
||||||
this.itemCount,
|
this.itemCount = 0,
|
||||||
this.controller,
|
this.controller,
|
||||||
this.width = double.infinity,
|
this.constraints = const BoxConstraints(),
|
||||||
this.height = double.infinity,
|
|
||||||
this.footer,
|
this.footer,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
final IndexedWidgetBuilder itemBuilder;
|
final IndexedWidgetBuilder itemBuilder;
|
||||||
final int? itemCount;
|
final int itemCount;
|
||||||
final ScrollController? controller;
|
final ScrollController? controller;
|
||||||
final double width;
|
final BoxConstraints constraints;
|
||||||
final double height;
|
|
||||||
final ListOverlayFooter? footer;
|
final ListOverlayFooter? footer;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
const padding = EdgeInsets.symmetric(horizontal: 6, vertical: 6);
|
const padding = EdgeInsets.symmetric(horizontal: 6, vertical: 6);
|
||||||
double totalHeight = height + padding.vertical;
|
double totalHeight = constraints.minHeight + padding.vertical;
|
||||||
if (footer != null) {
|
if (footer != null) {
|
||||||
totalHeight = totalHeight + footer!.height + footer!.padding.vertical;
|
totalHeight = totalHeight + footer!.height + footer!.padding.vertical;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final innerConstraints = BoxConstraints(
|
||||||
|
minHeight: totalHeight,
|
||||||
|
maxHeight: max(constraints.maxHeight, totalHeight),
|
||||||
|
minWidth: constraints.minWidth,
|
||||||
|
maxWidth: constraints.maxWidth,
|
||||||
|
);
|
||||||
|
|
||||||
|
List<Widget> children = [];
|
||||||
|
for (var i = 0; i < itemCount; i++) {
|
||||||
|
children.add(itemBuilder(context, i));
|
||||||
|
}
|
||||||
|
|
||||||
return OverlayContainer(
|
return OverlayContainer(
|
||||||
constraints: BoxConstraints.tight(Size(width, totalHeight)),
|
constraints: innerConstraints,
|
||||||
padding: padding,
|
padding: padding,
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
child: Column(
|
scrollDirection: Axis.horizontal,
|
||||||
children: [
|
child: IntrinsicWidth(
|
||||||
ListView.builder(
|
child: Column(
|
||||||
shrinkWrap: true,
|
mainAxisSize: MainAxisSize.max,
|
||||||
itemBuilder: itemBuilder,
|
children: [
|
||||||
itemCount: itemCount,
|
...children,
|
||||||
controller: controller,
|
if (footer != null)
|
||||||
),
|
Padding(
|
||||||
if (footer != null)
|
padding: footer!.padding,
|
||||||
Padding(
|
child: footer!.widget,
|
||||||
padding: footer!.padding,
|
),
|
||||||
child: footer!.widget,
|
],
|
||||||
),
|
),
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -68,10 +79,9 @@ class ListOverlay extends StatelessWidget {
|
|||||||
BuildContext context, {
|
BuildContext context, {
|
||||||
required String identifier,
|
required String identifier,
|
||||||
required IndexedWidgetBuilder itemBuilder,
|
required IndexedWidgetBuilder itemBuilder,
|
||||||
int? itemCount,
|
int itemCount = 0,
|
||||||
ScrollController? controller,
|
ScrollController? controller,
|
||||||
double width = double.infinity,
|
BoxConstraints constraints = const BoxConstraints(),
|
||||||
double height = double.infinity,
|
|
||||||
required BuildContext anchorContext,
|
required BuildContext anchorContext,
|
||||||
AnchorDirection? anchorDirection,
|
AnchorDirection? anchorDirection,
|
||||||
FlowyOverlayDelegate? delegate,
|
FlowyOverlayDelegate? delegate,
|
||||||
@ -85,8 +95,7 @@ class ListOverlay extends StatelessWidget {
|
|||||||
itemBuilder: itemBuilder,
|
itemBuilder: itemBuilder,
|
||||||
itemCount: itemCount,
|
itemCount: itemCount,
|
||||||
controller: controller,
|
controller: controller,
|
||||||
width: width,
|
constraints: constraints,
|
||||||
height: height,
|
|
||||||
footer: footer,
|
footer: footer,
|
||||||
),
|
),
|
||||||
identifier: identifier,
|
identifier: identifier,
|
||||||
@ -122,7 +131,9 @@ class OverlayContainer extends StatelessWidget {
|
|||||||
child: Container(
|
child: Container(
|
||||||
padding: padding,
|
padding: padding,
|
||||||
decoration: FlowyDecoration.decoration(
|
decoration: FlowyDecoration.decoration(
|
||||||
theme.surface, theme.shadowColor.withOpacity(0.15)),
|
theme.surface,
|
||||||
|
theme.shadowColor.withOpacity(0.15),
|
||||||
|
),
|
||||||
constraints: constraints,
|
constraints: constraints,
|
||||||
child: child,
|
child: child,
|
||||||
),
|
),
|
||||||
|
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",
|
||||||
|
@ -206,6 +206,15 @@ impl std::convert::From<&str> for ViewIdPB {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Default, ProtoBuf, Clone, Debug)]
|
||||||
|
pub struct DeletedViewPB {
|
||||||
|
#[pb(index = 1)]
|
||||||
|
pub view_id: String,
|
||||||
|
|
||||||
|
#[pb(index = 2, one_of)]
|
||||||
|
pub index: Option<i32>,
|
||||||
|
}
|
||||||
|
|
||||||
impl std::ops::Deref for ViewIdPB {
|
impl std::ops::Deref for ViewIdPB {
|
||||||
type Target = str;
|
type Target = str;
|
||||||
|
|
||||||
|
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