From b28422f25f56ba9db5f4d6191e88500ecb3f102b Mon Sep 17 00:00:00 2001 From: "shih.zhang" Date: Fri, 29 Jul 2022 09:58:56 +0800 Subject: [PATCH 001/106] Update zh-CN.json --- .../app_flowy/assets/translations/zh-CN.json | 79 ++++++++++++++++++- 1 file changed, 76 insertions(+), 3 deletions(-) diff --git a/frontend/app_flowy/assets/translations/zh-CN.json b/frontend/app_flowy/assets/translations/zh-CN.json index 81b0ad13b1..3a61d5987a 100644 --- a/frontend/app_flowy/assets/translations/zh-CN.json +++ b/frontend/app_flowy/assets/translations/zh-CN.json @@ -93,8 +93,14 @@ "highlight": "高亮" }, "tooltip": { - "lightMode": "切换到灯光模式", - "darkMode": "切换到暗模式" + "lightMode": "切换到亮色模式", + "darkMode": "切换到暗色模式" + }, + "notifications": { + "export": { + "markdown": "导出笔记为Markdown文档", + "path": "Documents/flowy" + } }, "contactsPage": { "title": "联系人", @@ -135,11 +141,78 @@ "menu": { "appearance": "外观", "language": "语言", + "user": "用户", "open": "打开设置" }, "appearance": { "lightLabel": "日间模式", "darkLabel": "夜间模式" } + }, + "grid": { + "settings": { + "filter": "过滤器", + "sortBy": "排序", + "Properties": "属性" + }, + "field": { + "hide": "隐藏", + "insertLeft": "左侧插入", + "insertRight": "右侧插入", + "duplicate": "拷贝", + "delete": "删除", + "textFieldName": "文本", + "checkboxFieldName": "勾选框", + "dateFieldName": "日期", + "numberFieldName": "数字", + "singleSelectFieldName": "单项选择器", + "multiSelectFieldName": "多项选择器", + "urlFieldName": "链接", + "numberFormat": " 数字格式", + "dateFormat": " 日期格式", + "includeTime": " 包含时间", + "dateFormatFriendly": "月 日,年", + "dateFormatISO": "年-月-日", + "dateFormatLocal": "年/月/日", + "dateFormatUS": "年/月/日", + "timeFormat": " 时间格式", + "invalidTimeFormat": "时间格式错误", + "timeFormatTwelveHour": "12小时制", + "timeFormatTwentyFourHour": "24小时制", + "addSelectOption": "添加一个标签", + "optionTitle": "标签", + "addOption": "添加标签", + "editProperty": "编辑列属性" + }, + "row": { + "duplicate": "复制", + "delete": "删除", + "textPlaceholder": "空", + "copyProperty": "复制列" + }, + "selectOption": { + "create": "新建", + "purpleColor": "紫色", + "pinkColor": "粉色", + "lightPinkColor": "浅粉色", + "orangeColor": "橙色", + "yellowColor": "黄色", + "limeColor": "鲜绿色", + "greenColor": "绿色", + "aquaColor": "水蓝色", + "blueColor": "蓝色", + "deleteTag": "删除标签", + "colorPannelTitle": "颜色", + "pannelTitle": "选择或新建一个标签", + "searchOption": "搜索标签" + }, + "menuName": "网格" + }, + "document": { + "menuName": "文档", + "date": { + "timeHintTextInTwelveHour": "12:00 AM", + "timeHintTextInTwentyFourHour": "12:00" + } } -} +} \ No newline at end of file From 88423c1e86ead2816804dd30af39ad72f8fb1bb1 Mon Sep 17 00:00:00 2001 From: Naughtz Date: Mon, 8 Aug 2022 16:36:26 +0800 Subject: [PATCH 002/106] feature: Shortcut for collapse the left sidebar #692 --- frontend/app_flowy/lib/main.dart | 4 ++ .../presentation/home/home_screen.dart | 59 ++++++++++++++----- .../workspace/presentation/home/hotkeys.dart | 32 ++++++++++ .../presentation/home/menu/menu.dart | 45 ++++++++++---- .../presentation/home/navigation.dart | 40 +++++++++---- frontend/app_flowy/pubspec.lock | 7 +++ frontend/app_flowy/pubspec.yaml | 1 + 7 files changed, 147 insertions(+), 41 deletions(-) create mode 100644 frontend/app_flowy/lib/workspace/presentation/home/hotkeys.dart diff --git a/frontend/app_flowy/lib/main.dart b/frontend/app_flowy/lib/main.dart index 8b1da85f75..58b784da26 100644 --- a/frontend/app_flowy/lib/main.dart +++ b/frontend/app_flowy/lib/main.dart @@ -1,6 +1,7 @@ import 'package:app_flowy/startup/startup.dart'; import 'package:app_flowy/user/presentation/splash_screen.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:hotkey_manager/hotkey_manager.dart'; import 'package:flutter/material.dart'; class FlowyApp implements EntryPoint { @@ -14,5 +15,8 @@ void main() async { WidgetsFlutterBinding.ensureInitialized(); await EasyLocalization.ensureInitialized(); + WidgetsFlutterBinding.ensureInitialized(); + await hotKeyManager.unregisterAll(); + await FlowyRunner.run(FlowyApp()); } diff --git a/frontend/app_flowy/lib/workspace/presentation/home/home_screen.dart b/frontend/app_flowy/lib/workspace/presentation/home/home_screen.dart index abe89c14cb..bdb6088f61 100644 --- a/frontend/app_flowy/lib/workspace/presentation/home/home_screen.dart +++ b/frontend/app_flowy/lib/workspace/presentation/home/home_screen.dart @@ -1,5 +1,6 @@ import 'package:app_flowy/plugin/plugin.dart'; import 'package:app_flowy/workspace/application/home/home_bloc.dart'; +import 'package:app_flowy/workspace/presentation/home/hotkeys.dart'; import 'package:app_flowy/workspace/presentation/widgets/edit_pannel/pannel_animation.dart'; import 'package:app_flowy/workspace/presentation/widgets/float_bubble/question_bubble.dart'; import 'package:app_flowy/startup/startup.dart'; @@ -21,7 +22,8 @@ import 'menu/menu.dart'; class HomeScreen extends StatefulWidget { final UserProfilePB user; final CurrentWorkspaceSettingPB workspaceSetting; - const HomeScreen(this.user, this.workspaceSetting, {Key? key}) : super(key: key); + const HomeScreen(this.user, this.workspaceSetting, {Key? key}) + : super(key: key); @override State createState() => _HomeScreenState(); @@ -47,11 +49,13 @@ class _HomeScreenState extends State { providers: [ BlocProvider( create: (context) { - return HomeBloc(widget.user, widget.workspaceSetting)..add(const HomeEvent.initial()); + return HomeBloc(widget.user, widget.workspaceSetting) + ..add(const HomeEvent.initial()); }, ), ], - child: Scaffold( + child: HomeHotKeys( + child: Scaffold( body: BlocListener( listenWhen: (p, c) => p.unauthorized != c.unauthorized, listener: (context, state) { @@ -62,9 +66,12 @@ class _HomeScreenState extends State { child: BlocBuilder( buildWhen: (previous, current) => previous != current, builder: (context, state) { - final collapasedNotifier = getIt().collapsedNotifier; + final collapasedNotifier = + getIt().collapsedNotifier; collapasedNotifier.addPublishListener((isCollapsed) { - context.read().add(HomeEvent.forceCollapse(isCollapsed)); + context + .read() + .add(HomeEvent.forceCollapse(isCollapsed)); }); return FlowyContainer( Theme.of(context).colorScheme.surface, @@ -74,7 +81,7 @@ class _HomeScreenState extends State { }, ), ), - ), + )), ); } @@ -107,7 +114,10 @@ class _HomeScreenState extends State { ); } - Widget _buildHomeMenu({required HomeLayout layout, required BuildContext context, required HomeState state}) { + Widget _buildHomeMenu( + {required HomeLayout layout, + required BuildContext context, + required HomeState state}) { final workspaceSetting = state.workspaceSetting; if (initialView == null && workspaceSetting.hasLatestView()) { initialView = workspaceSetting.latestView; @@ -124,7 +134,8 @@ class _HomeScreenState extends State { collapsedNotifier: getIt().collapsedNotifier, ); - final latestView = workspaceSetting.hasLatestView() ? workspaceSetting.latestView : null; + final latestView = + workspaceSetting.hasLatestView() ? workspaceSetting.latestView : null; if (getIt().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. getIt().latestOpenView = latestView; @@ -133,10 +144,14 @@ class _HomeScreenState extends State { return FocusTraversalGroup(child: RepaintBoundary(child: homeMenu)); } - Widget _buildEditPannel({required HomeState homeState, required BuildContext context, required HomeLayout layout}) { + Widget _buildEditPannel( + {required HomeState homeState, + required BuildContext context, + required HomeLayout layout}) { final homeBloc = context.read(); return BlocBuilder( - buildWhen: (previous, current) => previous.pannelContext != current.pannelContext, + buildWhen: (previous, current) => + previous.pannelContext != current.pannelContext, builder: (context, state) { return state.pannelContext.fold( () => const SizedBox(), @@ -144,7 +159,8 @@ class _HomeScreenState extends State { child: RepaintBoundary( child: EditPannel( pannelContext: pannelContext, - onEndEdit: () => homeBloc.add(const HomeEvent.dismissEditPannel()), + onEndEdit: () => + homeBloc.add(const HomeEvent.dismissEditPannel()), ), ), ), @@ -161,7 +177,9 @@ class _HomeScreenState extends State { child: GestureDetector( dragStartBehavior: DragStartBehavior.down, onPanUpdate: ((details) { - context.read().add(HomeEvent.editPannelResized(details.delta.dx)); + context + .read() + .add(HomeEvent.editPannelResized(details.delta.dx)); }), behavior: HitTestBehavior.translucent, child: SizedBox( @@ -186,11 +204,21 @@ class _HomeScreenState extends State { closeX: -layout.menuWidth, isClosed: !layout.showMenu, ) - .positioned(left: 0, top: 0, width: layout.menuWidth, bottom: 0, animate: true) + .positioned( + left: 0, + top: 0, + width: layout.menuWidth, + bottom: 0, + animate: true) .animate(layout.animDuration, Curves.easeOut), homeStack .constrained(minWidth: 500) - .positioned(left: layout.homePageLOffset, right: layout.homePageROffset, bottom: 0, top: 0, animate: true) + .positioned( + left: layout.homePageLOffset, + right: layout.homePageROffset, + bottom: 0, + top: 0, + animate: true) .animate(layout.animDuration, Curves.easeOut), homeMenuResizer.positioned(left: layout.homePageLOffset - 5), bubble @@ -206,7 +234,8 @@ class _HomeScreenState extends State { closeX: layout.editPannelWidth, isClosed: !layout.showEditPannel, ) - .positioned(right: 0, top: 0, bottom: 0, width: layout.editPannelWidth), + .positioned( + right: 0, top: 0, bottom: 0, width: layout.editPannelWidth), ], ); } diff --git a/frontend/app_flowy/lib/workspace/presentation/home/hotkeys.dart b/frontend/app_flowy/lib/workspace/presentation/home/hotkeys.dart new file mode 100644 index 0000000000..f082dd2d7f --- /dev/null +++ b/frontend/app_flowy/lib/workspace/presentation/home/hotkeys.dart @@ -0,0 +1,32 @@ +import 'dart:io'; + +import 'package:app_flowy/startup/startup.dart'; +import 'package:app_flowy/workspace/application/home/home_bloc.dart'; +import 'package:app_flowy/workspace/presentation/home/home_stack.dart'; +import 'package:flutter/material.dart'; +import 'package:hotkey_manager/hotkey_manager.dart'; +import 'package:provider/provider.dart'; + +class HomeHotKeys extends StatelessWidget { + final Widget child; + const HomeHotKeys({required this.child}); + + @override + Widget build(BuildContext context) { + HotKey _hotKey = HotKey( + KeyCode.backslash, + modifiers: [Platform.isMacOS ? KeyModifier.meta : KeyModifier.control], + // Set hotkey scope (default is HotKeyScope.system) + scope: HotKeyScope.inapp, // Set as inapp-wide hotkey. + ); + hotKeyManager.register( + _hotKey, + keyDownHandler: (hotKey) { + context.read().add(const HomeEvent.collapseMenu()); + getIt().collapsedNotifier.value = + !getIt().collapsedNotifier.currentValue!; + }, + ); + return child; + } +} diff --git a/frontend/app_flowy/lib/workspace/presentation/home/menu/menu.dart b/frontend/app_flowy/lib/workspace/presentation/home/menu/menu.dart index 3976598b7b..09370176bb 100644 --- a/frontend/app_flowy/lib/workspace/presentation/home/menu/menu.dart +++ b/frontend/app_flowy/lib/workspace/presentation/home/menu/menu.dart @@ -49,7 +49,8 @@ class HomeMenu extends StatelessWidget { providers: [ BlocProvider( create: (context) { - final menuBloc = getIt(param1: user, param2: workspaceSetting.workspace.id); + final menuBloc = getIt( + param1: user, param2: workspaceSetting.workspace.id); menuBloc.add(const MenuEvent.initial()); return menuBloc; }, @@ -106,18 +107,22 @@ class HomeMenu extends StatelessWidget { Widget _renderApps(BuildContext context) { return ExpandableTheme( - data: ExpandableThemeData(useInkWell: true, animationDuration: Durations.medium), + data: ExpandableThemeData( + useInkWell: true, animationDuration: Durations.medium), child: Expanded( child: ScrollConfiguration( behavior: const ScrollBehavior().copyWith(scrollbars: false), child: BlocSelector>( - selector: (state) => state.apps.map((app) => MenuApp(app, key: ValueKey(app.id))).toList(), + selector: (state) => state.apps + .map((app) => MenuApp(app, key: ValueKey(app.id))) + .toList(), builder: (context, menuItems) { return ReorderableListView.builder( itemCount: menuItems.length, buildDefaultDragHandles: false, header: Padding( - padding: EdgeInsets.only(bottom: 20.0 - MenuAppSizes.appVPadding), + padding: + EdgeInsets.only(bottom: 20.0 - MenuAppSizes.appVPadding), child: MenuUser(user), ), onReorder: (oldIndex, newIndex) { @@ -126,7 +131,9 @@ class HomeMenu extends StatelessWidget { // receive: oldIndex: 0, newIndex: 2 // Workaround: if newIndex > oldIndex, we just minus one int index = newIndex > oldIndex ? newIndex - 1 : newIndex; - context.read().add(MenuEvent.moveApp(oldIndex, index)); + context + .read() + .add(MenuEvent.moveApp(oldIndex, index)); }, physics: StyledScrollPhysics(), itemBuilder: (BuildContext context, int index) { @@ -134,7 +141,8 @@ class HomeMenu extends StatelessWidget { key: ValueKey(menuItems[index].key), index: index, child: Padding( - padding: EdgeInsets.symmetric(vertical: MenuAppSizes.appVPadding / 2), + padding: EdgeInsets.symmetric( + vertical: MenuAppSizes.appVPadding / 2), child: menuItems[index], ), ); @@ -149,7 +157,8 @@ class HomeMenu extends StatelessWidget { Widget _renderNewAppButton(BuildContext context) { return NewAppButton( - press: (appName) => context.read().add(MenuEvent.createApp(appName, desc: "")), + press: (appName) => + context.read().add(MenuEvent.createApp(appName, desc: "")), ); } } @@ -208,12 +217,22 @@ class MenuTopBar extends StatelessWidget { children: [ renderIcon(context), const Spacer(), - FlowyIconButton( - width: 28, - onPressed: () => context.read().add(const HomeEvent.collapseMenu()), - iconPadding: const EdgeInsets.fromLTRB(4, 4, 4, 4), - icon: svgWidget("home/hide_menu", color: theme.iconColor), - ) + Tooltip( + richMessage: TextSpan(children: [ + const TextSpan(text: "Close sidebar\n"), + TextSpan( + text: Platform.isMacOS ? "⌘+\\" : "Ctrl+\\", + style: const TextStyle(color: Colors.white60), + ), + ]), + child: FlowyIconButton( + width: 28, + onPressed: () => context + .read() + .add(const HomeEvent.collapseMenu()), + iconPadding: const EdgeInsets.fromLTRB(4, 4, 4, 4), + icon: svgWidget("home/hide_menu", color: theme.iconColor), + )) ], )), ); diff --git a/frontend/app_flowy/lib/workspace/presentation/home/navigation.dart b/frontend/app_flowy/lib/workspace/presentation/home/navigation.dart index f52b7224f6..4224fd6d18 100644 --- a/frontend/app_flowy/lib/workspace/presentation/home/navigation.dart +++ b/frontend/app_flowy/lib/workspace/presentation/home/navigation.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:app_flowy/workspace/application/home/home_bloc.dart'; import 'package:app_flowy/workspace/presentation/home/home_stack.dart'; import 'package:flowy_infra/image.dart'; @@ -14,7 +16,8 @@ typedef NaviAction = void Function(); class NavigationNotifier with ChangeNotifier { List navigationItems; PublishNotifier collapasedNotifier; - NavigationNotifier({required this.navigationItems, required this.collapasedNotifier}); + NavigationNotifier( + {required this.navigationItems, required this.collapasedNotifier}); void update(HomeStackNotifier notifier) { bool shouldNotify = false; @@ -69,7 +72,8 @@ class FlowyNavigation extends StatelessWidget { child: Row(children: [ Selector>( selector: (context, notifier) => notifier.collapasedNotifier, - builder: (ctx, collapsedNotifier, child) => _renderCollapse(ctx, collapsedNotifier, theme)), + builder: (ctx, collapsedNotifier, child) => + _renderCollapse(ctx, collapsedNotifier, theme)), Selector>( selector: (context, notifier) => notifier.navigationItems, builder: (ctx, items, child) => Expanded( @@ -84,7 +88,8 @@ class FlowyNavigation extends StatelessWidget { ); } - Widget _renderCollapse(BuildContext context, PublishNotifier collapsedNotifier, AppTheme theme) { + Widget _renderCollapse(BuildContext context, + PublishNotifier collapsedNotifier, AppTheme theme) { return ChangeNotifierProvider.value( value: collapsedNotifier, child: Consumer( @@ -92,15 +97,23 @@ class FlowyNavigation extends StatelessWidget { if (notifier.currentValue ?? false) { return RotationTransition( turns: const AlwaysStoppedAnimation(180 / 360), - child: FlowyIconButton( - width: 24, - onPressed: () { - notifier.value = false; - ctx.read().add(const HomeEvent.collapseMenu()); - }, - iconPadding: const EdgeInsets.fromLTRB(2, 2, 2, 2), - icon: svgWidget("home/hide_menu", color: theme.iconColor), - ), + child: Tooltip( + richMessage: TextSpan(children: [ + const TextSpan(text: "Open sidebar\n"), + TextSpan( + text: Platform.isMacOS ? "⌘+\\" : "Ctrl+\\", + style: const TextStyle(color: Colors.white60), + ), + ]), + child: FlowyIconButton( + width: 24, + onPressed: () { + notifier.value = false; + ctx.read().add(const HomeEvent.collapseMenu()); + }, + iconPadding: const EdgeInsets.fromLTRB(2, 2, 2, 2), + icon: svgWidget("home/hide_menu", color: theme.iconColor), + )), ); } else { return Container(); @@ -154,7 +167,8 @@ class NaviItemWidget extends StatelessWidget { @override Widget build(BuildContext context) { - return Expanded(child: item.leftBarItem.padding(horizontal: 2, vertical: 2)); + return Expanded( + child: item.leftBarItem.padding(horizontal: 2, vertical: 2)); } } diff --git a/frontend/app_flowy/pubspec.lock b/frontend/app_flowy/pubspec.lock index 505280115f..e0d5e46770 100644 --- a/frontend/app_flowy/pubspec.lock +++ b/frontend/app_flowy/pubspec.lock @@ -555,6 +555,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.0" + hotkey_manager: + dependency: "direct main" + description: + name: hotkey_manager + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.7" html: dependency: transitive description: diff --git a/frontend/app_flowy/pubspec.yaml b/frontend/app_flowy/pubspec.yaml index 5f4ff9c9b8..623d5df900 100644 --- a/frontend/app_flowy/pubspec.yaml +++ b/frontend/app_flowy/pubspec.yaml @@ -76,6 +76,7 @@ dependencies: table_calendar: ^3.0.5 reorderables: ^0.5.0 linked_scroll_controller: ^0.2.0 + hotkey_manager: ^0.1.7 dev_dependencies: flutter_lints: ^1.0.0 From 1c19bee773d4774c8b33a698ba20989198a61dbf Mon Sep 17 00:00:00 2001 From: Naughtz Date: Mon, 8 Aug 2022 17:12:27 +0800 Subject: [PATCH 003/106] fix: fix actions fail --- .../app_flowy/lib/workspace/presentation/home/hotkeys.dart | 2 +- frontend/scripts/install_dev_env/install_linux.sh | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/frontend/app_flowy/lib/workspace/presentation/home/hotkeys.dart b/frontend/app_flowy/lib/workspace/presentation/home/hotkeys.dart index f082dd2d7f..32c2bae7fe 100644 --- a/frontend/app_flowy/lib/workspace/presentation/home/hotkeys.dart +++ b/frontend/app_flowy/lib/workspace/presentation/home/hotkeys.dart @@ -9,7 +9,7 @@ import 'package:provider/provider.dart'; class HomeHotKeys extends StatelessWidget { final Widget child; - const HomeHotKeys({required this.child}); + const HomeHotKeys({required this.child, Key? key}) : super(key: key); @override Widget build(BuildContext context) { diff --git a/frontend/scripts/install_dev_env/install_linux.sh b/frontend/scripts/install_dev_env/install_linux.sh index 396a66cd63..94a8234a26 100755 --- a/frontend/scripts/install_dev_env/install_linux.sh +++ b/frontend/scripts/install_dev_env/install_linux.sh @@ -46,6 +46,9 @@ flutter config --enable-linux-desktop # Fix any problems reported by flutter doctor flutter doctor +# install keybinder-3.0 +apt-get install keybinder-3.0 + # Add the githooks directory to your git configuration printMessage "Setting up githooks." git config core.hooksPath .githooks From 799d2c57ab28f60737f0b70f59c09307d913533b Mon Sep 17 00:00:00 2001 From: Naughtz Date: Tue, 9 Aug 2022 10:39:33 +0800 Subject: [PATCH 004/106] Update ci.yaml --- .github/workflows/ci.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index dbbd729c81..b0d97658e4 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -58,6 +58,7 @@ jobs: sudo wget -qO /etc/apt/sources.list.d/dart_stable.list https://storage.googleapis.com/download.dartlang.org/linux/debian/dart_stable.list sudo apt-get update sudo apt-get install -y dart curl build-essential libsqlite3-dev libssl-dev clang cmake ninja-build pkg-config libgtk-3-dev + sudo apt-get install keybinder-3.0 elif [ "$RUNNER_OS" == "macOS" ]; then echo 'do nothing' fi From fd57b0dc3635154acb5a2577e7d0600dd20b0517 Mon Sep 17 00:00:00 2001 From: Naughtz Date: Tue, 9 Aug 2022 10:39:33 +0800 Subject: [PATCH 005/106] feat: install keybinder-3.0 on ubuntu --- .github/workflows/ci.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index dbbd729c81..b0d97658e4 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -58,6 +58,7 @@ jobs: sudo wget -qO /etc/apt/sources.list.d/dart_stable.list https://storage.googleapis.com/download.dartlang.org/linux/debian/dart_stable.list sudo apt-get update sudo apt-get install -y dart curl build-essential libsqlite3-dev libssl-dev clang cmake ninja-build pkg-config libgtk-3-dev + sudo apt-get install keybinder-3.0 elif [ "$RUNNER_OS" == "macOS" ]; then echo 'do nothing' fi From 22f544a9100fc4af7360eab79535858c78fa8a14 Mon Sep 17 00:00:00 2001 From: Naughtz Date: Mon, 22 Aug 2022 11:03:36 +0800 Subject: [PATCH 006/106] feat: add translation --- frontend/app_flowy/assets/translations/ca-ES.json | 4 ++++ frontend/app_flowy/assets/translations/de-DE.json | 4 ++++ frontend/app_flowy/assets/translations/en.json | 4 ++++ frontend/app_flowy/assets/translations/es-VE.json | 4 ++++ frontend/app_flowy/assets/translations/fr-CA.json | 4 ++++ frontend/app_flowy/assets/translations/fr-FR.json | 4 ++++ frontend/app_flowy/assets/translations/hu-HU.json | 4 ++++ frontend/app_flowy/assets/translations/id-ID.json | 4 ++++ frontend/app_flowy/assets/translations/it-IT.json | 4 ++++ frontend/app_flowy/assets/translations/ja-JP.json | 4 ++++ frontend/app_flowy/assets/translations/pl-PL.json | 4 ++++ frontend/app_flowy/assets/translations/pt-BR.json | 4 ++++ frontend/app_flowy/assets/translations/pt-PT.json | 4 ++++ frontend/app_flowy/assets/translations/ru-RU.json | 4 ++++ frontend/app_flowy/assets/translations/tr-TR.json | 4 ++++ frontend/app_flowy/assets/translations/zh-CN.json | 4 ++++ frontend/app_flowy/assets/translations/zh-TW.json | 4 ++++ .../app_flowy/lib/workspace/presentation/home/menu/menu.dart | 4 +++- .../app_flowy/lib/workspace/presentation/home/navigation.dart | 3 ++- 19 files changed, 73 insertions(+), 2 deletions(-) diff --git a/frontend/app_flowy/assets/translations/ca-ES.json b/frontend/app_flowy/assets/translations/ca-ES.json index d08639f5cf..23ae51bd69 100644 --- a/frontend/app_flowy/assets/translations/ca-ES.json +++ b/frontend/app_flowy/assets/translations/ca-ES.json @@ -141,5 +141,9 @@ "lightLabel": "Mode Clar", "darkLabel": "Mode Fosc" } + }, + "sideBar": { + "openSidebar": "Open sidebar", + "closeSidebar": "Close sidebar" } } diff --git a/frontend/app_flowy/assets/translations/de-DE.json b/frontend/app_flowy/assets/translations/de-DE.json index 5e1e5cd901..6ce661e7b9 100644 --- a/frontend/app_flowy/assets/translations/de-DE.json +++ b/frontend/app_flowy/assets/translations/de-DE.json @@ -141,6 +141,10 @@ "lightLabel": "Heller Modus", "darkLabel": "Dunkler Modus" } + }, + "sideBar": { + "openSidebar": "Open sidebar", + "closeSidebar": "Close sidebar" } } \ No newline at end of file diff --git a/frontend/app_flowy/assets/translations/en.json b/frontend/app_flowy/assets/translations/en.json index 2dbb970611..eb7ee15f49 100644 --- a/frontend/app_flowy/assets/translations/en.json +++ b/frontend/app_flowy/assets/translations/en.json @@ -214,5 +214,9 @@ "timeHintTextInTwelveHour": "12:00 AM", "timeHintTextInTwentyFourHour": "12:00" } + }, + "sideBar": { + "openSidebar": "Open sidebar", + "closeSidebar": "Close sidebar" } } \ No newline at end of file diff --git a/frontend/app_flowy/assets/translations/es-VE.json b/frontend/app_flowy/assets/translations/es-VE.json index d3740db8ec..cf78b8083d 100644 --- a/frontend/app_flowy/assets/translations/es-VE.json +++ b/frontend/app_flowy/assets/translations/es-VE.json @@ -213,5 +213,9 @@ "timeHintTextInTwelveHour": "12:00 AM", "timeHintTextInTwentyFourHour": "12:00" } + }, + "sideBar": { + "openSidebar": "Open sidebar", + "closeSidebar": "Close sidebar" } } diff --git a/frontend/app_flowy/assets/translations/fr-CA.json b/frontend/app_flowy/assets/translations/fr-CA.json index 329576028b..ff7f5ca251 100644 --- a/frontend/app_flowy/assets/translations/fr-CA.json +++ b/frontend/app_flowy/assets/translations/fr-CA.json @@ -141,5 +141,9 @@ "lightLabel": "Mode clair", "darkLabel": "Mode sombre" } + }, + "sideBar": { + "openSidebar": "Open sidebar", + "closeSidebar": "Close sidebar" } } diff --git a/frontend/app_flowy/assets/translations/fr-FR.json b/frontend/app_flowy/assets/translations/fr-FR.json index ed1d56bce8..773ae93e9f 100644 --- a/frontend/app_flowy/assets/translations/fr-FR.json +++ b/frontend/app_flowy/assets/translations/fr-FR.json @@ -141,5 +141,9 @@ "lightLabel": "Mode clair", "darkLabel": "Mode sombre" } + }, + "sideBar": { + "openSidebar": "Open sidebar", + "closeSidebar": "Close sidebar" } } diff --git a/frontend/app_flowy/assets/translations/hu-HU.json b/frontend/app_flowy/assets/translations/hu-HU.json index a2f99c55e1..7428f48520 100644 --- a/frontend/app_flowy/assets/translations/hu-HU.json +++ b/frontend/app_flowy/assets/translations/hu-HU.json @@ -141,5 +141,9 @@ "lightLabel": "Világos mód", "darkLabel": "Éjjeli mód" } + }, + "sideBar": { + "openSidebar": "Open sidebar", + "closeSidebar": "Close sidebar" } } diff --git a/frontend/app_flowy/assets/translations/id-ID.json b/frontend/app_flowy/assets/translations/id-ID.json index cbe721c2b8..d1c708721d 100644 --- a/frontend/app_flowy/assets/translations/id-ID.json +++ b/frontend/app_flowy/assets/translations/id-ID.json @@ -214,5 +214,9 @@ "timeHintTextInTwelveHour": "12:00 AM", "timeHintTextInTwentyFourHour": "12:00" } + }, + "sideBar": { + "openSidebar": "Open sidebar", + "closeSidebar": "Close sidebar" } } \ No newline at end of file diff --git a/frontend/app_flowy/assets/translations/it-IT.json b/frontend/app_flowy/assets/translations/it-IT.json index 9a546076c5..3433eb4d8e 100644 --- a/frontend/app_flowy/assets/translations/it-IT.json +++ b/frontend/app_flowy/assets/translations/it-IT.json @@ -147,5 +147,9 @@ }, "document":{ "menuName":"Documento" + }, + "sideBar": { + "openSidebar": "Open sidebar", + "closeSidebar": "Close sidebar" } } diff --git a/frontend/app_flowy/assets/translations/ja-JP.json b/frontend/app_flowy/assets/translations/ja-JP.json index aa726a0410..04409a82a2 100644 --- a/frontend/app_flowy/assets/translations/ja-JP.json +++ b/frontend/app_flowy/assets/translations/ja-JP.json @@ -195,5 +195,9 @@ "pannelTitle": "選択候補を検索 または 作成する", "searchOption": "選択候補を検索" } + }, + "sideBar": { + "openSidebar": "Open sidebar", + "closeSidebar": "Close sidebar" } } \ No newline at end of file diff --git a/frontend/app_flowy/assets/translations/pl-PL.json b/frontend/app_flowy/assets/translations/pl-PL.json index 0105e7aec7..ba6c4e1861 100644 --- a/frontend/app_flowy/assets/translations/pl-PL.json +++ b/frontend/app_flowy/assets/translations/pl-PL.json @@ -141,5 +141,9 @@ "lightLabel": "Tryb Jasny", "darkLabel": "Tryb Ciemny" } + }, + "sideBar": { + "openSidebar": "Open sidebar", + "closeSidebar": "Close sidebar" } } diff --git a/frontend/app_flowy/assets/translations/pt-BR.json b/frontend/app_flowy/assets/translations/pt-BR.json index 8ae5818b60..8e3fba0362 100644 --- a/frontend/app_flowy/assets/translations/pt-BR.json +++ b/frontend/app_flowy/assets/translations/pt-BR.json @@ -141,6 +141,10 @@ "lightLabel": "Modo Claro", "darkLabel": "Modo Escuro" } + }, + "sideBar": { + "openSidebar": "Open sidebar", + "closeSidebar": "Close sidebar" } } diff --git a/frontend/app_flowy/assets/translations/pt-PT.json b/frontend/app_flowy/assets/translations/pt-PT.json index 3aa37ee230..a9291949bd 100644 --- a/frontend/app_flowy/assets/translations/pt-PT.json +++ b/frontend/app_flowy/assets/translations/pt-PT.json @@ -141,6 +141,10 @@ "lightLabel": "Modo Claro", "darkLabel": "Modo Escuro" } + }, + "sideBar": { + "openSidebar": "Open sidebar", + "closeSidebar": "Close sidebar" } } diff --git a/frontend/app_flowy/assets/translations/ru-RU.json b/frontend/app_flowy/assets/translations/ru-RU.json index 65e61347e6..8be0e0a07f 100644 --- a/frontend/app_flowy/assets/translations/ru-RU.json +++ b/frontend/app_flowy/assets/translations/ru-RU.json @@ -203,6 +203,10 @@ "timeHintTextInTwelveHour": "12:00 AM", "timeHintTextInTwentyFourHour": "12:00" } + }, + "sideBar": { + "openSidebar": "Open sidebar", + "closeSidebar": "Close sidebar" } } \ No newline at end of file diff --git a/frontend/app_flowy/assets/translations/tr-TR.json b/frontend/app_flowy/assets/translations/tr-TR.json index c83cace0a7..aa2b1a3a39 100644 --- a/frontend/app_flowy/assets/translations/tr-TR.json +++ b/frontend/app_flowy/assets/translations/tr-TR.json @@ -141,5 +141,9 @@ "lightLabel": "Aydınlık Mod", "darkLabel": "Karanlık Mod" } + }, + "sideBar": { + "openSidebar": "Open sidebar", + "closeSidebar": "Close sidebar" } } diff --git a/frontend/app_flowy/assets/translations/zh-CN.json b/frontend/app_flowy/assets/translations/zh-CN.json index 81b0ad13b1..7304610714 100644 --- a/frontend/app_flowy/assets/translations/zh-CN.json +++ b/frontend/app_flowy/assets/translations/zh-CN.json @@ -141,5 +141,9 @@ "lightLabel": "日间模式", "darkLabel": "夜间模式" } + }, + "sideBar": { + "openSidebar": "打开侧边栏", + "closeSidebar": "关闭侧边栏" } } diff --git a/frontend/app_flowy/assets/translations/zh-TW.json b/frontend/app_flowy/assets/translations/zh-TW.json index 9f72b90512..53a9347126 100644 --- a/frontend/app_flowy/assets/translations/zh-TW.json +++ b/frontend/app_flowy/assets/translations/zh-TW.json @@ -214,5 +214,9 @@ "timeHintTextInTwelveHour": "12:00 AM", "timeHintTextInTwentyFourHour": "12:00" } + }, + "sideBar": { + "openSidebar": "Open sidebar", + "closeSidebar": "Close sidebar" } } \ No newline at end of file diff --git a/frontend/app_flowy/lib/workspace/presentation/home/menu/menu.dart b/frontend/app_flowy/lib/workspace/presentation/home/menu/menu.dart index 09370176bb..0aa714c07e 100644 --- a/frontend/app_flowy/lib/workspace/presentation/home/menu/menu.dart +++ b/frontend/app_flowy/lib/workspace/presentation/home/menu/menu.dart @@ -2,6 +2,7 @@ export './app/header/header.dart'; export './app/menu_app.dart'; import 'dart:io' show Platform; +import 'package:app_flowy/generated/locale_keys.g.dart'; import 'package:app_flowy/workspace/presentation/home/home_sizes.dart'; import 'package:app_flowy/workspace/presentation/home/home_stack.dart'; import 'package:app_flowy/workspace/presentation/plugins/trash/menu.dart'; @@ -219,7 +220,8 @@ class MenuTopBar extends StatelessWidget { const Spacer(), Tooltip( richMessage: TextSpan(children: [ - const TextSpan(text: "Close sidebar\n"), + const TextSpan( + text: LocaleKeys.sideBar_closeSidebar + "\n"), TextSpan( text: Platform.isMacOS ? "⌘+\\" : "Ctrl+\\", style: const TextStyle(color: Colors.white60), diff --git a/frontend/app_flowy/lib/workspace/presentation/home/navigation.dart b/frontend/app_flowy/lib/workspace/presentation/home/navigation.dart index 4224fd6d18..9513a20ccc 100644 --- a/frontend/app_flowy/lib/workspace/presentation/home/navigation.dart +++ b/frontend/app_flowy/lib/workspace/presentation/home/navigation.dart @@ -1,5 +1,6 @@ import 'dart:io'; +import 'package:app_flowy/generated/locale_keys.g.dart'; import 'package:app_flowy/workspace/application/home/home_bloc.dart'; import 'package:app_flowy/workspace/presentation/home/home_stack.dart'; import 'package:flowy_infra/image.dart'; @@ -99,7 +100,7 @@ class FlowyNavigation extends StatelessWidget { turns: const AlwaysStoppedAnimation(180 / 360), child: Tooltip( richMessage: TextSpan(children: [ - const TextSpan(text: "Open sidebar\n"), + const TextSpan(text: LocaleKeys.sideBar_openSidebar + "\n"), TextSpan( text: Platform.isMacOS ? "⌘+\\" : "Ctrl+\\", style: const TextStyle(color: Colors.white60), From f943aeacd7dd97162c7b6365ed744e3eef5b6c34 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Fri, 19 Aug 2022 23:22:59 +0800 Subject: [PATCH 007/106] chore: improve cursor display style --- .../src/render/rich_text/flowy_rich_text.dart | 31 ++++++++++++++----- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/flowy_rich_text.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/flowy_rich_text.dart index fb4dbdc4a1..89df8a54b1 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/flowy_rich_text.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/flowy_rich_text.dart @@ -65,18 +65,35 @@ class _FlowyRichTextState extends State with Selectable { @override Rect? getCursorRectInPosition(Position position) { final textPosition = TextPosition(offset: position.offset); - final cursorOffset = - _renderParagraph.getOffsetForCaret(textPosition, Rect.zero); - final cursorHeight = widget.cursorHeight ?? - _renderParagraph.getFullHeightForCaret(textPosition) ?? - _placeholderRenderParagraph.getFullHeightForCaret(textPosition) ?? - 16.0; // default height + var cursorHeight = _renderParagraph.getFullHeightForCaret(textPosition); + var cursorOffset = + _renderParagraph.getOffsetForCaret(textPosition, Rect.zero); + if (cursorHeight == null) { + cursorHeight = + _placeholderRenderParagraph.getFullHeightForCaret(textPosition); + cursorOffset = _placeholderRenderParagraph.getOffsetForCaret( + textPosition, Rect.zero); + } + if (cursorHeight != null) { + // workaround: Calling the `getFullHeightForCaret` function will return + // the full height of rich text component instead of the plain text + // if we set the line height. + // So need to divide by the line height to get the expected value. + // + // And the default height of plain text is too short. Add a magic height + // to expand it. + const magicHeight = 3.0; + cursorOffset = cursorOffset.translate( + 0, (cursorHeight - cursorHeight / _lineHeight) / 2.0); + cursorHeight /= _lineHeight; + cursorHeight += magicHeight; + } final rect = Rect.fromLTWH( cursorOffset.dx - (widget.cursorWidth / 2), cursorOffset.dy, widget.cursorWidth, - cursorHeight, + widget.cursorHeight ?? cursorHeight ?? 16.0, ); return rect; } From 1f90f3027430f6f4f5e3a07ebef69ff419362d76 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Mon, 22 Aug 2022 17:34:16 +0800 Subject: [PATCH 008/106] feat: implement link menu --- .../appflowy_editor/assets/images/delete.svg | 6 + .../appflowy_editor/assets/images/link.svg | 4 + .../assets/images/toolbar/link.svg | 4 +- .../src/extensions/text_node_extensions.dart | 25 ++ .../lib/src/render/link_menu/link_menu.dart | 137 +++++++++++ .../src/render/selection/toolbar_widget.dart | 217 ------------------ .../lib/src/render/toolbar/toolbar_item.dart | 192 ++++++++++++++++ .../render/toolbar/toolbar_item_widget.dart | 35 +++ .../src/render/toolbar/toolbar_widget.dart | 82 +++++++ .../lib/src/service/input_service.dart | 21 +- .../lib/src/service/toolbar_service.dart | 5 +- 11 files changed, 498 insertions(+), 230 deletions(-) create mode 100644 frontend/app_flowy/packages/appflowy_editor/assets/images/delete.svg create mode 100644 frontend/app_flowy/packages/appflowy_editor/assets/images/link.svg create mode 100644 frontend/app_flowy/packages/appflowy_editor/lib/src/render/link_menu/link_menu.dart delete mode 100644 frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection/toolbar_widget.dart create mode 100644 frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item.dart create mode 100644 frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item_widget.dart create mode 100644 frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_widget.dart diff --git a/frontend/app_flowy/packages/appflowy_editor/assets/images/delete.svg b/frontend/app_flowy/packages/appflowy_editor/assets/images/delete.svg new file mode 100644 index 0000000000..b7f242542d --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/assets/images/delete.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/frontend/app_flowy/packages/appflowy_editor/assets/images/link.svg b/frontend/app_flowy/packages/appflowy_editor/assets/images/link.svg new file mode 100644 index 0000000000..5fbcc8d787 --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/assets/images/link.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/app_flowy/packages/appflowy_editor/assets/images/toolbar/link.svg b/frontend/app_flowy/packages/appflowy_editor/assets/images/toolbar/link.svg index 612e8377b6..279e7ac471 100644 --- a/frontend/app_flowy/packages/appflowy_editor/assets/images/toolbar/link.svg +++ b/frontend/app_flowy/packages/appflowy_editor/assets/images/toolbar/link.svg @@ -1,4 +1,4 @@ - - + + diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/text_node_extensions.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/text_node_extensions.dart index 1d7c68ab80..8416485a5a 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/text_node_extensions.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/text_node_extensions.dart @@ -6,6 +6,31 @@ import 'package:appflowy_editor/src/document/text_delta.dart'; import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart'; extension TextNodeExtension on TextNode { + dynamic getAttributeInSelection(Selection selection, String styleKey) { + final ops = delta.whereType(); + final startOffset = + selection.isBackward ? selection.start.offset : selection.end.offset; + final endOffset = + selection.isBackward ? selection.end.offset : selection.start.offset; + var start = 0; + for (final op in ops) { + if (start >= endOffset) { + break; + } + final length = op.length; + if (start < endOffset && start + length > startOffset) { + if (op.attributes?.containsKey(styleKey) == true) { + return op.attributes![styleKey]; + } + } + start += length; + } + return null; + } + + bool allSatisfyLinkInSelection(Selection selection) => + allSatisfyInSelection(StyleKey.href, null, selection); + bool allSatisfyBoldInSelection(Selection selection) => allSatisfyInSelection(StyleKey.bold, true, selection); diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/link_menu/link_menu.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/link_menu/link_menu.dart new file mode 100644 index 0000000000..3ebf87f4fa --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/link_menu/link_menu.dart @@ -0,0 +1,137 @@ +import 'package:appflowy_editor/src/infra/flowy_svg.dart'; +import 'package:flutter/material.dart'; + +class LinkMenu extends StatefulWidget { + const LinkMenu({ + Key? key, + this.linkText, + required this.onSubmitted, + required this.onCopyLink, + required this.onRemoveLink, + }) : super(key: key); + + final String? linkText; + final void Function(String text) onSubmitted; + final VoidCallback onCopyLink; + final VoidCallback onRemoveLink; + + @override + State createState() => _LinkMenuState(); +} + +class _LinkMenuState extends State { + final _textEditingController = TextEditingController(); + + @override + void initState() { + super.initState(); + + _textEditingController.text = widget.linkText ?? ''; + } + + @override + Widget build(BuildContext context) { + return SizedBox( + width: 350, + height: 200, + child: Container( + decoration: BoxDecoration( + color: Colors.white, + boxShadow: [ + BoxShadow( + blurRadius: 5, + spreadRadius: 1, + color: Colors.black.withOpacity(0.1), + ), + ], + borderRadius: BorderRadius.circular(6.0), + ), + child: SizedBox( + width: 350, + height: 200, + child: Padding( + padding: const EdgeInsets.all(20.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildHeader(), + const SizedBox(height: 16.0), + _buildInput(), + const SizedBox(height: 16.0), + _buildIconButton( + iconName: 'link', + text: 'Copy link', + onPressed: widget.onCopyLink, + ), + _buildIconButton( + iconName: 'delete', + text: 'Remove link', + onPressed: widget.onRemoveLink, + ), + ], + ), + ), + ), + ), + ); + } + + Widget _buildHeader() { + return const Text( + 'Add your link', + style: TextStyle( + color: Colors.grey, + fontWeight: FontWeight.bold, + ), + ); + } + + Widget _buildInput() { + return TextField( + autofocus: true, + style: const TextStyle(fontSize: 14.0), + textAlign: TextAlign.left, + controller: _textEditingController, + onSubmitted: widget.onSubmitted, + decoration: const InputDecoration( + hintText: 'URL', + hintStyle: TextStyle(fontSize: 14.0), + border: OutlineInputBorder( + borderRadius: BorderRadius.all(Radius.circular(12.0)), + borderSide: BorderSide(color: Color(0xFFBDBDBD)), + ), + contentPadding: EdgeInsets.all(16.0), + isDense: true, + ), + ); + } + + Widget _buildIconButton({ + required String iconName, + required String text, + required VoidCallback onPressed, + }) { + return TextButton.icon( + icon: FlowySvg( + name: iconName, + width: 20.0, + height: 20.0, + ), + style: TextButton.styleFrom( + minimumSize: const Size.fromHeight(40), + padding: EdgeInsets.zero, + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + alignment: Alignment.centerLeft, + ), + label: Text( + text, + textAlign: TextAlign.left, + style: const TextStyle( + color: Colors.black, + fontSize: 14.0, + ), + ), + onPressed: onPressed, + ); + } +} diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection/toolbar_widget.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection/toolbar_widget.dart deleted file mode 100644 index 4c2b621795..0000000000 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection/toolbar_widget.dart +++ /dev/null @@ -1,217 +0,0 @@ -import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart'; -import 'package:flutter/material.dart'; - -import 'package:appflowy_editor/src/editor_state.dart'; -import 'package:appflowy_editor/src/infra/flowy_svg.dart'; -import 'package:appflowy_editor/src/service/default_text_operations/format_rich_text_style.dart'; - -typedef ToolbarEventHandler = void Function(EditorState editorState); - -typedef ToolbarEventHandlers = Map; - -ToolbarEventHandlers defaultToolbarEventHandlers = { - 'bold': (editorState) => formatBold(editorState), - 'italic': (editorState) => formatItalic(editorState), - 'strikethrough': (editorState) => formatStrikethrough(editorState), - 'underline': (editorState) => formatUnderline(editorState), - 'quote': (editorState) => formatQuote(editorState), - 'bulleted_list': (editorState) => formatBulletedList(editorState), - 'highlight': (editorState) => formatHighlight(editorState), - 'Text': (editorState) => formatText(editorState), - 'h1': (editorState) => formatHeading(editorState, StyleKey.h1), - 'h2': (editorState) => formatHeading(editorState, StyleKey.h2), - 'h3': (editorState) => formatHeading(editorState, StyleKey.h3), -}; - -List defaultListToolbarEventNames = [ - 'Text', - 'H1', - 'H2', - 'H3', -]; - -mixin ToolbarMixin on State { - void hide(); -} - -class ToolbarWidget extends StatefulWidget { - const ToolbarWidget({ - Key? key, - required this.editorState, - required this.layerLink, - required this.offset, - required this.handlers, - }) : super(key: key); - - final EditorState editorState; - final LayerLink layerLink; - final Offset offset; - final ToolbarEventHandlers handlers; - - @override - State createState() => _ToolbarWidgetState(); -} - -class _ToolbarWidgetState extends State with ToolbarMixin { - // final GlobalKey _listToolbarKey = GlobalKey(); - - final toolbarHeight = 32.0; - final topPadding = 5.0; - - final listToolbarWidth = 60.0; - final listToolbarHeight = 120.0; - - final cornerRadius = 8.0; - - OverlayEntry? _listToolbarOverlay; - - @override - Widget build(BuildContext context) { - return Positioned( - top: widget.offset.dx, - left: widget.offset.dy, - child: CompositedTransformFollower( - link: widget.layerLink, - showWhenUnlinked: true, - offset: widget.offset, - child: _buildToolbar(context), - ), - ); - } - - @override - void hide() { - _listToolbarOverlay?.remove(); - _listToolbarOverlay = null; - } - - Widget _buildToolbar(BuildContext context) { - return Material( - borderRadius: BorderRadius.circular(cornerRadius), - color: const Color(0xFF333333), - child: SizedBox( - height: toolbarHeight, - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // _listToolbar(context), - _centerToolbarIcon('h1', tooltipMessage: 'Heading 1'), - _centerToolbarIcon('h2', tooltipMessage: 'Heading 2'), - _centerToolbarIcon('h3', tooltipMessage: 'Heading 3'), - _centerToolbarIcon('divider', width: 2), - _centerToolbarIcon('bold', tooltipMessage: 'Bold'), - _centerToolbarIcon('italic', tooltipMessage: 'Italic'), - _centerToolbarIcon('strikethrough', - tooltipMessage: 'Strikethrough'), - _centerToolbarIcon('underline', tooltipMessage: 'Underline'), - _centerToolbarIcon('divider', width: 2), - _centerToolbarIcon('quote', tooltipMessage: 'Quote'), - // _centerToolbarIcon('number_list'), - _centerToolbarIcon('bulleted_list', - tooltipMessage: 'Bulleted List'), - _centerToolbarIcon('divider', width: 2), - _centerToolbarIcon('highlight', tooltipMessage: 'Highlight'), - ], - ), - ), - ); - } - - // Widget _listToolbar(BuildContext context) { - // return _centerToolbarIcon( - // 'quote', - // key: _listToolbarKey, - // width: listToolbarWidth, - // onTap: () => _onTapListToolbar(context), - // ); - // } - - Widget _centerToolbarIcon(String name, - {Key? key, String? tooltipMessage, double? width, VoidCallback? onTap}) { - return Tooltip( - key: key, - preferBelow: false, - message: tooltipMessage ?? '', - child: MouseRegion( - cursor: SystemMouseCursors.click, - child: GestureDetector( - onTap: onTap ?? () => _onTap(name), - child: SizedBox.fromSize( - size: - Size(toolbarHeight - (width != null ? 20 : 0), toolbarHeight), - child: Center( - child: FlowySvg( - width: width ?? 20, - name: 'toolbar/$name', - ), - ), - ), - ), - )); - } - - // void _onTapListToolbar(BuildContext context) { - // // TODO: implement more detailed UI. - // final items = defaultListToolbarEventNames; - // final renderBox = - // _listToolbarKey.currentContext?.findRenderObject() as RenderBox; - // final offset = renderBox - // .localToGlobal(Offset.zero) - // .translate(0, toolbarHeight - cornerRadius); - // final rect = offset & Size(listToolbarWidth, listToolbarHeight); - - // _listToolbarOverlay?.remove(); - // _listToolbarOverlay = OverlayEntry(builder: (context) { - // return Positioned.fromRect( - // rect: rect, - // child: Material( - // borderRadius: BorderRadius.only( - // bottomLeft: Radius.circular(cornerRadius), - // bottomRight: Radius.circular(cornerRadius), - // ), - // color: const Color(0xFF333333), - // child: SingleChildScrollView( - // child: ListView.builder( - // itemExtent: toolbarHeight, - // padding: const EdgeInsets.only(bottom: 10.0), - // shrinkWrap: true, - // itemCount: items.length, - // itemBuilder: ((context, index) { - // return ListTile( - // contentPadding: const EdgeInsets.only( - // left: 3.0, - // right: 3.0, - // ), - // minVerticalPadding: 0.0, - // title: FittedBox( - // fit: BoxFit.scaleDown, - // child: Text( - // items[index], - // textAlign: TextAlign.center, - // style: const TextStyle( - // color: Colors.white, - // ), - // ), - // ), - // onTap: () { - // _onTap(items[index]); - // }, - // ); - // }), - // ), - // ), - // ), - // ); - // }); - // // TODO: disable scrolling. - // Overlay.of(context)?.insert(_listToolbarOverlay!); - // } - - void _onTap(String eventName) { - if (defaultToolbarEventHandlers.containsKey(eventName)) { - defaultToolbarEventHandlers[eventName]!(widget.editorState); - return; - } - assert(false, 'Could not find the event handler for $eventName'); - } -} diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item.dart new file mode 100644 index 0000000000..49413311d2 --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item.dart @@ -0,0 +1,192 @@ +import 'package:appflowy_editor/appflowy_editor.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/rich_text/rich_text_style.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'; +import 'package:rich_clipboard/rich_clipboard.dart'; + +typedef ToolbarEventHandler = void Function( + EditorState editorState, BuildContext context); +typedef ToolbarShowValidator = bool Function(EditorState editorState); + +class ToolbarItem { + ToolbarItem({ + required this.icon, + this.tooltipsMessage = '', + required this.validator, + required this.handler, + }); + + final Widget icon; + final String tooltipsMessage; + final ToolbarShowValidator validator; + final ToolbarEventHandler handler; + + factory ToolbarItem.divider() { + return ToolbarItem( + icon: const FlowySvg(name: 'toolbar/divider'), + validator: (editorState) => true, + handler: (editorState, context) {}, + ); + } +} + +List defaultToolbarItems = [ + ToolbarItem( + tooltipsMessage: 'Heading 1', + icon: const FlowySvg(name: 'toolbar/h1'), + validator: _onlyShowInSingleTextSelection, + handler: (editorState, context) => formatHeading(editorState, StyleKey.h1), + ), + ToolbarItem( + tooltipsMessage: 'Heading 2', + icon: const FlowySvg(name: 'toolbar/h2'), + validator: _onlyShowInSingleTextSelection, + handler: (editorState, context) => formatHeading(editorState, StyleKey.h2), + ), + ToolbarItem( + tooltipsMessage: 'Heading 3', + icon: const FlowySvg(name: 'toolbar/h3'), + validator: _onlyShowInSingleTextSelection, + handler: (editorState, context) => formatHeading(editorState, StyleKey.h3), + ), + ToolbarItem.divider(), + ToolbarItem( + tooltipsMessage: 'Bold', + icon: const FlowySvg(name: 'toolbar/bold'), + validator: _showInTextSelection, + handler: (editorState, context) => formatBold(editorState), + ), + ToolbarItem( + tooltipsMessage: 'Italic', + icon: const FlowySvg(name: 'toolbar/italic'), + validator: _showInTextSelection, + handler: (editorState, context) => formatItalic(editorState), + ), + ToolbarItem( + tooltipsMessage: 'Underline', + icon: const FlowySvg(name: 'toolbar/underline'), + validator: _showInTextSelection, + handler: (editorState, context) => formatUnderline(editorState), + ), + ToolbarItem( + tooltipsMessage: 'Strikethrough', + icon: const FlowySvg(name: 'toolbar/strikethrough'), + validator: _showInTextSelection, + handler: (editorState, context) => formatStrikethrough(editorState), + ), + ToolbarItem.divider(), + ToolbarItem( + tooltipsMessage: 'Quote', + icon: const FlowySvg(name: 'toolbar/quote'), + validator: _onlyShowInSingleTextSelection, + handler: (editorState, context) => formatQuote(editorState), + ), + ToolbarItem( + tooltipsMessage: 'Bulleted list', + icon: const FlowySvg(name: 'toolbar/bulleted_list'), + validator: _onlyShowInSingleTextSelection, + handler: (editorState, context) => formatBulletedList(editorState), + ), + ToolbarItem.divider(), + ToolbarItem( + tooltipsMessage: 'Link', + icon: const FlowySvg(name: 'toolbar/link'), + validator: _onlyShowInSingleTextSelection, + handler: (editorState, context) => _showLinkMenu(editorState, context), + ), + ToolbarItem( + tooltipsMessage: 'Highlight', + icon: const FlowySvg(name: 'toolbar/highlight'), + validator: _showInTextSelection, + handler: (editorState, context) => formatHighlight(editorState), + ), +]; + +ToolbarShowValidator _onlyShowInSingleTextSelection = (editorState) { + final nodes = editorState.service.selectionService.currentSelectedNodes; + return (nodes.length == 1 && nodes.first is TextNode); +}; + +ToolbarShowValidator _showInTextSelection = (editorState) { + final nodes = editorState.service.selectionService.currentSelectedNodes + .whereType(); + return nodes.isNotEmpty; +}; + +OverlayEntry? _linkMenuOverlay; +EditorState? _editorState; +void _showLinkMenu(EditorState editorState, BuildContext context) { + _editorState = editorState; + + final rects = editorState.service.selectionService.selectionRects; + var maxBottom = 0.0; + late Rect matchRect; + for (final rect in rects) { + if (rect.bottom > maxBottom) { + maxBottom = rect.bottom; + matchRect = rect; + } + } + + _dismissLinkMenu(); + + // Since the link menu will only show in single text selection, + // We get the text node directly instead of judging details again. + final selection = + editorState.service.selectionService.currentSelection.value!; + final index = + selection.isBackward ? selection.start.offset : selection.end.offset; + final length = (selection.start.offset - selection.end.offset).abs(); + final node = editorState.service.selectionService.currentSelectedNodes.first + as TextNode; + final linkText = node.getAttributeInSelection(selection, StyleKey.href); + _linkMenuOverlay = OverlayEntry(builder: (context) { + return Positioned( + top: matchRect.bottom, + left: matchRect.left, + child: Material( + child: LinkMenu( + linkText: linkText, + onSubmitted: (text) { + TransactionBuilder(editorState) + ..formatText(node, index, length, { + StyleKey.href: text, + }) + ..commit(); + _dismissLinkMenu(); + }, + onCopyLink: () { + RichClipboard.setData(RichClipboardData(text: linkText)); + _dismissLinkMenu(); + }, + onRemoveLink: () { + TransactionBuilder(editorState) + ..formatText(node, index, length, { + StyleKey.href: null, + }) + ..commit(); + _dismissLinkMenu(); + }, + ), + ), + ); + }); + Overlay.of(context)?.insert(_linkMenuOverlay!); + + editorState.service.scrollService?.disable(); + editorState.service.selectionService.currentSelection + .addListener(_dismissLinkMenu); +} + +void _dismissLinkMenu() { + _linkMenuOverlay?.remove(); + _linkMenuOverlay = null; + + _editorState?.service.scrollService?.enable(); + _editorState?.service.selectionService.currentSelection + .removeListener(_dismissLinkMenu); + _editorState = null; +} diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item_widget.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item_widget.dart new file mode 100644 index 0000000000..ce89eef126 --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item_widget.dart @@ -0,0 +1,35 @@ +import 'package:flutter/material.dart'; + +import 'toolbar_item.dart'; + +class ToolbarItemWidget extends StatelessWidget { + const ToolbarItemWidget({ + Key? key, + required this.item, + required this.onPressed, + }) : super(key: key); + + final ToolbarItem item; + final VoidCallback onPressed; + + @override + Widget build(BuildContext context) { + return SizedBox( + width: 28, + height: 28, + child: Tooltip( + preferBelow: false, + message: item.tooltipsMessage, + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: IconButton( + padding: EdgeInsets.zero, + icon: item.icon, + iconSize: 28, + onPressed: onPressed, + ), + ), + ), + ); + } +} diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_widget.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_widget.dart new file mode 100644 index 0000000000..167f8e79ba --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_widget.dart @@ -0,0 +1,82 @@ +import 'package:appflowy_editor/src/render/toolbar/toolbar_item.dart'; +import 'package:appflowy_editor/src/render/toolbar/toolbar_item_widget.dart'; +import 'package:flutter/material.dart'; + +import 'package:appflowy_editor/src/editor_state.dart'; + +mixin ToolbarMixin on State { + void hide(); +} + +class ToolbarWidget extends StatefulWidget { + const ToolbarWidget({ + Key? key, + required this.editorState, + required this.layerLink, + required this.offset, + required this.items, + }) : super(key: key); + + final EditorState editorState; + final LayerLink layerLink; + final Offset offset; + final List items; + + @override + State createState() => _ToolbarWidgetState(); +} + +class _ToolbarWidgetState extends State with ToolbarMixin { + OverlayEntry? _listToolbarOverlay; + + @override + Widget build(BuildContext context) { + return Positioned( + top: widget.offset.dx, + left: widget.offset.dy, + child: CompositedTransformFollower( + link: widget.layerLink, + showWhenUnlinked: true, + offset: widget.offset, + child: _buildToolbar(context), + ), + ); + } + + @override + void hide() { + _listToolbarOverlay?.remove(); + _listToolbarOverlay = null; + } + + Widget _buildToolbar(BuildContext context) { + final items = widget.items.where( + (item) => item.validator(widget.editorState), + ); + return Material( + borderRadius: BorderRadius.circular(8.0), + color: const Color(0xFF333333), + child: Padding( + padding: const EdgeInsets.only(left: 8.0, right: 8.0), + child: SizedBox( + height: 32.0, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: items + .map( + (item) => Center( + child: ToolbarItemWidget( + item: item, + onPressed: () { + item.handler(widget.editorState, context); + }, + ), + ), + ) + .toList(growable: false), + ), + ), + ), + ); + } +} diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/input_service.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/input_service.dart index 9aae2b5fcb..a92fae1b95 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/input_service.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/input_service.dart @@ -87,15 +87,18 @@ class _AppFlowyInputState extends State @override void attach(TextEditingValue textEditingValue) { - _textInputConnection ??= TextInput.attach( - this, - const TextInputConfiguration( - // TODO: customize - enableDeltaModel: true, - inputType: TextInputType.multiline, - textCapitalization: TextCapitalization.sentences, - ), - ); + if (_textInputConnection == null || + _textInputConnection!.attached == false) { + _textInputConnection = TextInput.attach( + this, + const TextInputConfiguration( + // TODO: customize + enableDeltaModel: true, + inputType: TextInputType.multiline, + textCapitalization: TextCapitalization.sentences, + ), + ); + } _textInputConnection! ..setEditingState(textEditingValue) diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/toolbar_service.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/toolbar_service.dart index bf380290f9..fe4a2beace 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/toolbar_service.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/toolbar_service.dart @@ -1,7 +1,8 @@ +import 'package:appflowy_editor/src/render/toolbar/toolbar_item.dart'; import 'package:flutter/material.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:appflowy_editor/src/render/selection/toolbar_widget.dart'; +import 'package:appflowy_editor/src/render/toolbar/toolbar_widget.dart'; import 'package:appflowy_editor/src/extensions/object_extensions.dart'; abstract class FlowyToolbarService { @@ -41,7 +42,7 @@ class _FlowyToolbarState extends State editorState: widget.editorState, layerLink: layerLink, offset: offset.translate(0, -37.0), - handlers: const {}, + items: defaultToolbarItems, ), ); Overlay.of(context)?.insert(_toolbarOverlay!); From 886c1f00e541d6c1b11b646845d161ee98543d69 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Mon, 22 Aug 2022 17:51:47 +0800 Subject: [PATCH 009/106] feat: implement command + K to trigger link menu --- .../lib/src/render/link_menu/link_menu.dart | 17 ++++++++----- .../lib/src/render/toolbar/toolbar_item.dart | 24 ++++++++++++++----- ...pdate_text_style_by_command_x_handler.dart | 6 +++++ .../lib/src/service/toolbar_service.dart | 14 +++++++++++ 4 files changed, 49 insertions(+), 12 deletions(-) diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/link_menu/link_menu.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/link_menu/link_menu.dart index 3ebf87f4fa..679bea09f7 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/link_menu/link_menu.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/link_menu/link_menu.dart @@ -21,12 +21,21 @@ class LinkMenu extends StatefulWidget { class _LinkMenuState extends State { final _textEditingController = TextEditingController(); + final _focusNode = FocusNode(); @override void initState() { super.initState(); _textEditingController.text = widget.linkText ?? ''; + _focusNode.requestFocus(); + } + + @override + void dispose() { + _focusNode.dispose(); + + super.dispose(); } @override @@ -88,7 +97,7 @@ class _LinkMenuState extends State { Widget _buildInput() { return TextField( - autofocus: true, + focusNode: _focusNode, style: const TextStyle(fontSize: 14.0), textAlign: TextAlign.left, controller: _textEditingController, @@ -112,11 +121,7 @@ class _LinkMenuState extends State { required VoidCallback onPressed, }) { return TextButton.icon( - icon: FlowySvg( - name: iconName, - width: 20.0, - height: 20.0, - ), + icon: FlowySvg(name: iconName), style: TextButton.styleFrom( minimumSize: const Size.fromHeight(40), padding: EdgeInsets.zero, diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item.dart index 49413311d2..a068722cb2 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item.dart @@ -13,12 +13,14 @@ typedef ToolbarShowValidator = bool Function(EditorState editorState); class ToolbarItem { ToolbarItem({ + required this.id, required this.icon, this.tooltipsMessage = '', required this.validator, required this.handler, }); + final String id; final Widget icon; final String tooltipsMessage; final ToolbarShowValidator validator; @@ -26,6 +28,7 @@ class ToolbarItem { factory ToolbarItem.divider() { return ToolbarItem( + id: 'divider', icon: const FlowySvg(name: 'toolbar/divider'), validator: (editorState) => true, handler: (editorState, context) {}, @@ -35,18 +38,21 @@ class ToolbarItem { List defaultToolbarItems = [ ToolbarItem( + id: 'appflowy.toolbar.h1', tooltipsMessage: 'Heading 1', icon: const FlowySvg(name: 'toolbar/h1'), validator: _onlyShowInSingleTextSelection, handler: (editorState, context) => formatHeading(editorState, StyleKey.h1), ), ToolbarItem( + id: 'appflowy.toolbar.h2', tooltipsMessage: 'Heading 2', icon: const FlowySvg(name: 'toolbar/h2'), validator: _onlyShowInSingleTextSelection, handler: (editorState, context) => formatHeading(editorState, StyleKey.h2), ), ToolbarItem( + id: 'appflowy.toolbar.h3', tooltipsMessage: 'Heading 3', icon: const FlowySvg(name: 'toolbar/h3'), validator: _onlyShowInSingleTextSelection, @@ -54,24 +60,28 @@ List defaultToolbarItems = [ ), ToolbarItem.divider(), ToolbarItem( + id: 'appflowy.toolbar.bold', tooltipsMessage: 'Bold', icon: const FlowySvg(name: 'toolbar/bold'), validator: _showInTextSelection, handler: (editorState, context) => formatBold(editorState), ), ToolbarItem( + id: 'appflowy.toolbar.italic', tooltipsMessage: 'Italic', icon: const FlowySvg(name: 'toolbar/italic'), validator: _showInTextSelection, handler: (editorState, context) => formatItalic(editorState), ), ToolbarItem( + id: 'appflowy.toolbar.underline', tooltipsMessage: 'Underline', icon: const FlowySvg(name: 'toolbar/underline'), validator: _showInTextSelection, handler: (editorState, context) => formatUnderline(editorState), ), ToolbarItem( + id: 'appflowy.toolbar.strikethrough', tooltipsMessage: 'Strikethrough', icon: const FlowySvg(name: 'toolbar/strikethrough'), validator: _showInTextSelection, @@ -79,12 +89,14 @@ List defaultToolbarItems = [ ), ToolbarItem.divider(), ToolbarItem( + id: 'appflowy.toolbar.quote', tooltipsMessage: 'Quote', icon: const FlowySvg(name: 'toolbar/quote'), validator: _onlyShowInSingleTextSelection, handler: (editorState, context) => formatQuote(editorState), ), ToolbarItem( + id: 'appflowy.toolbar.bulleted_list', tooltipsMessage: 'Bulleted list', icon: const FlowySvg(name: 'toolbar/bulleted_list'), validator: _onlyShowInSingleTextSelection, @@ -92,12 +104,14 @@ List defaultToolbarItems = [ ), ToolbarItem.divider(), ToolbarItem( + id: 'appflowy.toolbar.link', tooltipsMessage: 'Link', icon: const FlowySvg(name: 'toolbar/link'), validator: _onlyShowInSingleTextSelection, handler: (editorState, context) => _showLinkMenu(editorState, context), ), ToolbarItem( + id: 'appflowy.toolbar.highlight', tooltipsMessage: 'Highlight', icon: const FlowySvg(name: 'toolbar/highlight'), validator: _showInTextSelection, @@ -152,9 +166,7 @@ void _showLinkMenu(EditorState editorState, BuildContext context) { linkText: linkText, onSubmitted: (text) { TransactionBuilder(editorState) - ..formatText(node, index, length, { - StyleKey.href: text, - }) + ..formatText(node, index, length, {StyleKey.href: text}) ..commit(); _dismissLinkMenu(); }, @@ -164,9 +176,7 @@ void _showLinkMenu(EditorState editorState, BuildContext context) { }, onRemoveLink: () { TransactionBuilder(editorState) - ..formatText(node, index, length, { - StyleKey.href: null, - }) + ..formatText(node, index, length, {StyleKey.href: null}) ..commit(); _dismissLinkMenu(); }, @@ -177,6 +187,7 @@ void _showLinkMenu(EditorState editorState, BuildContext context) { Overlay.of(context)?.insert(_linkMenuOverlay!); editorState.service.scrollService?.disable(); + editorState.service.keyboardService?.disable(); editorState.service.selectionService.currentSelection .addListener(_dismissLinkMenu); } @@ -186,6 +197,7 @@ void _dismissLinkMenu() { _linkMenuOverlay = null; _editorState?.service.scrollService?.enable(); + _editorState?.service.keyboardService?.enable(); _editorState?.service.selectionService.currentSelection .removeListener(_dismissLinkMenu); _editorState = null; diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart index 0eb926525b..00b304f527 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart @@ -36,6 +36,12 @@ AppFlowyKeyEventHandler updateTextStyleByCommandXHandler = event.isShiftPressed) { formatHighlight(editorState); return KeyEventResult.handled; + } else if (event.logicalKey == LogicalKeyboardKey.keyK) { + if (editorState.service.toolbarService + ?.triggerHandler('appflowy.toolbar.link') == + true) { + return KeyEventResult.handled; + } } return KeyEventResult.ignored; diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/toolbar_service.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/toolbar_service.dart index fe4a2beace..290fe4b4bb 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/toolbar_service.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/toolbar_service.dart @@ -11,6 +11,9 @@ abstract class FlowyToolbarService { /// Hide the toolbar widget. void hide(); + + /// Trigger the specified handler. + bool triggerHandler(String id); } class FlowyToolbar extends StatefulWidget { @@ -55,6 +58,17 @@ class _FlowyToolbarState extends State _toolbarOverlay = null; } + @override + bool triggerHandler(String id) { + final items = defaultToolbarItems.where((item) => item.id == id); + if (items.length != 1) { + assert(items.length == 1, 'The toolbar item\'s id must be unique'); + return false; + } + items.first.handler(widget.editorState, context); + return true; + } + @override Widget build(BuildContext context) { return Container( From 1cd9a77e007538d1056cf864bb8225bef0dbd022 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Mon, 22 Aug 2022 18:16:15 +0800 Subject: [PATCH 010/106] fix: could not open link without scheme --- .../lib/src/render/rich_text/rich_text_style.dart | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/rich_text_style.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/rich_text_style.dart index 7bd68c45e7..6bc50c5115 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/rich_text_style.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/rich_text_style.dart @@ -277,7 +277,13 @@ class RichTextStyle { if (href != null) { return TapGestureRecognizer() ..onTap = () async { - await launchUrlString(href); + final uri = Uri.parse(href); + // url_launcher cannot open a link without scheme. + final newHref = + (uri.scheme.isNotEmpty ? href : 'http://$href').trim(); + if (await canLaunchUrlString(newHref)) { + await launchUrlString(newHref); + } }; } return null; From 6517adece7a92bba53d774ad532dd11c82be4cba Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Mon, 22 Aug 2022 18:19:21 +0800 Subject: [PATCH 011/106] fix: show remove link and copy link entry when there is not linkd text --- .../lib/src/render/link_menu/link_menu.dart | 55 +++++++++---------- 1 file changed, 26 insertions(+), 29 deletions(-) diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/link_menu/link_menu.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/link_menu/link_menu.dart index 679bea09f7..3a519ed70f 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/link_menu/link_menu.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/link_menu/link_menu.dart @@ -40,33 +40,30 @@ class _LinkMenuState extends State { @override Widget build(BuildContext context) { - return SizedBox( - width: 350, - height: 200, - child: Container( - decoration: BoxDecoration( - color: Colors.white, - boxShadow: [ - BoxShadow( - blurRadius: 5, - spreadRadius: 1, - color: Colors.black.withOpacity(0.1), - ), - ], - borderRadius: BorderRadius.circular(6.0), - ), - child: SizedBox( - width: 350, - height: 200, - child: Padding( - padding: const EdgeInsets.all(20.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildHeader(), - const SizedBox(height: 16.0), - _buildInput(), - const SizedBox(height: 16.0), + return Container( + decoration: BoxDecoration( + color: Colors.white, + boxShadow: [ + BoxShadow( + blurRadius: 5, + spreadRadius: 1, + color: Colors.black.withOpacity(0.1), + ), + ], + borderRadius: BorderRadius.circular(6.0), + ), + child: SizedBox( + width: 350, + child: Padding( + padding: const EdgeInsets.all(20.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildHeader(), + const SizedBox(height: 16.0), + _buildInput(), + const SizedBox(height: 16.0), + if (widget.linkText != null) ...[ _buildIconButton( iconName: 'link', text: 'Copy link', @@ -77,8 +74,8 @@ class _LinkMenuState extends State { text: 'Remove link', onPressed: widget.onRemoveLink, ), - ], - ), + ] + ], ), ), ), From 70ea80878adea6c15e94c973d7155a1edd0cbf7a Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Mon, 22 Aug 2022 20:38:24 +0800 Subject: [PATCH 012/106] feat: single tap to edit link and double tap to open the link --- .../render/rich_text/bulleted_list_text.dart | 5 +- .../src/render/rich_text/checkbox_text.dart | 6 +- .../src/render/rich_text/flowy_rich_text.dart | 97 ++++++++++++------- .../render/rich_text/number_list_text.dart | 4 +- .../lib/src/render/rich_text/quoted_text.dart | 4 +- .../src/render/rich_text/rich_text_style.dart | 24 +---- .../lib/src/render/toolbar/toolbar_item.dart | 33 ++++++- .../src/render/toolbar/toolbar_widget.dart | 5 +- .../lib/src/service/toolbar_service.dart | 16 ++- 9 files changed, 115 insertions(+), 79 deletions(-) diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/bulleted_list_text.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/bulleted_list_text.dart index 267a5acc66..7d69ff459f 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/bulleted_list_text.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/bulleted_list_text.dart @@ -56,8 +56,6 @@ class _BulletedListTextNodeWidgetState extends State @override Widget build(BuildContext context) { - final topPadding = RichTextStyle.fromTextNode(widget.textNode).topPadding; - return SizedBox( width: defaultMaxTextNodeWidth, child: Padding( @@ -69,8 +67,7 @@ class _BulletedListTextNodeWidgetState extends State key: iconKey, width: _iconWidth, height: _iconWidth, - padding: - EdgeInsets.only(top: topPadding, right: _iconRightPadding), + padding: EdgeInsets.only(right: _iconRightPadding), name: 'point', ), Expanded( diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/checkbox_text.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/checkbox_text.dart index 9b7d3a730f..0255a84049 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/checkbox_text.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/checkbox_text.dart @@ -63,7 +63,6 @@ class _CheckboxNodeWidgetState extends State Widget _buildWithSingle(BuildContext context) { final check = widget.textNode.attributes.check; - final topPadding = RichTextStyle.fromTextNode(widget.textNode).topPadding; return SizedBox( width: defaultMaxTextNodeWidth, child: Padding( @@ -76,10 +75,7 @@ class _CheckboxNodeWidgetState extends State child: FlowySvg( width: _iconWidth, height: _iconWidth, - padding: EdgeInsets.only( - top: topPadding, - right: _iconRightPadding, - ), + padding: EdgeInsets.only(right: _iconRightPadding), name: check ? 'check' : 'uncheck', ), onTap: () { diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/flowy_rich_text.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/flowy_rich_text.dart index 89df8a54b1..39f484c23f 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/flowy_rich_text.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/flowy_rich_text.dart @@ -1,5 +1,7 @@ +import 'dart:async'; import 'dart:ui'; +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; @@ -11,6 +13,7 @@ import 'package:appflowy_editor/src/document/text_delta.dart'; import 'package:appflowy_editor/src/editor_state.dart'; import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart'; import 'package:appflowy_editor/src/render/selection/selectable.dart'; +import 'package:url_launcher/url_launcher_string.dart'; typedef FlowyTextSpanDecorator = TextSpan Function(TextSpan textSpan); @@ -143,6 +146,11 @@ class _FlowyRichTextState extends State with Selectable { ); } + @override + Offset localToGlobal(Offset offset) { + return _renderParagraph.localToGlobal(offset); + } + Widget _buildRichText(BuildContext context) { return MouseRegion( cursor: SystemMouseCursors.text, @@ -181,44 +189,63 @@ class _FlowyRichTextState extends State with Selectable { ); } - // unused now. - // Widget _buildRichTextWithChildren(BuildContext context) { - // return Column( - // crossAxisAlignment: CrossAxisAlignment.start, - // children: [ - // _buildSingleRichText(context), - // ...widget.textNode.children - // .map( - // (child) => widget.editorState.service.renderPluginService - // .buildPluginWidget( - // NodeWidgetContext( - // context: context, - // node: child, - // editorState: widget.editorState, - // ), - // ), - // ) - // .toList() - // ], - // ); - // } + TextSpan get _textSpan { + var offset = 0; + return TextSpan( + children: widget.textNode.delta.whereType().map((insert) { + GestureRecognizer? gestureDetector; + if (insert.attributes?[StyleKey.href] != null) { + final startOffset = offset; + Timer? timer; + var tapCount = 0; + gestureDetector = TapGestureRecognizer() + ..onTap = () async { + // implement a simple double tap logic + tapCount += 1; + timer?.cancel(); - @override - Offset localToGlobal(Offset offset) { - return _renderParagraph.localToGlobal(offset); + if (tapCount == 2) { + tapCount = 0; + final href = insert.attributes![StyleKey.href]; + final uri = Uri.parse(href); + // url_launcher cannot open a link without scheme. + final newHref = + (uri.scheme.isNotEmpty ? href : 'http://$href').trim(); + if (await canLaunchUrlString(newHref)) { + await launchUrlString(newHref); + } + return; + } + + timer = Timer(const Duration(milliseconds: 200), () { + tapCount = 0; + // update selection + final selection = Selection.single( + path: widget.textNode.path, + startOffset: startOffset, + endOffset: startOffset + insert.length, + ); + widget.editorState.service.selectionService + .updateSelection(selection); + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + widget.editorState.service.toolbarService + ?.triggerHandler('appflowy.toolbar.link'); + }); + }); + }; + } + offset += insert.length; + final textSpan = RichTextStyle( + attributes: insert.attributes ?? {}, + text: insert.content, + height: _lineHeight, + gestureRecognizer: gestureDetector, + ).toTextSpan(); + return textSpan; + }).toList(growable: false), + ); } - TextSpan get _textSpan => TextSpan( - children: widget.textNode.delta - .whereType() - .map((insert) => RichTextStyle( - attributes: insert.attributes ?? {}, - text: insert.content, - height: _lineHeight, - ).toTextSpan()) - .toList(growable: false), - ); - TextSpan get _placeholderTextSpan => TextSpan(children: [ RichTextStyle( text: widget.placeholderText, diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/number_list_text.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/number_list_text.dart index de3b0b55b6..c1062e1c3c 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/number_list_text.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/number_list_text.dart @@ -56,7 +56,6 @@ class _NumberListTextNodeWidgetState extends State @override Widget build(BuildContext context) { - final topPadding = RichTextStyle.fromTextNode(widget.textNode).topPadding; return Padding( padding: EdgeInsets.only(bottom: defaultLinePadding), child: SizedBox( @@ -68,8 +67,7 @@ class _NumberListTextNodeWidgetState extends State key: iconKey, width: _iconWidth, height: _iconWidth, - padding: - EdgeInsets.only(top: topPadding, right: _iconRightPadding), + padding: EdgeInsets.only(right: _iconRightPadding), number: widget.textNode.attributes.number, ), Expanded( diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/quoted_text.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/quoted_text.dart index 0389dfa50f..3391729779 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/quoted_text.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/quoted_text.dart @@ -55,7 +55,6 @@ class _QuotedTextNodeWidgetState extends State @override Widget build(BuildContext context) { - final topPadding = RichTextStyle.fromTextNode(widget.textNode).topPadding; return SizedBox( width: defaultMaxTextNodeWidth, child: Padding( @@ -67,8 +66,7 @@ class _QuotedTextNodeWidgetState extends State FlowySvg( key: iconKey, width: _iconWidth, - padding: EdgeInsets.only( - top: topPadding, right: _iconRightPadding), + padding: EdgeInsets.only(right: _iconRightPadding), name: 'quote', ), Expanded( diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/rich_text_style.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/rich_text_style.dart index 6bc50c5115..efcdd3790f 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/rich_text_style.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/rich_text_style.dart @@ -1,8 +1,6 @@ import 'package:appflowy_editor/src/document/attributes.dart'; -import 'package:appflowy_editor/src/document/node.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; -import 'package:url_launcher/url_launcher_string.dart'; /// /// Supported partial rendering types: @@ -182,14 +180,13 @@ class RichTextStyle { RichTextStyle({ required this.attributes, required this.text, + this.gestureRecognizer, this.height = 1.5, }); - RichTextStyle.fromTextNode(TextNode textNode) - : this(attributes: textNode.attributes, text: textNode.toRawString()); - final Attributes attributes; final String text; + final GestureRecognizer? gestureRecognizer; final double height; TextSpan toTextSpan() => _toTextSpan(height); @@ -201,6 +198,7 @@ class RichTextStyle { TextSpan _toTextSpan(double? height) { return TextSpan( text: text, + recognizer: _recognizer, style: TextStyle( fontWeight: _fontWeight, fontStyle: _fontStyle, @@ -210,7 +208,6 @@ class RichTextStyle { background: _background, height: height, ), - recognizer: _recognizer, ); } @@ -273,19 +270,6 @@ class RichTextStyle { // recognizer GestureRecognizer? get _recognizer { - final href = attributes.href; - if (href != null) { - return TapGestureRecognizer() - ..onTap = () async { - final uri = Uri.parse(href); - // url_launcher cannot open a link without scheme. - final newHref = - (uri.scheme.isNotEmpty ? href : 'http://$href').trim(); - if (await canLaunchUrlString(newHref)) { - await launchUrlString(newHref); - } - }; - } - return null; + return gestureRecognizer; } } diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item.dart index a068722cb2..b2f6a9b2f7 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item.dart @@ -14,6 +14,7 @@ typedef ToolbarShowValidator = bool Function(EditorState editorState); class ToolbarItem { ToolbarItem({ required this.id, + required this.type, required this.icon, this.tooltipsMessage = '', required this.validator, @@ -21,6 +22,7 @@ class ToolbarItem { }); final String id; + final int type; final Widget icon; final String tooltipsMessage; final ToolbarShowValidator validator; @@ -29,16 +31,32 @@ class ToolbarItem { factory ToolbarItem.divider() { return ToolbarItem( id: 'divider', + type: -1, icon: const FlowySvg(name: 'toolbar/divider'), validator: (editorState) => true, handler: (editorState, context) {}, ); } + + @override + bool operator ==(Object other) { + if (other is! ToolbarItem) { + return false; + } + if (identical(this, other)) { + return true; + } + return id == other.id; + } + + @override + int get hashCode => id.hashCode; } List defaultToolbarItems = [ ToolbarItem( id: 'appflowy.toolbar.h1', + type: 1, tooltipsMessage: 'Heading 1', icon: const FlowySvg(name: 'toolbar/h1'), validator: _onlyShowInSingleTextSelection, @@ -46,6 +64,7 @@ List defaultToolbarItems = [ ), ToolbarItem( id: 'appflowy.toolbar.h2', + type: 1, tooltipsMessage: 'Heading 2', icon: const FlowySvg(name: 'toolbar/h2'), validator: _onlyShowInSingleTextSelection, @@ -53,14 +72,15 @@ List defaultToolbarItems = [ ), ToolbarItem( id: 'appflowy.toolbar.h3', + type: 1, tooltipsMessage: 'Heading 3', icon: const FlowySvg(name: 'toolbar/h3'), validator: _onlyShowInSingleTextSelection, handler: (editorState, context) => formatHeading(editorState, StyleKey.h3), ), - ToolbarItem.divider(), ToolbarItem( id: 'appflowy.toolbar.bold', + type: 2, tooltipsMessage: 'Bold', icon: const FlowySvg(name: 'toolbar/bold'), validator: _showInTextSelection, @@ -68,6 +88,7 @@ List defaultToolbarItems = [ ), ToolbarItem( id: 'appflowy.toolbar.italic', + type: 2, tooltipsMessage: 'Italic', icon: const FlowySvg(name: 'toolbar/italic'), validator: _showInTextSelection, @@ -75,6 +96,7 @@ List defaultToolbarItems = [ ), ToolbarItem( id: 'appflowy.toolbar.underline', + type: 2, tooltipsMessage: 'Underline', icon: const FlowySvg(name: 'toolbar/underline'), validator: _showInTextSelection, @@ -82,14 +104,15 @@ List defaultToolbarItems = [ ), ToolbarItem( id: 'appflowy.toolbar.strikethrough', + type: 2, tooltipsMessage: 'Strikethrough', icon: const FlowySvg(name: 'toolbar/strikethrough'), validator: _showInTextSelection, handler: (editorState, context) => formatStrikethrough(editorState), ), - ToolbarItem.divider(), ToolbarItem( id: 'appflowy.toolbar.quote', + type: 3, tooltipsMessage: 'Quote', icon: const FlowySvg(name: 'toolbar/quote'), validator: _onlyShowInSingleTextSelection, @@ -97,14 +120,15 @@ List defaultToolbarItems = [ ), ToolbarItem( id: 'appflowy.toolbar.bulleted_list', + type: 3, tooltipsMessage: 'Bulleted list', icon: const FlowySvg(name: 'toolbar/bulleted_list'), validator: _onlyShowInSingleTextSelection, handler: (editorState, context) => formatBulletedList(editorState), ), - ToolbarItem.divider(), ToolbarItem( id: 'appflowy.toolbar.link', + type: 4, tooltipsMessage: 'Link', icon: const FlowySvg(name: 'toolbar/link'), validator: _onlyShowInSingleTextSelection, @@ -112,6 +136,7 @@ List defaultToolbarItems = [ ), ToolbarItem( id: 'appflowy.toolbar.highlight', + type: 4, tooltipsMessage: 'Highlight', icon: const FlowySvg(name: 'toolbar/highlight'), validator: _showInTextSelection, @@ -159,7 +184,7 @@ void _showLinkMenu(EditorState editorState, BuildContext context) { final linkText = node.getAttributeInSelection(selection, StyleKey.href); _linkMenuOverlay = OverlayEntry(builder: (context) { return Positioned( - top: matchRect.bottom, + top: matchRect.bottom + 5.0, left: matchRect.left, child: Material( child: LinkMenu( diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_widget.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_widget.dart index 167f8e79ba..395c6818bb 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_widget.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_widget.dart @@ -50,9 +50,6 @@ class _ToolbarWidgetState extends State with ToolbarMixin { } Widget _buildToolbar(BuildContext context) { - final items = widget.items.where( - (item) => item.validator(widget.editorState), - ); return Material( borderRadius: BorderRadius.circular(8.0), color: const Color(0xFF333333), @@ -62,7 +59,7 @@ class _ToolbarWidgetState extends State with ToolbarMixin { height: 32.0, child: Row( crossAxisAlignment: CrossAxisAlignment.start, - children: items + children: widget.items .map( (item) => Center( child: ToolbarItemWidget( diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/toolbar_service.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/toolbar_service.dart index 290fe4b4bb..143f899926 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/toolbar_service.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/toolbar_service.dart @@ -39,13 +39,27 @@ class _FlowyToolbarState extends State void showInOffset(Offset offset, LayerLink layerLink) { hide(); + final items = defaultToolbarItems + .where((item) => item.validator(widget.editorState)) + .toList(growable: false) + ..sort((a, b) => a.type.compareTo(b.type)); + if (items.isEmpty) { + return; + } + final List dividedItems = [items.first]; + for (var i = 1; i < items.length; i++) { + if (items[i].type != items[i - 1].type) { + dividedItems.add(ToolbarItem.divider()); + } + dividedItems.add(items[i]); + } _toolbarOverlay = OverlayEntry( builder: (context) => ToolbarWidget( key: _toolbarWidgetKey, editorState: widget.editorState, layerLink: layerLink, offset: offset.translate(0, -37.0), - items: defaultToolbarItems, + items: dividedItems, ), ); Overlay.of(context)?.insert(_toolbarOverlay!); From 44b4ef4ad77a147535cc41a2c56230359dd4a774 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Tue, 23 Aug 2022 09:59:14 +0800 Subject: [PATCH 013/106] feat: filter the toolbar item should not be displayed --- .../lib/src/service/toolbar_service.dart | 36 +++++++++++-------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/toolbar_service.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/toolbar_service.dart index 143f899926..40914a70e9 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/toolbar_service.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/toolbar_service.dart @@ -39,27 +39,13 @@ class _FlowyToolbarState extends State void showInOffset(Offset offset, LayerLink layerLink) { hide(); - final items = defaultToolbarItems - .where((item) => item.validator(widget.editorState)) - .toList(growable: false) - ..sort((a, b) => a.type.compareTo(b.type)); - if (items.isEmpty) { - return; - } - final List dividedItems = [items.first]; - for (var i = 1; i < items.length; i++) { - if (items[i].type != items[i - 1].type) { - dividedItems.add(ToolbarItem.divider()); - } - dividedItems.add(items[i]); - } _toolbarOverlay = OverlayEntry( builder: (context) => ToolbarWidget( key: _toolbarWidgetKey, editorState: widget.editorState, layerLink: layerLink, offset: offset.translate(0, -37.0), - items: dividedItems, + items: _filterItems(defaultToolbarItems), ), ); Overlay.of(context)?.insert(_toolbarOverlay!); @@ -96,4 +82,24 @@ class _FlowyToolbarState extends State super.dispose(); } + + // Filter items that should not be displayed, sort according to type, + // and insert dividers between different types. + List _filterItems(List items) { + final filterItems = items + .where((item) => item.validator(widget.editorState)) + .toList(growable: false) + ..sort((a, b) => a.type.compareTo(b.type)); + if (items.isEmpty) { + return []; + } + final List dividedItems = [filterItems.first]; + for (var i = 1; i < filterItems.length; i++) { + if (filterItems[i].type != filterItems[i - 1].type) { + dividedItems.add(ToolbarItem.divider()); + } + dividedItems.add(filterItems[i]); + } + return dividedItems; + } } From 60a7557520064929692914b66bf5ee818ced6822 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Tue, 23 Aug 2022 10:30:56 +0800 Subject: [PATCH 014/106] fix: link text is only displayed if all selected text satisfy linked style --- .../src/extensions/text_node_extensions.dart | 34 +++++++++++++------ .../appflowy_editor/lib/src/infra/log.dart | 5 +++ .../lib/src/render/toolbar/toolbar_item.dart | 7 +++- .../lib/src/service/service.dart | 6 ++-- .../lib/src/service/toolbar_service.dart | 4 +-- ..._text_style_by_command_x_handler_test.dart | 27 ++++++++++++--- 6 files changed, 63 insertions(+), 20 deletions(-) diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/text_node_extensions.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/text_node_extensions.dart index 8416485a5a..3ca8c89f21 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/text_node_extensions.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/text_node_extensions.dart @@ -29,24 +29,34 @@ extension TextNodeExtension on TextNode { } bool allSatisfyLinkInSelection(Selection selection) => - allSatisfyInSelection(StyleKey.href, null, selection); + allSatisfyInSelection(StyleKey.href, selection, (value) { + return value != null; + }); bool allSatisfyBoldInSelection(Selection selection) => - allSatisfyInSelection(StyleKey.bold, true, selection); + allSatisfyInSelection(StyleKey.bold, selection, (value) { + return value == true; + }); bool allSatisfyItalicInSelection(Selection selection) => - allSatisfyInSelection(StyleKey.italic, true, selection); + allSatisfyInSelection(StyleKey.italic, selection, (value) { + return value == true; + }); bool allSatisfyUnderlineInSelection(Selection selection) => - allSatisfyInSelection(StyleKey.underline, true, selection); + allSatisfyInSelection(StyleKey.underline, selection, (value) { + return value == true; + }); bool allSatisfyStrikethroughInSelection(Selection selection) => - allSatisfyInSelection(StyleKey.strikethrough, true, selection); + allSatisfyInSelection(StyleKey.strikethrough, selection, (value) { + return value == true; + }); bool allSatisfyInSelection( String styleKey, - dynamic value, Selection selection, + bool Function(dynamic value) compare, ) { final ops = delta.whereType(); final startOffset = @@ -62,7 +72,7 @@ extension TextNodeExtension on TextNode { if (start < endOffset && start + length > startOffset) { if (op.attributes == null || !op.attributes!.containsKey(styleKey) || - op.attributes![styleKey] != value) { + !compare(op.attributes![styleKey])) { return false; } } @@ -116,13 +126,15 @@ extension TextNodesExtension on List { bool allSatisfyInSelection( String styleKey, Selection selection, - dynamic value, + dynamic matchValue, ) { if (isEmpty) { return false; } if (length == 1) { - return first.allSatisfyInSelection(styleKey, value, selection); + return first.allSatisfyInSelection(styleKey, selection, (value) { + return value == matchValue; + }); } else { for (var i = 0; i < length; i++) { final node = this[i]; @@ -142,7 +154,9 @@ extension TextNodesExtension on List { end: Position(path: node.path, offset: node.toRawString().length), ); } - if (!node.allSatisfyInSelection(styleKey, value, newSelection)) { + if (!node.allSatisfyInSelection(styleKey, newSelection, (value) { + return value == matchValue; + })) { return false; } } diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/infra/log.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/infra/log.dart index 2218b10181..8175ecb705 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/infra/log.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/infra/log.dart @@ -75,6 +75,11 @@ class Log { /// For example, uses the logger when processing scroll events. static Log scroll = Log._(name: 'scroll'); + /// For logging message related to [AppFlowyToolbarService]. + /// + /// For example, uses the logger when processing toolbar events. + static Log toolbar = Log._(name: 'toolbar'); + /// For logging message related to UI. /// /// For example, uses the logger when building the widget. diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item.dart index b2f6a9b2f7..e20ebb798d 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item.dart @@ -181,7 +181,12 @@ void _showLinkMenu(EditorState editorState, BuildContext context) { final length = (selection.start.offset - selection.end.offset).abs(); final node = editorState.service.selectionService.currentSelectedNodes.first as TextNode; - final linkText = node.getAttributeInSelection(selection, StyleKey.href); + final String linkText; + if (node.allSatisfyLinkInSelection(selection)) { + linkText = node.getAttributeInSelection(selection, StyleKey.href); + } else { + linkText = ''; + } _linkMenuOverlay = OverlayEntry(builder: (context) { return Positioned( top: matchRect.bottom + 5.0, diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/service.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/service.dart index e3436ea7ee..b9b6ac390a 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/service.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/service.dart @@ -36,10 +36,10 @@ class FlowyService { // toolbar service final toolbarServiceKey = GlobalKey(debugLabel: 'flowy_toolbar_service'); - FlowyToolbarService? get toolbarService { + AppFlowyToolbarService? get toolbarService { if (toolbarServiceKey.currentState != null && - toolbarServiceKey.currentState is FlowyToolbarService) { - return toolbarServiceKey.currentState! as FlowyToolbarService; + toolbarServiceKey.currentState is AppFlowyToolbarService) { + return toolbarServiceKey.currentState! as AppFlowyToolbarService; } return null; } diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/toolbar_service.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/toolbar_service.dart index 40914a70e9..e26a186387 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/toolbar_service.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/toolbar_service.dart @@ -5,7 +5,7 @@ import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/src/render/toolbar/toolbar_widget.dart'; import 'package:appflowy_editor/src/extensions/object_extensions.dart'; -abstract class FlowyToolbarService { +abstract class AppFlowyToolbarService { /// Show the toolbar widget beside the offset. void showInOffset(Offset offset, LayerLink layerLink); @@ -31,7 +31,7 @@ class FlowyToolbar extends StatefulWidget { } class _FlowyToolbarState extends State - implements FlowyToolbarService { + implements AppFlowyToolbarService { OverlayEntry? _toolbarOverlay; final _toolbarWidgetKey = GlobalKey(debugLabel: '_toolbar_widget'); diff --git a/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/update_text_style_by_command_x_handler_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/update_text_style_by_command_x_handler_test.dart index 2e93d4c5f5..f27b11b37d 100644 --- a/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/update_text_style_by_command_x_handler_test.dart +++ b/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/update_text_style_by_command_x_handler_test.dart @@ -82,7 +82,14 @@ Future _testUpdateTextStyleByCommandX( ); var textNode = editor.nodeAtPath([1]) as TextNode; expect( - textNode.allSatisfyInSelection(matchStyle, matchValue, selection), true); + textNode.allSatisfyInSelection( + matchStyle, + selection, + (value) { + return value == matchValue; + }, + ), + true); selection = Selection.single(path: [1], startOffset: 0, endOffset: text.length); @@ -94,7 +101,14 @@ Future _testUpdateTextStyleByCommandX( ); textNode = editor.nodeAtPath([1]) as TextNode; expect( - textNode.allSatisfyInSelection(matchStyle, matchValue, selection), true); + textNode.allSatisfyInSelection( + matchStyle, + selection, + (value) { + return value == matchValue; + }, + ), + true); await editor.updateSelection(selection); await editor.pressLogicKey( @@ -123,9 +137,14 @@ Future _testUpdateTextStyleByCommandX( expect( node.allSatisfyInSelection( matchStyle, - matchValue, Selection.single( - path: node.path, startOffset: 0, endOffset: text.length), + path: node.path, + startOffset: 0, + endOffset: text.length, + ), + (value) { + return value == matchValue; + }, ), true, ); From 1262457755865166142c0009f9b92d6251dd99d0 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Tue, 23 Aug 2022 11:29:05 +0800 Subject: [PATCH 015/106] feat: add link menu test --- .../lib/src/render/link_menu/link_menu.dart | 29 ++++++------- .../lib/src/render/toolbar/toolbar_item.dart | 4 +- .../test/render/link_menu/link_menu_test.dart | 41 +++++++++++++++++++ 3 files changed, 57 insertions(+), 17 deletions(-) create mode 100644 frontend/app_flowy/packages/appflowy_editor/test/render/link_menu/link_menu_test.dart diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/link_menu/link_menu.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/link_menu/link_menu.dart index 3a519ed70f..2c4ba2b975 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/link_menu/link_menu.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/link_menu/link_menu.dart @@ -40,24 +40,25 @@ class _LinkMenuState extends State { @override Widget build(BuildContext context) { - return Container( - decoration: BoxDecoration( - color: Colors.white, - boxShadow: [ - BoxShadow( - blurRadius: 5, - spreadRadius: 1, - color: Colors.black.withOpacity(0.1), - ), - ], - borderRadius: BorderRadius.circular(6.0), - ), - child: SizedBox( - width: 350, + return SizedBox( + width: 350, + child: Container( + decoration: BoxDecoration( + color: Colors.white, + boxShadow: [ + BoxShadow( + blurRadius: 5, + spreadRadius: 1, + color: Colors.black.withOpacity(0.1), + ), + ], + borderRadius: BorderRadius.circular(6.0), + ), child: Padding( padding: const EdgeInsets.all(20.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, children: [ _buildHeader(), const SizedBox(height: 16.0), diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item.dart index e20ebb798d..41283e6b6a 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item.dart @@ -181,11 +181,9 @@ void _showLinkMenu(EditorState editorState, BuildContext context) { final length = (selection.start.offset - selection.end.offset).abs(); final node = editorState.service.selectionService.currentSelectedNodes.first as TextNode; - final String linkText; + String? linkText; if (node.allSatisfyLinkInSelection(selection)) { linkText = node.getAttributeInSelection(selection, StyleKey.href); - } else { - linkText = ''; } _linkMenuOverlay = OverlayEntry(builder: (context) { return Positioned( diff --git a/frontend/app_flowy/packages/appflowy_editor/test/render/link_menu/link_menu_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/render/link_menu/link_menu_test.dart new file mode 100644 index 0000000000..7b4541033b --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/test/render/link_menu/link_menu_test.dart @@ -0,0 +1,41 @@ +import 'package:appflowy_editor/src/render/link_menu/link_menu.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() async { + setUpAll(() { + TestWidgetsFlutterBinding.ensureInitialized(); + }); + + group('link_menu.dart', () { + testWidgets('test empty link menu actions', (tester) async { + const link = 'appflowy.io'; + var submittedText = ''; + final linkMenu = LinkMenu( + onCopyLink: () {}, + onRemoveLink: () {}, + onSubmitted: (text) { + submittedText = text; + }, + ); + await tester.pumpWidget( + MaterialApp( + home: Material( + child: linkMenu, + ), + ), + ); + + expect(find.byType(TextButton), findsNothing); + expect(find.byType(TextField), findsOneWidget); + + await tester.tap(find.byType(TextField)); + await tester.enterText(find.byType(TextField), link); + await tester.pumpAndSettle(); + await tester.testTextInput.receiveAction(TextInputAction.done); + await tester.pumpAndSettle(); + + expect(submittedText, link); + }); + }); +} From 104b4cca9def05cd5d8f99a791055997ce217d70 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Tue, 23 Aug 2022 13:25:05 +0800 Subject: [PATCH 016/106] test: test the toolbar_service, toolbar_*.dart --- .../lib/src/render/toolbar/toolbar_item.dart | 3 +- .../test/infra/test_raw_key_event.dart | 3 + .../toolbar/toolbar_item_widget_test.dart | 46 +++++++++++ .../render/toolbar/toolbar_widget_test.dart | 11 +++ ..._text_style_by_command_x_handler_test.dart | 79 +++++++++++++++++++ .../test/service/toolbar_service_test.dart | 36 +++++++++ 6 files changed, 176 insertions(+), 2 deletions(-) create mode 100644 frontend/app_flowy/packages/appflowy_editor/test/render/toolbar/toolbar_item_widget_test.dart create mode 100644 frontend/app_flowy/packages/appflowy_editor/test/render/toolbar/toolbar_widget_test.dart create mode 100644 frontend/app_flowy/packages/appflowy_editor/test/service/toolbar_service_test.dart diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item.dart index 41283e6b6a..107ae23b6f 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item.dart @@ -158,8 +158,6 @@ ToolbarShowValidator _showInTextSelection = (editorState) { OverlayEntry? _linkMenuOverlay; EditorState? _editorState; void _showLinkMenu(EditorState editorState, BuildContext context) { - _editorState = editorState; - final rects = editorState.service.selectionService.selectionRects; var maxBottom = 0.0; late Rect matchRect; @@ -171,6 +169,7 @@ void _showLinkMenu(EditorState editorState, BuildContext context) { } _dismissLinkMenu(); + _editorState = editorState; // Since the link menu will only show in single text selection, // We get the text node directly instead of judging details again. diff --git a/frontend/app_flowy/packages/appflowy_editor/test/infra/test_raw_key_event.dart b/frontend/app_flowy/packages/appflowy_editor/test/infra/test_raw_key_event.dart index 150a3e2d00..2450c4e6db 100644 --- a/frontend/app_flowy/packages/appflowy_editor/test/infra/test_raw_key_event.dart +++ b/frontend/app_flowy/packages/appflowy_editor/test/infra/test_raw_key_event.dart @@ -115,6 +115,9 @@ extension on LogicalKeyboardKey { if (this == LogicalKeyboardKey.keyI) { return PhysicalKeyboardKey.keyI; } + if (this == LogicalKeyboardKey.keyK) { + return PhysicalKeyboardKey.keyK; + } if (this == LogicalKeyboardKey.keyS) { return PhysicalKeyboardKey.keyS; } diff --git a/frontend/app_flowy/packages/appflowy_editor/test/render/toolbar/toolbar_item_widget_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/render/toolbar/toolbar_item_widget_test.dart new file mode 100644 index 0000000000..87ae922d91 --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/test/render/toolbar/toolbar_item_widget_test.dart @@ -0,0 +1,46 @@ +import 'package:appflowy_editor/src/render/toolbar/toolbar_item.dart'; +import 'package:appflowy_editor/src/render/toolbar/toolbar_item_widget.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() async { + setUpAll(() { + TestWidgetsFlutterBinding.ensureInitialized(); + }); + + group('toolbar_item_widget.dart', () { + testWidgets('test single toolbar item widget', (tester) async { + final key = GlobalKey(); + var hit = false; + final item = ToolbarItem( + id: 'appflowy.toolbar.test', + type: 1, + icon: const Icon(Icons.abc), + validator: (editorState) => true, + handler: (editorState, context) {}, + ); + final widget = ToolbarItemWidget( + key: key, + item: item, + onPressed: (() { + hit = true; + }), + ); + + await tester.pumpWidget( + MaterialApp( + home: Material( + child: widget, + ), + ), + ); + + expect(find.byKey(key), findsOneWidget); + + await tester.tap(find.byKey(key)); + await tester.pumpAndSettle(); + + expect(hit, true); + }); + }); +} diff --git a/frontend/app_flowy/packages/appflowy_editor/test/render/toolbar/toolbar_widget_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/render/toolbar/toolbar_widget_test.dart new file mode 100644 index 0000000000..d7e6b906f8 --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/test/render/toolbar/toolbar_widget_test.dart @@ -0,0 +1,11 @@ +import 'package:flutter_test/flutter_test.dart'; + +void main() async { + setUpAll(() { + TestWidgetsFlutterBinding.ensureInitialized(); + }); + + group('toolbar_widget.dart', () { + testWidgets('test toolbar widget', (tester) async {}); + }); +} diff --git a/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/update_text_style_by_command_x_handler_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/update_text_style_by_command_x_handler_test.dart index f27b11b37d..e29308ebbc 100644 --- a/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/update_text_style_by_command_x_handler_test.dart +++ b/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/update_text_style_by_command_x_handler_test.dart @@ -1,6 +1,10 @@ import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_editor/src/render/link_menu/link_menu.dart'; import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart'; import 'package:appflowy_editor/src/extensions/text_node_extensions.dart'; +import 'package:appflowy_editor/src/render/toolbar/toolbar_item.dart'; +import 'package:appflowy_editor/src/render/toolbar/toolbar_widget.dart'; +import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import '../../infra/test_editor.dart'; @@ -54,6 +58,10 @@ void main() async { LogicalKeyboardKey.keyH, ); }); + + testWidgets('Presses Command + K to trigger link menu', (tester) async { + await _testLinkMenuInSingleTextSelection(tester); + }); }); } @@ -171,3 +179,74 @@ Future _testUpdateTextStyleByCommandX( ); } } + +Future _testLinkMenuInSingleTextSelection(WidgetTester tester) async { + const link = 'appflowy.io'; + const text = 'Welcome to Appflowy 😁'; + final editor = tester.editor + ..insertTextNode(text) + ..insertTextNode(text) + ..insertTextNode(text); + await editor.startTesting(); + + final selection = + Selection.single(path: [1], startOffset: 0, endOffset: text.length); + await editor.updateSelection(selection); + + // show toolbar + expect(find.byType(ToolbarWidget), findsOneWidget); + + final item = defaultToolbarItems + .where((item) => item.id == 'appflowy.toolbar.link') + .first; + expect(find.byWidget(item.icon), findsOneWidget); + + // trigger the link menu + await editor.pressLogicKey(LogicalKeyboardKey.keyK, isMetaPressed: true); + + expect(find.byType(LinkMenu), findsOneWidget); + + await tester.enterText(find.byType(TextField), link); + await tester.testTextInput.receiveAction(TextInputAction.done); + await tester.pumpAndSettle(); + + expect(find.byType(LinkMenu), findsNothing); + + final node = editor.nodeAtPath([1]) as TextNode; + expect( + node.allSatisfyInSelection( + StyleKey.href, + selection, + (value) => value == link, + ), + true); + + await editor.updateSelection(selection); + await editor.pressLogicKey(LogicalKeyboardKey.keyK, isMetaPressed: true); + expect(find.byType(LinkMenu), findsOneWidget); + expect( + find.text(link, findRichText: true, skipOffstage: false), findsOneWidget); + + // Copy link + final copyLink = find.text('Copy link'); + expect(copyLink, findsOneWidget); + await tester.tap(copyLink); + await tester.pumpAndSettle(); + expect(find.byType(LinkMenu), findsNothing); + + // Remove link + await editor.pressLogicKey(LogicalKeyboardKey.keyK, isMetaPressed: true); + final removeLink = find.text('Remove link'); + expect(removeLink, findsOneWidget); + await tester.tap(removeLink); + await tester.pumpAndSettle(); + expect(find.byType(LinkMenu), findsNothing); + + expect( + node.allSatisfyInSelection( + StyleKey.href, + selection, + (value) => value == link, + ), + false); +} diff --git a/frontend/app_flowy/packages/appflowy_editor/test/service/toolbar_service_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/service/toolbar_service_test.dart new file mode 100644 index 0000000000..9d833095e7 --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/test/service/toolbar_service_test.dart @@ -0,0 +1,36 @@ +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_editor/src/render/toolbar/toolbar_item.dart'; +import 'package:appflowy_editor/src/render/toolbar/toolbar_widget.dart'; +import 'package:flutter_test/flutter_test.dart'; +import '../infra/test_editor.dart'; + +void main() async { + setUpAll(() { + TestWidgetsFlutterBinding.ensureInitialized(); + }); + + group('toolbar_service.dart', () { + testWidgets('Test toolbar service in multi text selection', (tester) async { + const text = 'Welcome to Appflowy 😁'; + final editor = tester.editor + ..insertTextNode(text) + ..insertTextNode(text) + ..insertTextNode(text); + await editor.startTesting(); + + final selection = Selection( + start: Position(path: [0], offset: 0), + end: Position(path: [1], offset: text.length), + ); + await editor.updateSelection(selection); + + expect(find.byType(ToolbarWidget), findsOneWidget); + + // no link item + final item = defaultToolbarItems + .where((item) => item.id == 'appflowy.toolbar.link') + .first; + expect(find.byWidget(item.icon), findsNothing); + }); + }); +} From a2c4f73e7d9cddd7a7fff01f2771403ceea15ec7 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Tue, 23 Aug 2022 13:44:01 +0800 Subject: [PATCH 017/106] feat: add clear icon into link menu --- .../appflowy_editor/assets/images/clear.svg | 5 +++++ .../lib/src/render/link_menu/link_menu.dart | 21 ++++++++++++++----- 2 files changed, 21 insertions(+), 5 deletions(-) create mode 100644 frontend/app_flowy/packages/appflowy_editor/assets/images/clear.svg diff --git a/frontend/app_flowy/packages/appflowy_editor/assets/images/clear.svg b/frontend/app_flowy/packages/appflowy_editor/assets/images/clear.svg new file mode 100644 index 0000000000..7f303d737f --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/assets/images/clear.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/link_menu/link_menu.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/link_menu/link_menu.dart index 2c4ba2b975..a33adf3b8c 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/link_menu/link_menu.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/link_menu/link_menu.dart @@ -100,15 +100,26 @@ class _LinkMenuState extends State { textAlign: TextAlign.left, controller: _textEditingController, onSubmitted: widget.onSubmitted, - decoration: const InputDecoration( + decoration: InputDecoration( hintText: 'URL', - hintStyle: TextStyle(fontSize: 14.0), - border: OutlineInputBorder( + hintStyle: const TextStyle(fontSize: 14.0), + contentPadding: const EdgeInsets.all(16.0), + isDense: true, + suffixIcon: IconButton( + padding: const EdgeInsets.all(4.0), + icon: const FlowySvg( + name: 'clear', + width: 24, + height: 24, + ), + onPressed: () { + _textEditingController.clear(); + }, + ), + border: const OutlineInputBorder( borderRadius: BorderRadius.all(Radius.circular(12.0)), borderSide: BorderSide(color: Color(0xFFBDBDBD)), ), - contentPadding: EdgeInsets.all(16.0), - isDense: true, ), ); } From 82f1f0e3e3da813991b9d2b29656314add038969 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Tue, 23 Aug 2022 20:24:54 +0800 Subject: [PATCH 018/106] feat: support resize, copy, align, delete in image ndoe widget --- .../images/image_toolbar/align_center.svg | 5 + .../images/image_toolbar/align_left.svg | 5 + .../images/image_toolbar/align_right.svg | 5 + .../assets/images/image_toolbar/copy.svg | 4 + .../assets/images/image_toolbar/delete.svg | 6 + .../assets/images/image_toolbar/divider.svg | 3 + .../assets/images/image_toolbar/share.svg | 4 + .../example/assets/example.json | 10 +- .../appflowy_editor/example/lib/main.dart | 4 +- .../src/render/image/image_node_builder.dart | 64 ++++ .../src/render/image/image_node_widget.dart | 276 ++++++++++++++++++ .../lib/src/service/editor_service.dart | 2 + .../packages/appflowy_editor/pubspec.yaml | 1 + 13 files changed, 386 insertions(+), 3 deletions(-) create mode 100644 frontend/app_flowy/packages/appflowy_editor/assets/images/image_toolbar/align_center.svg create mode 100644 frontend/app_flowy/packages/appflowy_editor/assets/images/image_toolbar/align_left.svg create mode 100644 frontend/app_flowy/packages/appflowy_editor/assets/images/image_toolbar/align_right.svg create mode 100644 frontend/app_flowy/packages/appflowy_editor/assets/images/image_toolbar/copy.svg create mode 100644 frontend/app_flowy/packages/appflowy_editor/assets/images/image_toolbar/delete.svg create mode 100644 frontend/app_flowy/packages/appflowy_editor/assets/images/image_toolbar/divider.svg create mode 100644 frontend/app_flowy/packages/appflowy_editor/assets/images/image_toolbar/share.svg create mode 100644 frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_builder.dart create mode 100644 frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_widget.dart diff --git a/frontend/app_flowy/packages/appflowy_editor/assets/images/image_toolbar/align_center.svg b/frontend/app_flowy/packages/appflowy_editor/assets/images/image_toolbar/align_center.svg new file mode 100644 index 0000000000..ae9c2cfd44 --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/assets/images/image_toolbar/align_center.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/app_flowy/packages/appflowy_editor/assets/images/image_toolbar/align_left.svg b/frontend/app_flowy/packages/appflowy_editor/assets/images/image_toolbar/align_left.svg new file mode 100644 index 0000000000..b4f2d0101e --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/assets/images/image_toolbar/align_left.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/app_flowy/packages/appflowy_editor/assets/images/image_toolbar/align_right.svg b/frontend/app_flowy/packages/appflowy_editor/assets/images/image_toolbar/align_right.svg new file mode 100644 index 0000000000..86a1facaac --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/assets/images/image_toolbar/align_right.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/app_flowy/packages/appflowy_editor/assets/images/image_toolbar/copy.svg b/frontend/app_flowy/packages/appflowy_editor/assets/images/image_toolbar/copy.svg new file mode 100644 index 0000000000..101cf34205 --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/assets/images/image_toolbar/copy.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/app_flowy/packages/appflowy_editor/assets/images/image_toolbar/delete.svg b/frontend/app_flowy/packages/appflowy_editor/assets/images/image_toolbar/delete.svg new file mode 100644 index 0000000000..5a3d972872 --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/assets/images/image_toolbar/delete.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/frontend/app_flowy/packages/appflowy_editor/assets/images/image_toolbar/divider.svg b/frontend/app_flowy/packages/appflowy_editor/assets/images/image_toolbar/divider.svg new file mode 100644 index 0000000000..3e57a6b000 --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/assets/images/image_toolbar/divider.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/app_flowy/packages/appflowy_editor/assets/images/image_toolbar/share.svg b/frontend/app_flowy/packages/appflowy_editor/assets/images/image_toolbar/share.svg new file mode 100644 index 0000000000..279e7ac471 --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/assets/images/image_toolbar/share.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/app_flowy/packages/appflowy_editor/example/assets/example.json b/frontend/app_flowy/packages/appflowy_editor/example/assets/example.json index 901e57f796..99943dea38 100644 --- a/frontend/app_flowy/packages/appflowy_editor/example/assets/example.json +++ b/frontend/app_flowy/packages/appflowy_editor/example/assets/example.json @@ -6,7 +6,8 @@ { "type": "image", "attributes": { - "image_src": "https://images.squarespace-cdn.com/content/v1/617f6f16b877c06711e87373/c3f23723-37f4-44d7-9c5d-6e2a53064ae7/Asset+10.png" + "image_src": "https://images.squarespace-cdn.com/content/v1/617f6f16b877c06711e87373/c3f23723-37f4-44d7-9c5d-6e2a53064ae7/Asset+10.png", + "align": "center" } }, { @@ -58,6 +59,13 @@ } ] }, + { + "type": "image", + "attributes": { + "image_src": "https://images.unsplash.com/photo-1616530940355-351fabd9524b?ixlib=rb-1.2.1&q=80&cs=tinysrgb&fm=jpg", + "align": "center" + } + }, { "type": "text", "delta": [ diff --git a/frontend/app_flowy/packages/appflowy_editor/example/lib/main.dart b/frontend/app_flowy/packages/appflowy_editor/example/lib/main.dart index e17088c954..539bb74425 100644 --- a/frontend/app_flowy/packages/appflowy_editor/example/lib/main.dart +++ b/frontend/app_flowy/packages/appflowy_editor/example/lib/main.dart @@ -4,7 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'expandable_floating_action_button.dart'; -import 'plugin/image_node_widget.dart'; +// import 'plugin/image_node_widget.dart'; import 'plugin/youtube_link_node_widget.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; @@ -139,7 +139,7 @@ class _MyHomePageState extends State { editorState: editorState, keyEventHandlers: const [], customBuilders: { - 'image': ImageNodeBuilder(), + // 'image': ImageNodeBuilder(), 'youtube_link': YouTubeLinkNodeBuilder() }, ), diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_builder.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_builder.dart new file mode 100644 index 0000000000..5824849540 --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_builder.dart @@ -0,0 +1,64 @@ +import 'package:appflowy_editor/src/document/node.dart'; +import 'package:appflowy_editor/src/operation/transaction_builder.dart'; +import 'package:appflowy_editor/src/service/render_plugin_service.dart'; +import 'package:flutter/material.dart'; +import 'package:rich_clipboard/rich_clipboard.dart'; + +import 'image_node_widget.dart'; + +class ImageNodeBuilder extends NodeWidgetBuilder { + @override + Widget build(NodeWidgetContext context) { + final src = context.node.attributes['image_src']; + final align = context.node.attributes['align']; + return ImageNodeWidget( + key: context.node.key, + src: src, + alignment: _textToAlignment(align), + onCopy: () { + RichClipboard.setData(RichClipboardData(text: src)); + }, + onDelete: () { + TransactionBuilder(context.editorState) + ..deleteNode(context.node) + ..commit(); + }, + onAlign: (alignment) { + TransactionBuilder(context.editorState) + ..updateNode(context.node, { + 'align': _alignmentToText(alignment), + }) + ..commit(); + }, + ); + } + + @override + NodeValidator get nodeValidator => ((node) { + return node.type == 'image' && + node.attributes.containsKey('image_src') && + node.attributes.containsKey('align'); + }); + + Alignment _textToAlignment(String text) { + if (text == 'center') { + return Alignment.center; + } else if (text == 'left') { + return Alignment.centerLeft; + } else if (text == 'right') { + return Alignment.centerRight; + } + throw UnimplementedError(); + } + + String _alignmentToText(Alignment alignment) { + if (alignment == Alignment.center) { + return 'center'; + } else if (alignment == Alignment.centerLeft) { + return 'left'; + } else if (alignment == Alignment.centerRight) { + return 'right'; + } + throw UnimplementedError(); + } +} diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_widget.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_widget.dart new file mode 100644 index 0000000000..12b22787ff --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_widget.dart @@ -0,0 +1,276 @@ +import 'package:appflowy_editor/src/infra/flowy_svg.dart'; +import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart'; +import 'package:flutter/material.dart'; + +class ImageNodeWidget extends StatefulWidget { + const ImageNodeWidget({ + Key? key, + required this.src, + required this.alignment, + required this.onCopy, + required this.onDelete, + required this.onAlign, + }) : super(key: key); + + final String src; + final Alignment alignment; + final VoidCallback onCopy; + final VoidCallback onDelete; + final void Function(Alignment alignment) onAlign; + + @override + State createState() => _ImageNodeWidgetState(); +} + +class _ImageNodeWidgetState extends State { + double? imageWidth = defaultMaxTextNodeWidth; + double _initial = 0; + double _distance = 0; + bool _onFocus = false; + + @override + Widget build(BuildContext context) { + // only support network image. + return _buildNetworkImage(context); + } + + Widget _buildNetworkImage(BuildContext context) { + return Align( + alignment: widget.alignment, + child: MouseRegion( + onEnter: (event) => setState(() { + _onFocus = true; + }), + onExit: (event) => setState(() { + _onFocus = false; + }), + child: _buildResizableImage(context), + ), + ); + } + + Widget _buildResizableImage(BuildContext context) { + final networkImage = Image.network( + widget.src, + width: imageWidth == null ? null : imageWidth! - _distance, + loadingBuilder: (context, child, loadingProgress) => + loadingProgress == null + ? child + : SizedBox( + width: imageWidth, + height: 300, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox.fromSize( + size: const Size(18, 18), + child: const CircularProgressIndicator(), + ), + SizedBox.fromSize( + size: const Size(10, 10), + ), + const Text('Loading'), + ], + ), + ), + ); + if (imageWidth == null) { + networkImage.image.resolve(const ImageConfiguration()).addListener( + ImageStreamListener( + (image, _) { + imageWidth = image.image.width.toDouble(); + }, + ), + ); + } + return Stack( + children: [ + networkImage, + _buildEdgeGesture( + context, + top: 0, + left: 0, + bottom: 0, + width: 5, + onUpdate: (distance) { + setState(() { + _distance = distance; + }); + }, + ), + _buildEdgeGesture( + context, + top: 0, + right: 0, + bottom: 0, + width: 5, + onUpdate: (distance) { + setState(() { + _distance = -distance; + }); + }, + ), + if (_onFocus) + _buildImageToolbar( + context, + top: 8, + right: 8, + height: 30, + ), + ], + ); + } + + Widget _buildEdgeGesture( + BuildContext context, { + double? top, + double? left, + double? right, + double? bottom, + double? width, + void Function(double distance)? onUpdate, + }) { + return Positioned( + top: top, + left: left, + right: right, + bottom: bottom, + width: width, + child: GestureDetector( + onHorizontalDragStart: (details) { + _initial = details.globalPosition.dx; + }, + onHorizontalDragUpdate: (details) { + if (onUpdate != null) { + onUpdate(details.globalPosition.dx - _initial); + } + }, + onHorizontalDragEnd: (details) { + imageWidth = imageWidth! - _distance; + _initial = 0; + _distance = 0; + }, + child: MouseRegion( + cursor: SystemMouseCursors.resizeLeftRight, + child: _onFocus + ? Center( + child: Container( + height: 40, + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.2), + borderRadius: const BorderRadius.all( + Radius.circular(5.0), + ), + ), + ), + ) + : null, + ), + ), + ); + } + + Widget _buildImageToolbar( + BuildContext context, { + double? top, + double? left, + double? right, + double? width, + double? height, + }) { + return Positioned( + top: top, + left: left, + right: right, + width: width, + height: height, + child: Container( + decoration: BoxDecoration( + color: const Color(0xFF333333), + boxShadow: [ + BoxShadow( + blurRadius: 5, + spreadRadius: 1, + color: Colors.black.withOpacity(0.1), + ), + ], + borderRadius: BorderRadius.circular(8.0), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + IconButton( + hoverColor: Colors.transparent, + constraints: const BoxConstraints(), + padding: const EdgeInsets.fromLTRB(6.0, 4.0, 0.0, 4.0), + icon: FlowySvg( + name: 'image_toolbar/align_left', + color: widget.alignment == Alignment.centerLeft + ? const Color(0xFF00BCF0) + : null, + ), + onPressed: () { + widget.onAlign(Alignment.centerLeft); + }, + ), + IconButton( + hoverColor: Colors.transparent, + constraints: const BoxConstraints(), + padding: const EdgeInsets.fromLTRB(0.0, 4.0, 0.0, 4.0), + icon: FlowySvg( + name: 'image_toolbar/align_center', + color: widget.alignment == Alignment.center + ? const Color(0xFF00BCF0) + : null, + ), + onPressed: () { + widget.onAlign(Alignment.center); + }, + ), + IconButton( + hoverColor: Colors.transparent, + constraints: const BoxConstraints(), + padding: const EdgeInsets.fromLTRB(0.0, 4.0, 4.0, 4.0), + icon: FlowySvg( + name: 'image_toolbar/align_right', + color: widget.alignment == Alignment.centerRight + ? const Color(0xFF00BCF0) + : null, + ), + onPressed: () { + widget.onAlign(Alignment.centerRight); + }, + ), + const Center( + child: FlowySvg( + name: 'image_toolbar/divider', + ), + ), + IconButton( + hoverColor: Colors.transparent, + constraints: const BoxConstraints(), + padding: const EdgeInsets.fromLTRB(4.0, 4.0, 0.0, 4.0), + icon: const FlowySvg( + name: 'image_toolbar/copy', + ), + onPressed: () { + widget.onCopy(); + }, + ), + IconButton( + hoverColor: Colors.transparent, + constraints: const BoxConstraints(), + padding: const EdgeInsets.fromLTRB(0.0, 4.0, 6.0, 4.0), + icon: const FlowySvg( + name: 'image_toolbar/delete', + ), + onPressed: () { + widget.onDelete(); + }, + ), + ], + ), + ), + ); + } +} diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/editor_service.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/editor_service.dart index d7b4f33914..2781471b46 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/editor_service.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/editor_service.dart @@ -1,3 +1,4 @@ +import 'package:appflowy_editor/src/render/image/image_node_builder.dart'; import 'package:appflowy_editor/src/render/selection_menu/selection_menu_widget.dart'; import 'package:appflowy_editor/src/service/internal_key_event_handlers/default_key_event_handlers.dart'; import 'package:flutter/material.dart'; @@ -25,6 +26,7 @@ NodeWidgetBuilders defaultBuilders = { 'text/bulleted-list': BulletedListTextNodeWidgetBuilder(), 'text/number-list': NumberListTextNodeWidgetBuilder(), 'text/quote': QuotedTextNodeWidgetBuilder(), + 'image': ImageNodeBuilder(), }; class AppFlowyEditor extends StatefulWidget { diff --git a/frontend/app_flowy/packages/appflowy_editor/pubspec.yaml b/frontend/app_flowy/packages/appflowy_editor/pubspec.yaml index 33f443d066..99e431d9f1 100644 --- a/frontend/app_flowy/packages/appflowy_editor/pubspec.yaml +++ b/frontend/app_flowy/packages/appflowy_editor/pubspec.yaml @@ -32,6 +32,7 @@ flutter: assets: - assets/images/toolbar/ - assets/images/selection_menu/ + - assets/images/image_toolbar/ - assets/images/ # # For details regarding assets in packages, see From 8855822fd16a1b19f548659488f5bbf50acb9686 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Tue, 23 Aug 2022 20:34:15 +0800 Subject: [PATCH 019/106] chore: rename compare to test --- .../lib/src/extensions/text_node_extensions.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/text_node_extensions.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/text_node_extensions.dart index 3ca8c89f21..119cbae8d2 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/text_node_extensions.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/text_node_extensions.dart @@ -56,7 +56,7 @@ extension TextNodeExtension on TextNode { bool allSatisfyInSelection( String styleKey, Selection selection, - bool Function(dynamic value) compare, + bool Function(dynamic value) test, ) { final ops = delta.whereType(); final startOffset = @@ -72,7 +72,7 @@ extension TextNodeExtension on TextNode { if (start < endOffset && start + length > startOffset) { if (op.attributes == null || !op.attributes!.containsKey(styleKey) || - !compare(op.attributes![styleKey])) { + !test(op.attributes![styleKey])) { return false; } } From 012c3a851a5cccbab9cb2c6dffa34c11e6f15d29 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Tue, 23 Aug 2022 22:54:47 +0800 Subject: [PATCH 020/106] test: interge network_image_mock and add image node widget test --- .../example/assets/example.json | 2 +- .../packages/appflowy_editor/pubspec.yaml | 1 + .../test/infra/test_editor.dart | 13 ++ .../render/image/image_node_widget_test.dart | 131 ++++++++++++++++++ 4 files changed, 146 insertions(+), 1 deletion(-) create mode 100644 frontend/app_flowy/packages/appflowy_editor/test/render/image/image_node_widget_test.dart diff --git a/frontend/app_flowy/packages/appflowy_editor/example/assets/example.json b/frontend/app_flowy/packages/appflowy_editor/example/assets/example.json index 99943dea38..e9edcfa268 100644 --- a/frontend/app_flowy/packages/appflowy_editor/example/assets/example.json +++ b/frontend/app_flowy/packages/appflowy_editor/example/assets/example.json @@ -62,7 +62,7 @@ { "type": "image", "attributes": { - "image_src": "https://images.unsplash.com/photo-1616530940355-351fabd9524b?ixlib=rb-1.2.1&q=80&cs=tinysrgb&fm=jpg", + "image_src": "https://images.unsplash.com/photo-1471897488648-5eae4ac6686b?ixlib=rb-1.2.1&dl=sarah-dorweiler-QeVmJxZOv3k-unsplash.jpg&w=640&q=80&fm=jpg&crop=entropy&cs=tinysrgb", "align": "center" } }, diff --git a/frontend/app_flowy/packages/appflowy_editor/pubspec.yaml b/frontend/app_flowy/packages/appflowy_editor/pubspec.yaml index 99e431d9f1..9eb730f213 100644 --- a/frontend/app_flowy/packages/appflowy_editor/pubspec.yaml +++ b/frontend/app_flowy/packages/appflowy_editor/pubspec.yaml @@ -22,6 +22,7 @@ dev_dependencies: flutter_test: sdk: flutter flutter_lints: ^2.0.0 + network_image_mock: ^2.1.1 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/frontend/app_flowy/packages/appflowy_editor/test/infra/test_editor.dart b/frontend/app_flowy/packages/appflowy_editor/test/infra/test_editor.dart index 8c89b603aa..a815d91875 100644 --- a/frontend/app_flowy/packages/appflowy_editor/test/infra/test_editor.dart +++ b/frontend/app_flowy/packages/appflowy_editor/test/infra/test_editor.dart @@ -57,6 +57,19 @@ class EditorWidgetTester { ); } + void insertImageNode(String src, {String? align}) { + insert( + Node( + type: 'image', + children: LinkedList(), + attributes: { + 'image_src': src, + 'align': align ?? 'center', + }, + ), + ); + } + Node? nodeAtPath(Path path) { return root.childAtPath(path); } diff --git a/frontend/app_flowy/packages/appflowy_editor/test/render/image/image_node_widget_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/render/image/image_node_widget_test.dart new file mode 100644 index 0000000000..7829c456eb --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/test/render/image/image_node_widget_test.dart @@ -0,0 +1,131 @@ +import 'package:appflowy_editor/src/render/image/image_node_widget.dart'; +import 'package:appflowy_editor/src/service/editor_service.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:network_image_mock/network_image_mock.dart'; + +import '../../infra/test_editor.dart'; + +void main() async { + setUpAll(() { + TestWidgetsFlutterBinding.ensureInitialized(); + }); + + group('image_node_widget.dart', () { + testWidgets('render image node', (tester) async { + mockNetworkImagesFor(() async { + const text = 'Welcome to Appflowy 😁'; + const src = + 'https://images.unsplash.com/photo-1471897488648-5eae4ac6686b?ixlib=rb-1.2.1&dl=sarah-dorweiler-QeVmJxZOv3k-unsplash.jpg&w=640&q=80&fm=jpg&crop=entropy&cs=tinysrgb'; + final editor = tester.editor + ..insertTextNode(text) + ..insertImageNode(src) + ..insertTextNode(text); + await editor.startTesting(); + + expect(editor.documentLength, 3); + expect(find.byType(Image), findsOneWidget); + }); + }); + + testWidgets('render image align', (tester) async { + mockNetworkImagesFor(() async { + const text = 'Welcome to Appflowy 😁'; + const src = + 'https://images.unsplash.com/photo-1471897488648-5eae4ac6686b?ixlib=rb-1.2.1&dl=sarah-dorweiler-QeVmJxZOv3k-unsplash.jpg&w=640&q=80&fm=jpg&crop=entropy&cs=tinysrgb'; + final editor = tester.editor + ..insertTextNode(text) + ..insertImageNode(src, align: 'left') + ..insertImageNode(src, align: 'center') + ..insertImageNode(src, align: 'right') + ..insertTextNode(text); + await editor.startTesting(); + + expect(editor.documentLength, 5); + final imageFinder = find.byType(Image); + expect(imageFinder, findsNWidgets(3)); + + final editorFinder = find.byType(AppFlowyEditor); + final editorRect = tester.getRect(editorFinder); + + final leftImageRect = tester.getRect(imageFinder.at(0)); + expect(leftImageRect.left, editorRect.left); + final rightImageRect = tester.getRect(imageFinder.at(2)); + expect(rightImageRect.right, editorRect.right); + final centerImageRect = tester.getRect(imageFinder.at(1)); + expect(centerImageRect.left, + (leftImageRect.left + rightImageRect.left) / 2.0); + expect(leftImageRect.size, centerImageRect.size); + expect(rightImageRect.size, centerImageRect.size); + + final imageNodeWidgetFinder = find.byType(ImageNodeWidget); + + final leftImage = + tester.firstWidget(imageNodeWidgetFinder) as ImageNodeWidget; + + leftImage.onAlign(Alignment.center); + await tester.pump(const Duration(milliseconds: 100)); + expect( + tester.getRect(imageFinder.at(0)).left, + centerImageRect.left, + ); + + leftImage.onAlign(Alignment.centerRight); + await tester.pump(const Duration(milliseconds: 100)); + expect( + tester.getRect(imageFinder.at(0)).left, + rightImageRect.left, + ); + }); + }); + + testWidgets('render image copy', (tester) async { + mockNetworkImagesFor(() async { + const text = 'Welcome to Appflowy 😁'; + const src = + 'https://images.unsplash.com/photo-1471897488648-5eae4ac6686b?ixlib=rb-1.2.1&dl=sarah-dorweiler-QeVmJxZOv3k-unsplash.jpg&w=640&q=80&fm=jpg&crop=entropy&cs=tinysrgb'; + final editor = tester.editor + ..insertTextNode(text) + ..insertImageNode(src) + ..insertTextNode(text); + await editor.startTesting(); + + expect(editor.documentLength, 3); + final imageFinder = find.byType(Image); + expect(imageFinder, findsOneWidget); + + final imageNodeWidgetFinder = find.byType(ImageNodeWidget); + final image = + tester.firstWidget(imageNodeWidgetFinder) as ImageNodeWidget; + image.onCopy(); + }); + }); + + testWidgets('render image delete', (tester) async { + mockNetworkImagesFor(() async { + const text = 'Welcome to Appflowy 😁'; + const src = + 'https://images.unsplash.com/photo-1471897488648-5eae4ac6686b?ixlib=rb-1.2.1&dl=sarah-dorweiler-QeVmJxZOv3k-unsplash.jpg&w=640&q=80&fm=jpg&crop=entropy&cs=tinysrgb'; + final editor = tester.editor + ..insertTextNode(text) + ..insertImageNode(src) + ..insertImageNode(src) + ..insertTextNode(text); + await editor.startTesting(); + + expect(editor.documentLength, 4); + final imageFinder = find.byType(Image); + expect(imageFinder, findsNWidgets(2)); + + final imageNodeWidgetFinder = find.byType(ImageNodeWidget); + final image = + tester.firstWidget(imageNodeWidgetFinder) as ImageNodeWidget; + image.onDelete(); + + await tester.pump(const Duration(milliseconds: 100)); + expect(editor.documentLength, 3); + expect(find.byType(Image), findsNWidgets(1)); + }); + }); + }); +} From 0b553ae01355d8973edd6e0659985b44b688fb38 Mon Sep 17 00:00:00 2001 From: Pranshu Agrawal Date: Tue, 23 Aug 2022 23:20:18 +0530 Subject: [PATCH 021/106] Fix typo --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ee49d36f3..f74c1f6239 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,7 +40,7 @@ v0.0.4 - beta.1 is pre-release New features - Table-view database - - supported column types: Text, Checbox, Single-select, Multi-select, Numbers + - supported column types: Text, Checkbox, Single-select, Multi-select, Numbers - hide / delete columns - insert rows From b7297993d2093bcf0c23d5660684d7e1731919e1 Mon Sep 17 00:00:00 2001 From: Pranshu Agrawal Date: Tue, 23 Aug 2022 23:21:39 +0530 Subject: [PATCH 022/106] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f74c1f6239..06e3d600ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,7 +17,7 @@ - Row record open as a page - Auto resize the height of the row in the grid - Support more number formats -- Search column options, supporting Single select, Multi-select, and number format +- Search column options, supporting Single-select, Multi-select, and number format ![May-03-2022 10-03-00](https://user-images.githubusercontent.com/86001920/166394640-a8f1f3bc-5f20-4033-93e9-16bc308d7005.gif) From f5f3f51cca280cd330482a4585a62897657b0371 Mon Sep 17 00:00:00 2001 From: appflowy Date: Tue, 23 Aug 2022 20:36:28 +0800 Subject: [PATCH 023/106] chore: update column name when field was changed --- .../app_flowy/lib/plugins/board/board.dart | 2 +- .../group_entities/group_changeset.rs | 25 ++++++++++++++----- .../src/services/grid_view_editor.rs | 23 +++++++++++------ .../src/services/grid_view_manager.rs | 6 +++++ .../flowy-grid/src/services/group/action.rs | 12 +++------ .../src/services/group/controller.rs | 20 +++++++++------ .../controller_impls/checkbox_controller.rs | 16 +++--------- .../multi_select_controller.rs | 16 +++--------- .../single_select_controller.rs | 16 +++--------- .../select_option_controller/util.rs | 20 +++++++-------- .../src/services/group/group_service.rs | 15 ++++++++--- 11 files changed, 91 insertions(+), 80 deletions(-) diff --git a/frontend/app_flowy/lib/plugins/board/board.dart b/frontend/app_flowy/lib/plugins/board/board.dart index c55d7f2e17..213cc8bc3c 100644 --- a/frontend/app_flowy/lib/plugins/board/board.dart +++ b/frontend/app_flowy/lib/plugins/board/board.dart @@ -31,7 +31,7 @@ class BoardPluginBuilder implements PluginBuilder { class BoardPluginConfig implements PluginConfig { @override - bool get creatable => false; + bool get creatable => true; } class BoardPlugin extends Plugin { diff --git a/frontend/rust-lib/flowy-grid/src/entities/group_entities/group_changeset.rs b/frontend/rust-lib/flowy-grid/src/entities/group_entities/group_changeset.rs index a3ebee9cb7..b7fdda8b3d 100644 --- a/frontend/rust-lib/flowy-grid/src/entities/group_entities/group_changeset.rs +++ b/frontend/rust-lib/flowy-grid/src/entities/group_entities/group_changeset.rs @@ -1,25 +1,29 @@ use crate::entities::{GroupPB, InsertedRowPB, RowPB}; +use diesel::insertable::ColumnInsertValue::Default; use flowy_derive::ProtoBuf; use flowy_error::ErrorCode; use flowy_grid_data_model::parser::NotEmptyStr; use std::fmt::Formatter; #[derive(Debug, Default, ProtoBuf)] -pub struct GroupRowsChangesetPB { +pub struct GroupChangesetPB { #[pb(index = 1)] pub group_id: String, - #[pb(index = 2)] - pub inserted_rows: Vec, + #[pb(index = 2, one_of)] + pub group_name: Option, #[pb(index = 3)] - pub deleted_rows: Vec, + pub inserted_rows: Vec, #[pb(index = 4)] + pub deleted_rows: Vec, + + #[pb(index = 5)] pub updated_rows: Vec, } -impl std::fmt::Display for GroupRowsChangesetPB { +impl std::fmt::Display for GroupChangesetPB { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { for inserted_row in &self.inserted_rows { let _ = f.write_fmt(format_args!( @@ -36,10 +40,19 @@ impl std::fmt::Display for GroupRowsChangesetPB { } } -impl GroupRowsChangesetPB { +impl GroupChangesetPB { pub fn is_empty(&self) -> bool { self.inserted_rows.is_empty() && self.deleted_rows.is_empty() && self.updated_rows.is_empty() } + + pub fn name(group_id: String, name: &str) -> Self { + Self { + group_id, + group_name: Some(name.to_owned()), + ..Default::default() + } + } + pub fn insert(group_id: String, inserted_rows: Vec) -> Self { Self { group_id, diff --git a/frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs b/frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs index 4e23d9be19..015756954b 100644 --- a/frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs +++ b/frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs @@ -1,8 +1,8 @@ use crate::dart_notification::{send_dart_notification, GridNotification}; use crate::entities::{ CreateFilterParams, CreateRowParams, DeleteFilterParams, GridFilterConfiguration, GridLayout, GridLayoutPB, - GridSettingPB, GroupPB, GroupRowsChangesetPB, GroupViewChangesetPB, InsertedGroupPB, InsertedRowPB, - MoveGroupParams, RepeatedGridConfigurationFilterPB, RepeatedGridGroupConfigurationPB, RowPB, + GridSettingPB, GroupChangesetPB, GroupPB, GroupViewChangesetPB, InsertedGroupPB, InsertedRowPB, MoveGroupParams, + RepeatedGridConfigurationFilterPB, RepeatedGridGroupConfigurationPB, RowPB, }; use crate::services::grid_editor_task::GridServiceTaskScheduler; use crate::services::grid_view_manager::{GridViewFieldDelegate, GridViewRowDelegate}; @@ -99,8 +99,8 @@ impl GridViewRevisionEditor { row: row_pb.clone(), index: None, }; - let changeset = GroupRowsChangesetPB::insert(group_id.clone(), vec![inserted_row]); - self.notify_did_update_group_rows(changeset).await; + let changeset = GroupChangesetPB::insert(group_id.clone(), vec![inserted_row]); + self.notify_did_update_group(changeset).await; } } } @@ -115,7 +115,7 @@ impl GridViewRevisionEditor { .await { for changeset in changesets { - self.notify_did_update_group_rows(changeset).await; + self.notify_did_update_group(changeset).await; } } } @@ -129,7 +129,7 @@ impl GridViewRevisionEditor { .await { for changeset in changesets { - self.notify_did_update_group_rows(changeset).await; + self.notify_did_update_group(changeset).await; } } } @@ -151,7 +151,7 @@ impl GridViewRevisionEditor { .await { for changeset in changesets { - self.notify_did_update_group_rows(changeset).await; + self.notify_did_update_group(changeset).await; } } } @@ -253,7 +253,14 @@ impl GridViewRevisionEditor { .await } - async fn notify_did_update_group_rows(&self, changeset: GroupRowsChangesetPB) { + pub(crate) async fn did_update_field(&self, field_id: &str) -> FlowyResult<()> { + if let Some(field_rev) = self.field_delegate.get_field_rev(&field_id).await { + let _ = self.group_service.write().await.did_update_field(&field_rev).await?; + } + Ok(()) + } + + async fn notify_did_update_group(&self, changeset: GroupChangesetPB) { send_dart_notification(&changeset.group_id, GridNotification::DidUpdateGroup) .payload(changeset) .send(); diff --git a/frontend/rust-lib/flowy-grid/src/services/grid_view_manager.rs b/frontend/rust-lib/flowy-grid/src/services/grid_view_manager.rs index b0e578f804..b93ac2871f 100644 --- a/frontend/rust-lib/flowy-grid/src/services/grid_view_manager.rs +++ b/frontend/rust-lib/flowy-grid/src/services/grid_view_manager.rs @@ -149,6 +149,12 @@ impl GridViewManager { } } + pub(crate) async fn did_update_field(&self, field_id: &str) -> FlowyResult<()> { + let view_editor = self.get_default_view_editor().await?; + let _ = view_editor.did_update_field(field_id).await?; + Ok(()) + } + pub(crate) async fn get_view_editor(&self, view_id: &str) -> FlowyResult> { debug_assert!(!view_id.is_empty()); match self.view_editors.get(view_id) { diff --git a/frontend/rust-lib/flowy-grid/src/services/group/action.rs b/frontend/rust-lib/flowy-grid/src/services/group/action.rs index 29dc51cc37..d19be8395e 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/action.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/action.rs @@ -1,4 +1,4 @@ -use crate::entities::GroupRowsChangesetPB; +use crate::entities::GroupChangesetPB; use crate::services::group::controller::MoveGroupRowContext; use flowy_grid_data_model::revision::RowRevision; @@ -6,12 +6,8 @@ use flowy_grid_data_model::revision::RowRevision; pub trait GroupAction: Send + Sync { type CellDataType; fn can_group(&self, content: &str, cell_data: &Self::CellDataType) -> bool; - fn add_row_if_match(&mut self, row_rev: &RowRevision, cell_data: &Self::CellDataType) -> Vec; - fn remove_row_if_match( - &mut self, - row_rev: &RowRevision, - cell_data: &Self::CellDataType, - ) -> Vec; + fn add_row_if_match(&mut self, row_rev: &RowRevision, cell_data: &Self::CellDataType) -> Vec; + fn remove_row_if_match(&mut self, row_rev: &RowRevision, cell_data: &Self::CellDataType) -> Vec; - fn move_row(&mut self, cell_data: &Self::CellDataType, context: MoveGroupRowContext) -> Vec; + fn move_row(&mut self, cell_data: &Self::CellDataType, context: MoveGroupRowContext) -> Vec; } diff --git a/frontend/rust-lib/flowy-grid/src/services/group/controller.rs b/frontend/rust-lib/flowy-grid/src/services/group/controller.rs index 1a5ee23694..7cf7897dfc 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/controller.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/controller.rs @@ -1,4 +1,4 @@ -use crate::entities::{GroupRowsChangesetPB, RowPB}; +use crate::entities::{GroupChangesetPB, RowPB}; use crate::services::cell::{decode_any_cell_data, CellBytesParser}; use crate::services::group::action::GroupAction; use crate::services::group::configuration::GenericGroupConfiguration; @@ -51,15 +51,17 @@ pub trait GroupControllerSharedOperation: Send + Sync { &mut self, row_rev: &RowRevision, field_rev: &FieldRevision, - ) -> FlowyResult>; + ) -> FlowyResult>; fn did_delete_row( &mut self, row_rev: &RowRevision, field_rev: &FieldRevision, - ) -> FlowyResult>; + ) -> FlowyResult>; - fn move_group_row(&mut self, context: MoveGroupRowContext) -> FlowyResult>; + fn move_group_row(&mut self, context: MoveGroupRowContext) -> FlowyResult>; + + fn did_update_field(&mut self, field_rev: &FieldRevision) -> FlowyResult<()>; } /// C: represents the group configuration that impl [GroupConfigurationSerde] @@ -173,7 +175,7 @@ where &mut self, row_rev: &RowRevision, field_rev: &FieldRevision, - ) -> FlowyResult> { + ) -> FlowyResult> { if let Some(cell_rev) = row_rev.cells.get(&self.field_id) { let cell_bytes = decode_any_cell_data(cell_rev.data.clone(), field_rev); let cell_data = cell_bytes.parser::

()?; @@ -187,7 +189,7 @@ where &mut self, row_rev: &RowRevision, field_rev: &FieldRevision, - ) -> FlowyResult> { + ) -> FlowyResult> { if let Some(cell_rev) = row_rev.cells.get(&self.field_id) { let cell_bytes = decode_any_cell_data(cell_rev.data.clone(), field_rev); let cell_data = cell_bytes.parser::

()?; @@ -197,7 +199,7 @@ where } } - fn move_group_row(&mut self, context: MoveGroupRowContext) -> FlowyResult> { + fn move_group_row(&mut self, context: MoveGroupRowContext) -> FlowyResult> { if let Some(cell_rev) = context.row_rev.cells.get(&self.field_id) { let cell_bytes = decode_any_cell_data(cell_rev.data.clone(), context.field_rev); let cell_data = cell_bytes.parser::

()?; @@ -206,6 +208,10 @@ where Ok(vec![]) } } + + fn did_update_field(&mut self, field_rev: &FieldRevision) -> FlowyResult<()> { + todo!() + } } struct GroupRow { diff --git a/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/checkbox_controller.rs b/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/checkbox_controller.rs index ffcbf117fe..4c06ba63ce 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/checkbox_controller.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/checkbox_controller.rs @@ -1,4 +1,4 @@ -use crate::entities::GroupRowsChangesetPB; +use crate::entities::GroupChangesetPB; use crate::services::field::{CheckboxCellData, CheckboxCellDataParser, CheckboxTypeOptionPB, CHECK, UNCHECK}; use crate::services::group::action::GroupAction; use crate::services::group::configuration::GenericGroupConfiguration; @@ -24,11 +24,7 @@ impl GroupAction for CheckboxGroupController { false } - fn add_row_if_match( - &mut self, - _row_rev: &RowRevision, - _cell_data: &Self::CellDataType, - ) -> Vec { + fn add_row_if_match(&mut self, _row_rev: &RowRevision, _cell_data: &Self::CellDataType) -> Vec { todo!() } @@ -36,15 +32,11 @@ impl GroupAction for CheckboxGroupController { &mut self, _row_rev: &RowRevision, _cell_data: &Self::CellDataType, - ) -> Vec { + ) -> Vec { todo!() } - fn move_row( - &mut self, - _cell_data: &Self::CellDataType, - _context: MoveGroupRowContext, - ) -> Vec { + fn move_row(&mut self, _cell_data: &Self::CellDataType, _context: MoveGroupRowContext) -> Vec { todo!() } } diff --git a/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/multi_select_controller.rs b/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/multi_select_controller.rs index cce2698158..0c1a7117d4 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/multi_select_controller.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/multi_select_controller.rs @@ -1,4 +1,4 @@ -use crate::entities::GroupRowsChangesetPB; +use crate::entities::GroupChangesetPB; use crate::services::cell::insert_select_option_cell; use crate::services::field::{MultiSelectTypeOptionPB, SelectOptionCellDataPB, SelectOptionCellDataParser}; use crate::services::group::action::GroupAction; @@ -25,7 +25,7 @@ impl GroupAction for MultiSelectGroupController { cell_data.select_options.iter().any(|option| option.id == content) } - fn add_row_if_match(&mut self, row_rev: &RowRevision, cell_data: &Self::CellDataType) -> Vec { + fn add_row_if_match(&mut self, row_rev: &RowRevision, cell_data: &Self::CellDataType) -> Vec { let mut changesets = vec![]; self.configuration.with_mut_groups(|group| { add_row(group, &mut changesets, cell_data, row_rev); @@ -33,11 +33,7 @@ impl GroupAction for MultiSelectGroupController { changesets } - fn remove_row_if_match( - &mut self, - row_rev: &RowRevision, - cell_data: &Self::CellDataType, - ) -> Vec { + fn remove_row_if_match(&mut self, row_rev: &RowRevision, cell_data: &Self::CellDataType) -> Vec { let mut changesets = vec![]; self.configuration.with_mut_groups(|group| { remove_row(group, &mut changesets, cell_data, row_rev); @@ -45,11 +41,7 @@ impl GroupAction for MultiSelectGroupController { changesets } - fn move_row( - &mut self, - cell_data: &Self::CellDataType, - mut context: MoveGroupRowContext, - ) -> Vec { + fn move_row(&mut self, cell_data: &Self::CellDataType, mut context: MoveGroupRowContext) -> Vec { let mut group_changeset = vec![]; self.configuration.with_mut_groups(|group| { move_select_option_row(group, &mut group_changeset, cell_data, &mut context); diff --git a/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/single_select_controller.rs b/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/single_select_controller.rs index d48cdd8ee7..ad4d296fab 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/single_select_controller.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/single_select_controller.rs @@ -1,4 +1,4 @@ -use crate::entities::{GroupRowsChangesetPB, RowPB}; +use crate::entities::{GroupChangesetPB, RowPB}; use crate::services::cell::insert_select_option_cell; use crate::services::field::{SelectOptionCellDataPB, SelectOptionCellDataParser, SingleSelectTypeOptionPB}; use crate::services::group::action::GroupAction; @@ -25,7 +25,7 @@ impl GroupAction for SingleSelectGroupController { cell_data.select_options.iter().any(|option| option.id == content) } - fn add_row_if_match(&mut self, row_rev: &RowRevision, cell_data: &Self::CellDataType) -> Vec { + fn add_row_if_match(&mut self, row_rev: &RowRevision, cell_data: &Self::CellDataType) -> Vec { let mut changesets = vec![]; self.configuration.with_mut_groups(|group| { add_row(group, &mut changesets, cell_data, row_rev); @@ -33,11 +33,7 @@ impl GroupAction for SingleSelectGroupController { changesets } - fn remove_row_if_match( - &mut self, - row_rev: &RowRevision, - cell_data: &Self::CellDataType, - ) -> Vec { + fn remove_row_if_match(&mut self, row_rev: &RowRevision, cell_data: &Self::CellDataType) -> Vec { let mut changesets = vec![]; self.configuration.with_mut_groups(|group| { remove_row(group, &mut changesets, cell_data, row_rev); @@ -45,11 +41,7 @@ impl GroupAction for SingleSelectGroupController { changesets } - fn move_row( - &mut self, - cell_data: &Self::CellDataType, - mut context: MoveGroupRowContext, - ) -> Vec { + fn move_row(&mut self, cell_data: &Self::CellDataType, mut context: MoveGroupRowContext) -> Vec { let mut group_changeset = vec![]; self.configuration.with_mut_groups(|group| { move_select_option_row(group, &mut group_changeset, cell_data, &mut context); diff --git a/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/util.rs b/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/util.rs index bdca688531..9a1658c19f 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/util.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/util.rs @@ -1,4 +1,4 @@ -use crate::entities::{GroupRowsChangesetPB, InsertedRowPB, RowPB}; +use crate::entities::{GroupChangesetPB, InsertedRowPB, RowPB}; use crate::services::cell::insert_select_option_cell; use crate::services::field::SelectOptionCellDataPB; use crate::services::group::configuration::GenericGroupConfiguration; @@ -11,7 +11,7 @@ pub type SelectOptionGroupConfiguration = GenericGroupConfiguration, + changesets: &mut Vec, cell_data: &SelectOptionCellDataPB, row_rev: &RowRevision, ) { @@ -19,14 +19,14 @@ pub fn add_row( if option.id == group.id { if !group.contains_row(&row_rev.id) { let row_pb = RowPB::from(row_rev); - changesets.push(GroupRowsChangesetPB::insert( + changesets.push(GroupChangesetPB::insert( group.id.clone(), vec![InsertedRowPB::new(row_pb.clone())], )); group.add_row(row_pb); } } else if group.contains_row(&row_rev.id) { - changesets.push(GroupRowsChangesetPB::delete(group.id.clone(), vec![row_rev.id.clone()])); + changesets.push(GroupChangesetPB::delete(group.id.clone(), vec![row_rev.id.clone()])); group.remove_row(&row_rev.id); } }); @@ -34,13 +34,13 @@ pub fn add_row( pub fn remove_row( group: &mut Group, - changesets: &mut Vec, + changesets: &mut Vec, cell_data: &SelectOptionCellDataPB, row_rev: &RowRevision, ) { cell_data.select_options.iter().for_each(|option| { if option.id == group.id && group.contains_row(&row_rev.id) { - changesets.push(GroupRowsChangesetPB::delete(group.id.clone(), vec![row_rev.id.clone()])); + changesets.push(GroupChangesetPB::delete(group.id.clone(), vec![row_rev.id.clone()])); group.remove_row(&row_rev.id); } }); @@ -48,7 +48,7 @@ pub fn remove_row( pub fn move_select_option_row( group: &mut Group, - group_changeset: &mut Vec, + group_changeset: &mut Vec, _cell_data: &SelectOptionCellDataPB, context: &mut MoveGroupRowContext, ) { @@ -68,7 +68,7 @@ pub fn move_select_option_row( // Remove the row in which group contains it if from_index.is_some() { - group_changeset.push(GroupRowsChangesetPB::delete(group.id.clone(), vec![row_rev.id.clone()])); + group_changeset.push(GroupChangesetPB::delete(group.id.clone(), vec![row_rev.id.clone()])); tracing::debug!("Group:{} remove row:{}", group.id, row_rev.id); group.remove_row(&row_rev.id); } @@ -78,7 +78,7 @@ pub fn move_select_option_row( let mut inserted_row = InsertedRowPB::new(row_pb.clone()); match to_index { None => { - group_changeset.push(GroupRowsChangesetPB::insert(group.id.clone(), vec![inserted_row])); + group_changeset.push(GroupChangesetPB::insert(group.id.clone(), vec![inserted_row])); tracing::debug!("Group:{} append row:{}", group.id, row_rev.id); group.add_row(row_pb); } @@ -91,7 +91,7 @@ pub fn move_select_option_row( tracing::debug!("Group:{} append row:{}", group.id, row_rev.id); group.add_row(row_pb); } - group_changeset.push(GroupRowsChangesetPB::insert(group.id.clone(), vec![inserted_row])); + group_changeset.push(GroupChangesetPB::insert(group.id.clone(), vec![inserted_row])); } } diff --git a/frontend/rust-lib/flowy-grid/src/services/group/group_service.rs b/frontend/rust-lib/flowy-grid/src/services/group/group_service.rs index 6afec9dd82..a6ae504c05 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/group_service.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/group_service.rs @@ -1,4 +1,4 @@ -use crate::entities::{FieldType, GroupRowsChangesetPB}; +use crate::entities::{FieldType, GroupChangesetPB}; use crate::services::group::configuration::GroupConfigurationReader; use crate::services::group::controller::{GroupController, MoveGroupRowContext}; use crate::services::group::{ @@ -86,7 +86,7 @@ impl GroupService { &mut self, row_rev: &RowRevision, get_field_fn: F, - ) -> Option> + ) -> Option> where F: FnOnce(String) -> O, O: Future>> + Send + Sync + 'static, @@ -111,7 +111,7 @@ impl GroupService { to_group_id: &str, to_row_id: Option, get_field_fn: F, - ) -> Option> + ) -> Option> where F: FnOnce(String) -> O, O: Future>> + Send + Sync + 'static, @@ -141,7 +141,7 @@ impl GroupService { &mut self, row_rev: &RowRevision, get_field_fn: F, - ) -> Option> + ) -> Option> where F: FnOnce(String) -> O, O: Future>> + Send + Sync + 'static, @@ -170,6 +170,13 @@ impl GroupService { } } + pub(crate) async fn did_update_field(&mut self, field_rev: &FieldRevision) -> FlowyResult<()> { + match self.group_controller.as_mut() { + None => Ok(()), + Some(group_controller) => group_controller.did_update_field(field_rev), + } + } + #[tracing::instrument(level = "trace", skip(self, field_rev), err)] async fn make_group_controller( &self, From 3832af5fa868e07007bed37db6830a3bc0362cb5 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Wed, 24 Aug 2022 12:02:50 +0800 Subject: [PATCH 024/106] test: add test for image_node_widget and image_node_builder --- .../src/render/image/image_node_builder.dart | 12 +- .../src/render/image/image_node_widget.dart | 99 +++++++----- .../render/image/image_node_builder_test.dart | 131 +++++++++++++++ .../render/image/image_node_widget_test.dart | 153 ++++++------------ 4 files changed, 245 insertions(+), 150 deletions(-) create mode 100644 frontend/app_flowy/packages/appflowy_editor/test/render/image/image_node_builder_test.dart diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_builder.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_builder.dart index 5824849540..cab86a5dc4 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_builder.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_builder.dart @@ -41,24 +41,20 @@ class ImageNodeBuilder extends NodeWidgetBuilder { }); Alignment _textToAlignment(String text) { - if (text == 'center') { - return Alignment.center; - } else if (text == 'left') { + if (text == 'left') { return Alignment.centerLeft; } else if (text == 'right') { return Alignment.centerRight; } - throw UnimplementedError(); + return Alignment.center; } String _alignmentToText(Alignment alignment) { - if (alignment == Alignment.center) { - return 'center'; - } else if (alignment == Alignment.centerLeft) { + if (alignment == Alignment.centerLeft) { return 'left'; } else if (alignment == Alignment.centerRight) { return 'right'; } - throw UnimplementedError(); + return 'center'; } } diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_widget.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_widget.dart index 12b22787ff..f5ffdeb4ce 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_widget.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_widget.dart @@ -54,25 +54,7 @@ class _ImageNodeWidgetState extends State { widget.src, width: imageWidth == null ? null : imageWidth! - _distance, loadingBuilder: (context, child, loadingProgress) => - loadingProgress == null - ? child - : SizedBox( - width: imageWidth, - height: 300, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - SizedBox.fromSize( - size: const Size(18, 18), - child: const CircularProgressIndicator(), - ), - SizedBox.fromSize( - size: const Size(10, 10), - ), - const Text('Loading'), - ], - ), - ), + loadingProgress == null ? child : _buildLoading(context), ); if (imageWidth == null) { networkImage.image.resolve(const ImageConfiguration()).addListener( @@ -111,16 +93,39 @@ class _ImageNodeWidgetState extends State { }, ), if (_onFocus) - _buildImageToolbar( - context, + ImageToolbar( top: 8, right: 8, height: 30, - ), + alignment: widget.alignment, + onAlign: widget.onAlign, + onCopy: widget.onCopy, + onDelete: widget.onDelete, + ) ], ); } + Widget _buildLoading(BuildContext context) { + return SizedBox( + width: imageWidth, + height: 300, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox.fromSize( + size: const Size(18, 18), + child: const CircularProgressIndicator(), + ), + SizedBox.fromSize( + size: const Size(10, 10), + ), + const Text('Loading'), + ], + ), + ); + } + Widget _buildEdgeGesture( BuildContext context, { double? top, @@ -169,20 +174,34 @@ class _ImageNodeWidgetState extends State { ), ); } +} - Widget _buildImageToolbar( - BuildContext context, { - double? top, - double? left, - double? right, - double? width, - double? height, - }) { +@visibleForTesting +class ImageToolbar extends StatelessWidget { + const ImageToolbar({ + Key? key, + required this.top, + required this.right, + required this.height, + required this.alignment, + required this.onCopy, + required this.onDelete, + required this.onAlign, + }) : super(key: key); + + final double top; + final double right; + final double height; + final Alignment alignment; + final VoidCallback onCopy; + final VoidCallback onDelete; + final void Function(Alignment alignment) onAlign; + + @override + Widget build(BuildContext context) { return Positioned( top: top, - left: left, right: right, - width: width, height: height, child: Container( decoration: BoxDecoration( @@ -205,12 +224,12 @@ class _ImageNodeWidgetState extends State { padding: const EdgeInsets.fromLTRB(6.0, 4.0, 0.0, 4.0), icon: FlowySvg( name: 'image_toolbar/align_left', - color: widget.alignment == Alignment.centerLeft + color: alignment == Alignment.centerLeft ? const Color(0xFF00BCF0) : null, ), onPressed: () { - widget.onAlign(Alignment.centerLeft); + onAlign(Alignment.centerLeft); }, ), IconButton( @@ -219,12 +238,12 @@ class _ImageNodeWidgetState extends State { padding: const EdgeInsets.fromLTRB(0.0, 4.0, 0.0, 4.0), icon: FlowySvg( name: 'image_toolbar/align_center', - color: widget.alignment == Alignment.center + color: alignment == Alignment.center ? const Color(0xFF00BCF0) : null, ), onPressed: () { - widget.onAlign(Alignment.center); + onAlign(Alignment.center); }, ), IconButton( @@ -233,12 +252,12 @@ class _ImageNodeWidgetState extends State { padding: const EdgeInsets.fromLTRB(0.0, 4.0, 4.0, 4.0), icon: FlowySvg( name: 'image_toolbar/align_right', - color: widget.alignment == Alignment.centerRight + color: alignment == Alignment.centerRight ? const Color(0xFF00BCF0) : null, ), onPressed: () { - widget.onAlign(Alignment.centerRight); + onAlign(Alignment.centerRight); }, ), const Center( @@ -254,7 +273,7 @@ class _ImageNodeWidgetState extends State { name: 'image_toolbar/copy', ), onPressed: () { - widget.onCopy(); + onCopy(); }, ), IconButton( @@ -265,7 +284,7 @@ class _ImageNodeWidgetState extends State { name: 'image_toolbar/delete', ), onPressed: () { - widget.onDelete(); + onDelete(); }, ), ], diff --git a/frontend/app_flowy/packages/appflowy_editor/test/render/image/image_node_builder_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/render/image/image_node_builder_test.dart new file mode 100644 index 0000000000..9121fa1868 --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/test/render/image/image_node_builder_test.dart @@ -0,0 +1,131 @@ +import 'package:appflowy_editor/src/render/image/image_node_widget.dart'; +import 'package:appflowy_editor/src/service/editor_service.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:network_image_mock/network_image_mock.dart'; + +import '../../infra/test_editor.dart'; + +void main() async { + setUpAll(() { + TestWidgetsFlutterBinding.ensureInitialized(); + }); + + group('image_node_builder.dart', () { + testWidgets('render image node', (tester) async { + mockNetworkImagesFor(() async { + const text = 'Welcome to Appflowy 😁'; + const src = + 'https://images.unsplash.com/photo-1471897488648-5eae4ac6686b?ixlib=rb-1.2.1&dl=sarah-dorweiler-QeVmJxZOv3k-unsplash.jpg&w=640&q=80&fm=jpg&crop=entropy&cs=tinysrgb'; + final editor = tester.editor + ..insertTextNode(text) + ..insertImageNode(src) + ..insertTextNode(text); + await editor.startTesting(); + + expect(editor.documentLength, 3); + expect(find.byType(Image), findsOneWidget); + }); + }); + + testWidgets('render image align', (tester) async { + mockNetworkImagesFor(() async { + const text = 'Welcome to Appflowy 😁'; + const src = + 'https://images.unsplash.com/photo-1471897488648-5eae4ac6686b?ixlib=rb-1.2.1&dl=sarah-dorweiler-QeVmJxZOv3k-unsplash.jpg&w=640&q=80&fm=jpg&crop=entropy&cs=tinysrgb'; + final editor = tester.editor + ..insertTextNode(text) + ..insertImageNode(src, align: 'left') + ..insertImageNode(src, align: 'center') + ..insertImageNode(src, align: 'right') + ..insertTextNode(text); + await editor.startTesting(); + + expect(editor.documentLength, 5); + final imageFinder = find.byType(Image); + expect(imageFinder, findsNWidgets(3)); + + final editorFinder = find.byType(AppFlowyEditor); + final editorRect = tester.getRect(editorFinder); + + final leftImageRect = tester.getRect(imageFinder.at(0)); + expect(leftImageRect.left, editorRect.left); + final rightImageRect = tester.getRect(imageFinder.at(2)); + expect(rightImageRect.right, editorRect.right); + final centerImageRect = tester.getRect(imageFinder.at(1)); + expect(centerImageRect.left, + (leftImageRect.left + rightImageRect.left) / 2.0); + expect(leftImageRect.size, centerImageRect.size); + expect(rightImageRect.size, centerImageRect.size); + + final imageNodeWidgetFinder = find.byType(ImageNodeWidget); + + final leftImage = + tester.firstWidget(imageNodeWidgetFinder) as ImageNodeWidget; + + leftImage.onAlign(Alignment.center); + await tester.pump(const Duration(milliseconds: 100)); + expect( + tester.getRect(imageFinder.at(0)).left, + centerImageRect.left, + ); + + leftImage.onAlign(Alignment.centerRight); + await tester.pump(const Duration(milliseconds: 100)); + expect( + tester.getRect(imageFinder.at(0)).left, + rightImageRect.left, + ); + }); + }); + + testWidgets('render image copy', (tester) async { + mockNetworkImagesFor(() async { + const text = 'Welcome to Appflowy 😁'; + const src = + 'https://images.unsplash.com/photo-1471897488648-5eae4ac6686b?ixlib=rb-1.2.1&dl=sarah-dorweiler-QeVmJxZOv3k-unsplash.jpg&w=640&q=80&fm=jpg&crop=entropy&cs=tinysrgb'; + final editor = tester.editor + ..insertTextNode(text) + ..insertImageNode(src) + ..insertTextNode(text); + await editor.startTesting(); + + expect(editor.documentLength, 3); + final imageFinder = find.byType(Image); + expect(imageFinder, findsOneWidget); + + final imageNodeWidgetFinder = find.byType(ImageNodeWidget); + final image = + tester.firstWidget(imageNodeWidgetFinder) as ImageNodeWidget; + image.onCopy(); + }); + }); + + testWidgets('render image delete', (tester) async { + mockNetworkImagesFor(() async { + const text = 'Welcome to Appflowy 😁'; + const src = + 'https://images.unsplash.com/photo-1471897488648-5eae4ac6686b?ixlib=rb-1.2.1&dl=sarah-dorweiler-QeVmJxZOv3k-unsplash.jpg&w=640&q=80&fm=jpg&crop=entropy&cs=tinysrgb'; + final editor = tester.editor + ..insertTextNode(text) + ..insertImageNode(src) + ..insertImageNode(src) + ..insertTextNode(text); + await editor.startTesting(); + + expect(editor.documentLength, 4); + final imageFinder = find.byType(Image); + expect(imageFinder, findsNWidgets(2)); + + final imageNodeWidgetFinder = find.byType(ImageNodeWidget); + final image = + tester.firstWidget(imageNodeWidgetFinder) as ImageNodeWidget; + image.onDelete(); + + await tester.pump(const Duration(milliseconds: 100)); + expect(editor.documentLength, 3); + expect(find.byType(Image), findsNWidgets(1)); + }); + }); + }); +} diff --git a/frontend/app_flowy/packages/appflowy_editor/test/render/image/image_node_widget_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/render/image/image_node_widget_test.dart index 7829c456eb..4cb68a488f 100644 --- a/frontend/app_flowy/packages/appflowy_editor/test/render/image/image_node_widget_test.dart +++ b/frontend/app_flowy/packages/appflowy_editor/test/render/image/image_node_widget_test.dart @@ -1,130 +1,79 @@ import 'package:appflowy_editor/src/render/image/image_node_widget.dart'; -import 'package:appflowy_editor/src/service/editor_service.dart'; +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:network_image_mock/network_image_mock.dart'; -import '../../infra/test_editor.dart'; - void main() async { setUpAll(() { TestWidgetsFlutterBinding.ensureInitialized(); }); group('image_node_widget.dart', () { - testWidgets('render image node', (tester) async { + testWidgets('build the image node widget', (tester) async { mockNetworkImagesFor(() async { - const text = 'Welcome to Appflowy 😁'; + var onCopyHit = false; + var onDeleteHit = false; + var onAlignHit = false; const src = 'https://images.unsplash.com/photo-1471897488648-5eae4ac6686b?ixlib=rb-1.2.1&dl=sarah-dorweiler-QeVmJxZOv3k-unsplash.jpg&w=640&q=80&fm=jpg&crop=entropy&cs=tinysrgb'; - final editor = tester.editor - ..insertTextNode(text) - ..insertImageNode(src) - ..insertTextNode(text); - await editor.startTesting(); - expect(editor.documentLength, 3); - expect(find.byType(Image), findsOneWidget); - }); - }); - - testWidgets('render image align', (tester) async { - mockNetworkImagesFor(() async { - const text = 'Welcome to Appflowy 😁'; - const src = - 'https://images.unsplash.com/photo-1471897488648-5eae4ac6686b?ixlib=rb-1.2.1&dl=sarah-dorweiler-QeVmJxZOv3k-unsplash.jpg&w=640&q=80&fm=jpg&crop=entropy&cs=tinysrgb'; - final editor = tester.editor - ..insertTextNode(text) - ..insertImageNode(src, align: 'left') - ..insertImageNode(src, align: 'center') - ..insertImageNode(src, align: 'right') - ..insertTextNode(text); - await editor.startTesting(); - - expect(editor.documentLength, 5); - final imageFinder = find.byType(Image); - expect(imageFinder, findsNWidgets(3)); - - final editorFinder = find.byType(AppFlowyEditor); - final editorRect = tester.getRect(editorFinder); - - final leftImageRect = tester.getRect(imageFinder.at(0)); - expect(leftImageRect.left, editorRect.left); - final rightImageRect = tester.getRect(imageFinder.at(2)); - expect(rightImageRect.right, editorRect.right); - final centerImageRect = tester.getRect(imageFinder.at(1)); - expect(centerImageRect.left, - (leftImageRect.left + rightImageRect.left) / 2.0); - expect(leftImageRect.size, centerImageRect.size); - expect(rightImageRect.size, centerImageRect.size); - - final imageNodeWidgetFinder = find.byType(ImageNodeWidget); - - final leftImage = - tester.firstWidget(imageNodeWidgetFinder) as ImageNodeWidget; - - leftImage.onAlign(Alignment.center); - await tester.pump(const Duration(milliseconds: 100)); - expect( - tester.getRect(imageFinder.at(0)).left, - centerImageRect.left, + final widget = ImageNodeWidget( + src: src, + alignment: Alignment.center, + onCopy: () { + onCopyHit = true; + }, + onDelete: () { + onDeleteHit = true; + }, + onAlign: (alignment) { + onAlignHit = true; + }, ); - leftImage.onAlign(Alignment.centerRight); - await tester.pump(const Duration(milliseconds: 100)); - expect( - tester.getRect(imageFinder.at(0)).left, - rightImageRect.left, + await tester.pumpWidget( + MaterialApp( + home: Material( + child: widget, + ), + ), ); - }); - }); + expect(find.byType(ImageNodeWidget), findsOneWidget); - testWidgets('render image copy', (tester) async { - mockNetworkImagesFor(() async { - const text = 'Welcome to Appflowy 😁'; - const src = - 'https://images.unsplash.com/photo-1471897488648-5eae4ac6686b?ixlib=rb-1.2.1&dl=sarah-dorweiler-QeVmJxZOv3k-unsplash.jpg&w=640&q=80&fm=jpg&crop=entropy&cs=tinysrgb'; - final editor = tester.editor - ..insertTextNode(text) - ..insertImageNode(src) - ..insertTextNode(text); - await editor.startTesting(); + final gesture = + await tester.createGesture(kind: PointerDeviceKind.mouse); + await gesture.addPointer(location: Offset.zero); - expect(editor.documentLength, 3); - final imageFinder = find.byType(Image); - expect(imageFinder, findsOneWidget); + expect(find.byType(ImageToolbar), findsNothing); - final imageNodeWidgetFinder = find.byType(ImageNodeWidget); - final image = - tester.firstWidget(imageNodeWidgetFinder) as ImageNodeWidget; - image.onCopy(); - }); - }); + addTearDown(gesture.removePointer); + await tester.pump(); + await gesture.moveTo(tester.getCenter(find.byType(ImageNodeWidget))); + await tester.pump(); - testWidgets('render image delete', (tester) async { - mockNetworkImagesFor(() async { - const text = 'Welcome to Appflowy 😁'; - const src = - 'https://images.unsplash.com/photo-1471897488648-5eae4ac6686b?ixlib=rb-1.2.1&dl=sarah-dorweiler-QeVmJxZOv3k-unsplash.jpg&w=640&q=80&fm=jpg&crop=entropy&cs=tinysrgb'; - final editor = tester.editor - ..insertTextNode(text) - ..insertImageNode(src) - ..insertImageNode(src) - ..insertTextNode(text); - await editor.startTesting(); + expect(find.byType(ImageToolbar), findsOneWidget); - expect(editor.documentLength, 4); - final imageFinder = find.byType(Image); - expect(imageFinder, findsNWidgets(2)); + final iconFinder = find.byType(IconButton); + expect(iconFinder, findsNWidgets(5)); - final imageNodeWidgetFinder = find.byType(ImageNodeWidget); - final image = - tester.firstWidget(imageNodeWidgetFinder) as ImageNodeWidget; - image.onDelete(); + await tester.tap(iconFinder.at(0)); + expect(onAlignHit, true); + onAlignHit = false; - await tester.pump(const Duration(milliseconds: 100)); - expect(editor.documentLength, 3); - expect(find.byType(Image), findsNWidgets(1)); + await tester.tap(iconFinder.at(1)); + expect(onAlignHit, true); + onAlignHit = false; + + await tester.tap(iconFinder.at(2)); + expect(onAlignHit, true); + onAlignHit = false; + + await tester.tap(iconFinder.at(3)); + expect(onCopyHit, true); + + await tester.tap(iconFinder.at(4)); + expect(onDeleteHit, true); }); }); }); From 6af85fbe56b809e94df4b8f3f12c41753feb14dc Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Wed, 24 Aug 2022 15:42:01 +0800 Subject: [PATCH 025/106] feat: add image entry into selection menu --- .../assets/images/selection_menu/image.svg | 5 + .../example/assets/example.json | 2 +- .../src/render/image/image_node_builder.dart | 9 + .../src/render/image/image_node_widget.dart | 47 +++-- .../src/render/image/image_upload_widget.dart | 194 ++++++++++++++++++ .../selection_menu_item_widget.dart | 2 +- .../selection_menu_service.dart | 26 ++- .../selection_menu/selection_menu_widget.dart | 9 +- .../render/image/image_node_widget_test.dart | 1 + .../selection_menu_item_widget_test.dart | 2 +- 10 files changed, 270 insertions(+), 27 deletions(-) create mode 100644 frontend/app_flowy/packages/appflowy_editor/assets/images/selection_menu/image.svg create mode 100644 frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_upload_widget.dart diff --git a/frontend/app_flowy/packages/appflowy_editor/assets/images/selection_menu/image.svg b/frontend/app_flowy/packages/appflowy_editor/assets/images/selection_menu/image.svg new file mode 100644 index 0000000000..0e2aafe0ec --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/assets/images/selection_menu/image.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/app_flowy/packages/appflowy_editor/example/assets/example.json b/frontend/app_flowy/packages/appflowy_editor/example/assets/example.json index e9edcfa268..c6b27d9ae1 100644 --- a/frontend/app_flowy/packages/appflowy_editor/example/assets/example.json +++ b/frontend/app_flowy/packages/appflowy_editor/example/assets/example.json @@ -62,7 +62,7 @@ { "type": "image", "attributes": { - "image_src": "https://images.unsplash.com/photo-1471897488648-5eae4ac6686b?ixlib=rb-1.2.1&dl=sarah-dorweiler-QeVmJxZOv3k-unsplash.jpg&w=640&q=80&fm=jpg&crop=entropy&cs=tinysrgb", + "image_src": "https://s1.ax1x.com/2022/08/24/vgAJED.png", "align": "center" } }, diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_builder.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_builder.dart index cab86a5dc4..f62378fc1a 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_builder.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_builder.dart @@ -11,9 +11,11 @@ class ImageNodeBuilder extends NodeWidgetBuilder { Widget build(NodeWidgetContext context) { final src = context.node.attributes['image_src']; final align = context.node.attributes['align']; + final width = context.node.attributes['width']; return ImageNodeWidget( key: context.node.key, src: src, + width: width, alignment: _textToAlignment(align), onCopy: () { RichClipboard.setData(RichClipboardData(text: src)); @@ -30,6 +32,13 @@ class ImageNodeBuilder extends NodeWidgetBuilder { }) ..commit(); }, + onResize: (width) { + TransactionBuilder(context.editorState) + ..updateNode(context.node, { + 'width': width, + }) + ..commit(); + }, ); } diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_widget.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_widget.dart index f5ffdeb4ce..110cff01b3 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_widget.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_widget.dart @@ -1,33 +1,57 @@ import 'package:appflowy_editor/src/infra/flowy_svg.dart'; -import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart'; import 'package:flutter/material.dart'; class ImageNodeWidget extends StatefulWidget { const ImageNodeWidget({ Key? key, required this.src, + this.width, required this.alignment, required this.onCopy, required this.onDelete, required this.onAlign, + required this.onResize, }) : super(key: key); final String src; + final double? width; final Alignment alignment; final VoidCallback onCopy; final VoidCallback onDelete; final void Function(Alignment alignment) onAlign; + final void Function(double width) onResize; @override State createState() => _ImageNodeWidgetState(); } class _ImageNodeWidgetState extends State { - double? imageWidth = defaultMaxTextNodeWidth; + double? _imageWidth; double _initial = 0; double _distance = 0; bool _onFocus = false; + ImageStream? _imageStream; + late ImageStreamListener _imageStreamListener; + + @override + void initState() { + super.initState(); + + _imageWidth = widget.width; + _imageStreamListener = ImageStreamListener( + (image, _) { + _imageWidth = image.image.width.toDouble(); + }, + ); + } + + @override + void dispose() { + _imageStream?.removeListener(_imageStreamListener); + super.dispose(); + } + @override Widget build(BuildContext context) { // only support network image. @@ -52,18 +76,13 @@ class _ImageNodeWidgetState extends State { Widget _buildResizableImage(BuildContext context) { final networkImage = Image.network( widget.src, - width: imageWidth == null ? null : imageWidth! - _distance, + width: _imageWidth == null ? null : _imageWidth! - _distance, loadingBuilder: (context, child, loadingProgress) => loadingProgress == null ? child : _buildLoading(context), ); - if (imageWidth == null) { - networkImage.image.resolve(const ImageConfiguration()).addListener( - ImageStreamListener( - (image, _) { - imageWidth = image.image.width.toDouble(); - }, - ), - ); + if (_imageWidth == null) { + _imageStream = networkImage.image.resolve(const ImageConfiguration()) + ..addListener(_imageStreamListener); } return Stack( children: [ @@ -108,7 +127,7 @@ class _ImageNodeWidgetState extends State { Widget _buildLoading(BuildContext context) { return SizedBox( - width: imageWidth, + width: _imageWidth, height: 300, child: Row( mainAxisAlignment: MainAxisAlignment.center, @@ -151,9 +170,11 @@ class _ImageNodeWidgetState extends State { } }, onHorizontalDragEnd: (details) { - imageWidth = imageWidth! - _distance; + _imageWidth = _imageWidth! - _distance; _initial = 0; _distance = 0; + + widget.onResize(_imageWidth!); }, child: MouseRegion( cursor: SystemMouseCursors.resizeLeftRight, diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_upload_widget.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_upload_widget.dart new file mode 100644 index 0000000000..c7353532ab --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_upload_widget.dart @@ -0,0 +1,194 @@ +import 'dart:collection'; + +import 'package:appflowy_editor/src/document/node.dart'; +import 'package:appflowy_editor/src/editor_state.dart'; +import 'package:appflowy_editor/src/infra/flowy_svg.dart'; +import 'package:appflowy_editor/src/operation/transaction_builder.dart'; +import 'package:appflowy_editor/src/render/selection_menu/selection_menu_service.dart'; +import 'package:flutter/material.dart'; + +OverlayEntry? _imageUploadMenu; +void showImageUploadMenu( + EditorState editorState, + SelectionMenuService menuService, + BuildContext context, +) { + menuService.dismiss(); + + _imageUploadMenu?.remove(); + _imageUploadMenu = OverlayEntry(builder: (context) { + return Positioned( + top: menuService.topLeft.dy, + left: menuService.topLeft.dx, + child: Material( + child: ImageUploadMenu( + onSubmitted: (text) { + _dismissImageUploadMenu(); + editorState.insertImageNode(text); + }, + onUpload: (text) { + _dismissImageUploadMenu(); + editorState.insertImageNode(text); + }, + ), + ), + ); + }); + + Overlay.of(context)?.insert(_imageUploadMenu!); +} + +void _dismissImageUploadMenu() { + _imageUploadMenu?.remove(); + _imageUploadMenu = null; +} + +class ImageUploadMenu extends StatefulWidget { + const ImageUploadMenu({ + Key? key, + required this.onSubmitted, + required this.onUpload, + }) : super(key: key); + + final void Function(String text) onSubmitted; + final void Function(String text) onUpload; + + @override + State createState() => _ImageUploadMenuState(); +} + +class _ImageUploadMenuState extends State { + final _textEditingController = TextEditingController(); + final _focusNode = FocusNode(); + + @override + void initState() { + super.initState(); + _focusNode.requestFocus(); + } + + @override + void dispose() { + _focusNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Container( + width: 300, + padding: const EdgeInsets.all(24.0), + decoration: BoxDecoration( + color: Colors.white, + boxShadow: [ + BoxShadow( + blurRadius: 5, + spreadRadius: 1, + color: Colors.black.withOpacity(0.1), + ), + ], + borderRadius: BorderRadius.circular(6.0), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildHeader(context), + const SizedBox(height: 16.0), + _buildInput(), + const SizedBox(height: 18.0), + _buildUploadButton(context), + ], + ), + ); + } + + Widget _buildHeader(BuildContext context) { + return const Text( + 'URL Image', + textAlign: TextAlign.left, + style: TextStyle( + fontSize: 14.0, + color: Colors.black, + fontWeight: FontWeight.w500, + ), + ); + } + + Widget _buildInput() { + return TextField( + focusNode: _focusNode, + style: const TextStyle(fontSize: 14.0), + textAlign: TextAlign.left, + controller: _textEditingController, + onSubmitted: widget.onSubmitted, + decoration: InputDecoration( + hintText: 'URL', + hintStyle: const TextStyle(fontSize: 14.0), + contentPadding: const EdgeInsets.all(16.0), + isDense: true, + suffixIcon: IconButton( + padding: const EdgeInsets.all(4.0), + icon: const FlowySvg( + name: 'clear', + width: 24, + height: 24, + ), + onPressed: () { + _textEditingController.clear(); + }, + ), + border: const OutlineInputBorder( + borderRadius: BorderRadius.all(Radius.circular(12.0)), + borderSide: BorderSide(color: Color(0xFFBDBDBD)), + ), + ), + ); + } + + Widget _buildUploadButton(BuildContext context) { + return SizedBox( + width: 170, + height: 48, + child: TextButton( + style: ButtonStyle( + backgroundColor: MaterialStateProperty.all(const Color(0xFF00BCF0)), + shape: MaterialStateProperty.all( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12.0), + ), + ), + ), + onPressed: () { + widget.onUpload(_textEditingController.text); + }, + child: const Text( + 'Upload', + style: TextStyle(color: Colors.white, fontSize: 14.0), + ), + ), + ); + } +} + +extension on EditorState { + void insertImageNode(String src) { + final selection = service.selectionService.currentSelection.value; + if (selection == null) { + return; + } + final imageNode = Node( + type: 'image', + children: LinkedList(), + attributes: { + 'image_src': src, + 'align': 'center', + }, + ); + TransactionBuilder(this) + ..insertNode( + selection.start.path, + imageNode, + ) + ..commit(); + } +} diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_item_widget.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_item_widget.dart index 36e0a2e02e..3b7307f039 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_item_widget.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_item_widget.dart @@ -45,7 +45,7 @@ class SelectionMenuItemWidget extends StatelessWidget { ), ), onPressed: () { - item.handler(editorState, menuService); + item.handler(editorState, menuService, context); }, ), ), diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_service.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_service.dart index 94fa6190d8..7f4f803610 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_service.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_service.dart @@ -1,5 +1,6 @@ import 'package:appflowy_editor/src/editor_state.dart'; import 'package:appflowy_editor/src/infra/flowy_svg.dart'; +import 'package:appflowy_editor/src/render/image/image_upload_widget.dart'; import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart'; import 'package:appflowy_editor/src/render/selection_menu/selection_menu_widget.dart'; import 'package:appflowy_editor/src/service/default_text_operations/format_rich_text_style.dart'; @@ -23,6 +24,7 @@ class SelectionMenu implements SelectionMenuService { OverlayEntry? _selectionMenuEntry; bool _selectionUpdateByInner = false; + Offset? _topLeft; @override void dismiss() { @@ -53,6 +55,7 @@ class SelectionMenu implements SelectionMenuService { return; } final offset = selectionRects.first.bottomRight + const Offset(10, 10); + _topLeft = offset; _selectionMenuEntry = OverlayEntry(builder: (context) { return Positioned( @@ -84,8 +87,9 @@ class SelectionMenu implements SelectionMenuService { } @override - // TODO: implement topLeft - Offset get topLeft => throw UnimplementedError(); + Offset get topLeft { + return _topLeft ?? Offset.zero; + } void _onSelectionChange() { // workaround: SelectionService has been released after hot reload. @@ -115,7 +119,7 @@ final List _defaultSelectionMenuItems = [ name: 'Text', icon: _selectionMenuIcon('text'), keywords: ['text'], - handler: (editorState, menuService) { + handler: (editorState, _, __) { insertTextNodeAfterSelection(editorState, {}); }, ), @@ -123,7 +127,7 @@ final List _defaultSelectionMenuItems = [ name: 'Heading 1', icon: _selectionMenuIcon('h1'), keywords: ['heading 1, h1'], - handler: (editorState, menuService) { + handler: (editorState, _, __) { insertHeadingAfterSelection(editorState, StyleKey.h1); }, ), @@ -131,7 +135,7 @@ final List _defaultSelectionMenuItems = [ name: 'Heading 2', icon: _selectionMenuIcon('h2'), keywords: ['heading 2, h2'], - handler: (editorState, menuService) { + handler: (editorState, _, __) { insertHeadingAfterSelection(editorState, StyleKey.h2); }, ), @@ -139,15 +143,21 @@ final List _defaultSelectionMenuItems = [ name: 'Heading 3', icon: _selectionMenuIcon('h3'), keywords: ['heading 3, h3'], - handler: (editorState, menuService) { + handler: (editorState, _, __) { insertHeadingAfterSelection(editorState, StyleKey.h3); }, ), + SelectionMenuItem( + name: 'Image', + icon: _selectionMenuIcon('image'), + keywords: ['image'], + handler: showImageUploadMenu, + ), SelectionMenuItem( name: 'Bulleted list', icon: _selectionMenuIcon('bulleted_list'), keywords: ['bulleted list', 'list', 'unordered list'], - handler: (editorState, menuService) { + handler: (editorState, _, __) { insertBulletedListAfterSelection(editorState); }, ), @@ -155,7 +165,7 @@ final List _defaultSelectionMenuItems = [ name: 'Checkbox', icon: _selectionMenuIcon('checkbox'), keywords: ['todo list', 'list', 'checkbox list'], - handler: (editorState, menuService) { + handler: (editorState, _, __) { insertCheckboxAfterSelection(editorState); }, ), diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_widget.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_widget.dart index 70f7bbc337..f73251081f 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_widget.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_widget.dart @@ -22,8 +22,11 @@ class SelectionMenuItem { /// /// The keywords are used to quickly retrieve items. final List keywords; - final void Function(EditorState editorState, SelectionMenuService menuService) - handler; + final void Function( + EditorState editorState, + SelectionMenuService menuService, + BuildContext context, + ) handler; } class SelectionMenuWidget extends StatefulWidget { @@ -203,7 +206,7 @@ class _SelectionMenuWidgetState extends State { if (0 <= _selectedIndex && _selectedIndex < _showingItems.length) { _deleteLastCharacters(length: keyword.length + 1); _showingItems[_selectedIndex] - .handler(widget.editorState, widget.menuService); + .handler(widget.editorState, widget.menuService, context); return KeyEventResult.handled; } } else if (event.logicalKey == LogicalKeyboardKey.escape) { diff --git a/frontend/app_flowy/packages/appflowy_editor/test/render/image/image_node_widget_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/render/image/image_node_widget_test.dart index 4cb68a488f..d2f774d33f 100644 --- a/frontend/app_flowy/packages/appflowy_editor/test/render/image/image_node_widget_test.dart +++ b/frontend/app_flowy/packages/appflowy_editor/test/render/image/image_node_widget_test.dart @@ -30,6 +30,7 @@ void main() async { onAlign: (alignment) { onAlignHit = true; }, + onResize: (width) {}, ); await tester.pumpWidget( diff --git a/frontend/app_flowy/packages/appflowy_editor/test/render/selection_menu/selection_menu_item_widget_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/render/selection_menu/selection_menu_item_widget_test.dart index 1488b15b18..01c1403738 100644 --- a/frontend/app_flowy/packages/appflowy_editor/test/render/selection_menu/selection_menu_item_widget_test.dart +++ b/frontend/app_flowy/packages/appflowy_editor/test/render/selection_menu/selection_menu_item_widget_test.dart @@ -20,7 +20,7 @@ void main() async { name: 'example', icon: icon, keywords: ['example A', 'example B'], - handler: (editorState, menuService) { + handler: (editorState, menuService, context) { flag = true; }, ); From d3194de9e618af73fc6d549f8b81908f818786ef Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Wed, 24 Aug 2022 15:53:30 +0800 Subject: [PATCH 026/106] fix: could not delete text when insert image --- .../internal_key_event_handlers/delete_text_handler.dart | 3 --- .../render/selection_menu/selection_menu_widget_test.dart | 4 +++- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/delete_text_handler.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/delete_text_handler.dart index b931ee3d61..24049dbe1b 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/delete_text_handler.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/delete_text_handler.dart @@ -13,9 +13,6 @@ KeyEventResult _handleBackspace(EditorState editorState, RawKeyEvent event) { selection = selection.isBackward ? selection : selection.reversed; // make sure all nodes is [TextNode]. final textNodes = nodes.whereType().toList(); - if (textNodes.length != nodes.length) { - return KeyEventResult.ignored; - } final transactionBuilder = TransactionBuilder(editorState); if (textNodes.length == 1) { diff --git a/frontend/app_flowy/packages/appflowy_editor/test/render/selection_menu/selection_menu_widget_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/render/selection_menu/selection_menu_widget_test.dart index 1efcfa640d..2711921352 100644 --- a/frontend/app_flowy/packages/appflowy_editor/test/render/selection_menu/selection_menu_widget_test.dart +++ b/frontend/app_flowy/packages/appflowy_editor/test/render/selection_menu/selection_menu_widget_test.dart @@ -25,7 +25,9 @@ void main() async { find.byType(SelectionMenuWidget, skipOffstage: false), findsNothing, ); - await _testDefaultSelectionMenuItems(i, editor); + if (defaultSelectionMenuItems[i].name != 'Image') { + await _testDefaultSelectionMenuItems(i, editor); + } }); } }); From 82b44c2c982f33ad7ad3cca73ca4daf8e2dabda3 Mon Sep 17 00:00:00 2001 From: appflowy Date: Wed, 24 Aug 2022 16:57:53 +0800 Subject: [PATCH 027/106] chore: update board column name --- .../plugins/board/application/board_bloc.dart | 46 ++++--- .../application/board_data_controller.dart | 38 +++++- .../board/application/board_listener.dart | 50 ++++++++ .../board/application/group_controller.dart | 12 +- .../board/application/group_listener.dart | 4 +- .../board/presentation/board_page.dart | 8 +- .../example/lib/multi_board_list_example.dart | 32 +++-- .../lib/single_board_list_example.dart | 16 ++- .../appflowy_board/lib/src/widgets/board.dart | 20 ++- .../widgets/board_column/board_column.dart | 8 +- .../board_column/board_column_data.dart | 38 +++++- .../lib/src/widgets/board_data.dart | 32 ++--- frontend/app_flowy/pubspec.lock | 2 +- .../flowy-grid/src/entities/block_entities.rs | 2 +- .../src/entities/group_entities/group.rs | 12 ++ .../group_entities/group_changeset.rs | 22 +++- .../flowy-grid/src/services/grid_editor.rs | 102 ++++++++------- .../src/services/grid_view_editor.rs | 19 +-- .../src/services/grid_view_manager.rs | 6 +- .../src/services/group/configuration.rs | 117 ++++++++++++++---- .../src/services/group/controller.rs | 20 ++- .../multi_select_controller.rs | 18 ++- .../single_select_controller.rs | 18 ++- .../select_option_controller/util.rs | 45 ++++--- .../flowy-grid/src/services/group/entities.rs | 23 +--- .../src/services/group/group_service.rs | 21 +++- .../tests/grid/group_test/script.rs | 13 +- .../flowy-grid/tests/grid/group_test/test.rs | 23 ++++ .../src/revision/grid_block.rs | 4 +- .../src/revision/group_rev.rs | 6 +- 30 files changed, 562 insertions(+), 215 deletions(-) create mode 100644 frontend/app_flowy/lib/plugins/board/application/board_listener.dart diff --git a/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart b/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart index f84c2a2bd1..7c04dec814 100644 --- a/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart +++ b/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart @@ -20,19 +20,19 @@ import 'group_controller.dart'; part 'board_bloc.freezed.dart'; class BoardBloc extends Bloc { - final BoardDataController _dataController; - late final AFBoardDataController afBoardDataController; + final BoardDataController _gridDataController; + late final AFBoardDataController boardController; final MoveRowFFIService _rowService; LinkedHashMap groupControllers = LinkedHashMap.new(); - GridFieldCache get fieldCache => _dataController.fieldCache; - String get gridId => _dataController.gridId; + GridFieldCache get fieldCache => _gridDataController.fieldCache; + String get gridId => _gridDataController.gridId; BoardBloc({required ViewPB view}) : _rowService = MoveRowFFIService(gridId: view.id), - _dataController = BoardDataController(view: view), + _gridDataController = BoardDataController(view: view), super(BoardState.initial(view.id)) { - afBoardDataController = AFBoardDataController( + boardController = AFBoardDataController( onMoveColumn: ( fromColumnId, fromIndex, @@ -70,7 +70,7 @@ class BoardBloc extends Bloc { await _loadGrid(emit); }, createRow: (groupId) async { - final result = await _dataController.createBoardCard(groupId); + final result = await _gridDataController.createBoardCard(groupId); result.fold( (rowPB) { emit(state.copyWith(editingRow: some(rowPB))); @@ -126,7 +126,7 @@ class BoardBloc extends Bloc { @override Future close() async { - await _dataController.dispose(); + await _gridDataController.dispose(); for (final controller in groupControllers.values) { controller.dispose(); } @@ -135,7 +135,7 @@ class BoardBloc extends Bloc { void initializeGroups(List groups) { for (final group in groups) { - final delegate = GroupControllerDelegateImpl(afBoardDataController); + final delegate = GroupControllerDelegateImpl(boardController); final controller = GroupController( gridId: state.gridId, group: group, @@ -147,12 +147,12 @@ class BoardBloc extends Bloc { } GridRowCache? getRowCache(String blockId) { - final GridBlockCache? blockCache = _dataController.blocks[blockId]; + final GridBlockCache? blockCache = _gridDataController.blocks[blockId]; return blockCache?.rowCache; } void _startListening() { - _dataController.addListener( + _gridDataController.addListener( onGridChanged: (grid) { if (!isClosed) { add(BoardEvent.didReceiveGridUpdate(grid)); @@ -162,18 +162,34 @@ class BoardBloc extends Bloc { List columns = groups.map((group) { return AFBoardColumnData( id: group.groupId, - desc: group.desc, + name: group.desc, items: _buildRows(group.rows), customData: group, ); }).toList(); - afBoardDataController.addColumns(columns); + boardController.addColumns(columns); initializeGroups(groups); }, onRowsChanged: (List rowInfos, RowsChangedReason reason) { add(BoardEvent.didReceiveRows(rowInfos)); }, + onDeletedGroup: (groupIds) { + // + }, + onInsertedGroup: (insertedGroups) { + // + }, + onUpdatedGroup: (updatedGroups) { + // + for (final group in updatedGroups) { + final columnController = + boardController.getColumnController(group.groupId); + if (columnController != null) { + columnController.updateColumnName(group.desc); + } + } + }, onError: (err) { Log.error(err); }, @@ -189,7 +205,7 @@ class BoardBloc extends Bloc { } Future _loadGrid(Emitter emit) async { - final result = await _dataController.loadData(); + final result = await _gridDataController.loadData(); result.fold( (grid) => emit( state.copyWith(loadingState: GridLoadingState.finish(left(unit))), @@ -301,6 +317,6 @@ class GroupControllerDelegateImpl extends GroupControllerDelegate { @override void updateRow(String groupId, RowPB row) { - // + controller.updateColumnItem(groupId, BoardColumnItem(row: row)); } } diff --git a/frontend/app_flowy/lib/plugins/board/application/board_data_controller.dart b/frontend/app_flowy/lib/plugins/board/application/board_data_controller.dart index 1d17431713..31b2594497 100644 --- a/frontend/app_flowy/lib/plugins/board/application/board_data_controller.dart +++ b/frontend/app_flowy/lib/plugins/board/application/board_data_controller.dart @@ -10,9 +10,15 @@ import 'dart:async'; import 'package:dartz/dartz.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/protobuf.dart'; +import 'board_listener.dart'; + typedef OnFieldsChanged = void Function(UnmodifiableListView); typedef OnGridChanged = void Function(GridPB); typedef DidLoadGroups = void Function(List); +typedef OnUpdatedGroup = void Function(List); +typedef OnDeletedGroup = void Function(List); +typedef OnInsertedGroup = void Function(List); + typedef OnRowsChanged = void Function( List, RowsChangedReason, @@ -23,6 +29,7 @@ class BoardDataController { final String gridId; final GridFFIService _gridFFIService; final GridFieldCache fieldCache; + final BoardListener _listener; // key: the block id final LinkedHashMap _blocks; @@ -44,16 +51,20 @@ class BoardDataController { BoardDataController({required ViewPB view}) : gridId = view.id, + _listener = BoardListener(view.id), _blocks = LinkedHashMap.new(), _gridFFIService = GridFFIService(gridId: view.id), fieldCache = GridFieldCache(gridId: view.id); void addListener({ - OnGridChanged? onGridChanged, + required OnGridChanged onGridChanged, OnFieldsChanged? onFieldsChanged, - DidLoadGroups? didLoadGroups, - OnRowsChanged? onRowsChanged, - OnError? onError, + required DidLoadGroups didLoadGroups, + required OnRowsChanged onRowsChanged, + required OnUpdatedGroup onUpdatedGroup, + required OnDeletedGroup onDeletedGroup, + required OnInsertedGroup onInsertedGroup, + required OnError? onError, }) { _onGridChanged = onGridChanged; _onFieldsChanged = onFieldsChanged; @@ -64,6 +75,25 @@ class BoardDataController { fieldCache.addListener(onFields: (fields) { _onFieldsChanged?.call(UnmodifiableListView(fields)); }); + + _listener.start(onBoardChanged: (result) { + result.fold( + (changeset) { + if (changeset.updateGroups.isNotEmpty) { + onUpdatedGroup.call(changeset.updateGroups); + } + + if (changeset.insertedGroups.isNotEmpty) { + onInsertedGroup.call(changeset.insertedGroups); + } + + if (changeset.deletedGroups.isNotEmpty) { + onDeletedGroup.call(changeset.deletedGroups); + } + }, + (e) => _onError?.call(e), + ); + }); } Future> loadData() async { diff --git a/frontend/app_flowy/lib/plugins/board/application/board_listener.dart b/frontend/app_flowy/lib/plugins/board/application/board_listener.dart new file mode 100644 index 0000000000..a953a993cc --- /dev/null +++ b/frontend/app_flowy/lib/plugins/board/application/board_listener.dart @@ -0,0 +1,50 @@ +import 'dart:typed_data'; + +import 'package:app_flowy/core/grid_notification.dart'; +import 'package:flowy_infra/notifier.dart'; +import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid/dart_notification.pb.dart'; +import 'package:dartz/dartz.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid/group_changeset.pb.dart'; + +typedef UpdateBoardNotifiedValue = Either; + +class BoardListener { + final String viewId; + PublishNotifier? _groupNotifier = PublishNotifier(); + GridNotificationListener? _listener; + BoardListener(this.viewId); + + void start({ + required void Function(UpdateBoardNotifiedValue) onBoardChanged, + }) { + _groupNotifier?.addPublishListener(onBoardChanged); + _listener = GridNotificationListener( + objectId: viewId, + handler: _handler, + ); + } + + void _handler( + GridNotification ty, + Either result, + ) { + switch (ty) { + case GridNotification.DidUpdateGroupView: + result.fold( + (payload) => _groupNotifier?.value = + left(GroupViewChangesetPB.fromBuffer(payload)), + (error) => _groupNotifier?.value = right(error), + ); + break; + default: + break; + } + } + + Future stop() async { + await _listener?.stop(); + _groupNotifier?.dispose(); + _groupNotifier = null; + } +} diff --git a/frontend/app_flowy/lib/plugins/board/application/group_controller.dart b/frontend/app_flowy/lib/plugins/board/application/group_controller.dart index 6fd68b1df8..b0a89baaa3 100644 --- a/frontend/app_flowy/lib/plugins/board/application/group_controller.dart +++ b/frontend/app_flowy/lib/plugins/board/application/group_controller.dart @@ -34,7 +34,12 @@ class GroupController { void startListening() { _listener.start(onGroupChanged: (result) { result.fold( - (GroupRowsChangesetPB changeset) { + (GroupChangesetPB changeset) { + for (final deletedRow in changeset.deletedRows) { + group.rows.removeWhere((rowPB) => rowPB.id == deletedRow); + delegate.removeRow(group.groupId, deletedRow); + } + for (final insertedRow in changeset.insertedRows) { final index = insertedRow.hasIndex() ? insertedRow.index : null; @@ -52,11 +57,6 @@ class GroupController { ); } - for (final deletedRow in changeset.deletedRows) { - group.rows.removeWhere((rowPB) => rowPB.id == deletedRow); - delegate.removeRow(group.groupId, deletedRow); - } - for (final updatedRow in changeset.updatedRows) { final index = group.rows.indexWhere( (rowPB) => rowPB.id == updatedRow.id, diff --git a/frontend/app_flowy/lib/plugins/board/application/group_listener.dart b/frontend/app_flowy/lib/plugins/board/application/group_listener.dart index 797177deca..e3b626af07 100644 --- a/frontend/app_flowy/lib/plugins/board/application/group_listener.dart +++ b/frontend/app_flowy/lib/plugins/board/application/group_listener.dart @@ -8,7 +8,7 @@ import 'package:flowy_sdk/protobuf/flowy-grid/group.pb.dart'; import 'package:dartz/dartz.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/group_changeset.pb.dart'; -typedef UpdateGroupNotifiedValue = Either; +typedef UpdateGroupNotifiedValue = Either; class GroupListener { final GroupPB group; @@ -34,7 +34,7 @@ class GroupListener { case GridNotification.DidUpdateGroup: result.fold( (payload) => _groupNotifier?.value = - left(GroupRowsChangesetPB.fromBuffer(payload)), + left(GroupChangesetPB.fromBuffer(payload)), (error) => _groupNotifier?.value = right(error), ); break; diff --git a/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart b/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart index e7202e0a6d..eb47e8c134 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart @@ -62,9 +62,8 @@ class BoardContent extends StatelessWidget { child: Padding( padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 20), child: AFBoard( - // key: UniqueKey(), scrollController: ScrollController(), - dataController: context.read().afBoardDataController, + dataController: context.read().boardController, headerBuilder: _buildHeader, footBuilder: _buildFooter, cardBuilder: (_, data) => _buildCard(context, data), @@ -79,10 +78,11 @@ class BoardContent extends StatelessWidget { ); } - Widget _buildHeader(BuildContext context, AFBoardColumnData columnData) { + Widget _buildHeader( + BuildContext context, AFBoardColumnHeaderData headerData) { return AppFlowyColumnHeader( icon: const Icon(Icons.lightbulb_circle), - title: Text(columnData.desc), + title: Text(headerData.columnName), addIcon: const Icon(Icons.add, size: 20), moreIcon: const Icon(Icons.more_horiz, size: 20), height: 50, diff --git a/frontend/app_flowy/packages/appflowy_board/example/lib/multi_board_list_example.dart b/frontend/app_flowy/packages/appflowy_board/example/lib/multi_board_list_example.dart index 218331d198..5df7ce54ff 100644 --- a/frontend/app_flowy/packages/appflowy_board/example/lib/multi_board_list_example.dart +++ b/frontend/app_flowy/packages/appflowy_board/example/lib/multi_board_list_example.dart @@ -34,13 +34,18 @@ class _MultiBoardListExampleState extends State { RichTextItem(title: "Card 8", subtitle: 'Aug 1, 2020 4:05 PM'), TextItem("Card 9"), ]; - final column1 = AFBoardColumnData(id: "To Do", items: a); - final column2 = AFBoardColumnData(id: "In Progress", items: [ - RichTextItem(title: "Card 10", subtitle: 'Aug 1, 2020 4:05 PM'), - TextItem("Card 11"), - ]); + final column1 = AFBoardColumnData(id: "To Do", name: "To Do", items: a); + final column2 = AFBoardColumnData( + id: "In Progress", + name: "In Progress", + items: [ + RichTextItem(title: "Card 10", subtitle: 'Aug 1, 2020 4:05 PM'), + TextItem("Card 11"), + ], + ); - final column3 = AFBoardColumnData(id: "Done", items: []); + final column3 = + AFBoardColumnData(id: "Done", name: "Done", items: []); boardDataController.addColumn(column1); boardDataController.addColumn(column2); @@ -68,10 +73,21 @@ class _MultiBoardListExampleState extends State { margin: config.columnItemPadding, ); }, - headerBuilder: (context, columnData) { + headerBuilder: (context, headerData) { return AppFlowyColumnHeader( icon: const Icon(Icons.lightbulb_circle), - title: Text(columnData.id), + title: SizedBox( + width: 60, + child: TextField( + controller: TextEditingController() + ..text = headerData.columnName, + onSubmitted: (val) { + boardDataController + .getColumnController(headerData.columnId)! + .updateColumnName(val); + }, + ), + ), addIcon: const Icon(Icons.add, size: 20), moreIcon: const Icon(Icons.more_horiz, size: 20), height: 50, diff --git a/frontend/app_flowy/packages/appflowy_board/example/lib/single_board_list_example.dart b/frontend/app_flowy/packages/appflowy_board/example/lib/single_board_list_example.dart index 97e83df448..4dda616621 100644 --- a/frontend/app_flowy/packages/appflowy_board/example/lib/single_board_list_example.dart +++ b/frontend/app_flowy/packages/appflowy_board/example/lib/single_board_list_example.dart @@ -13,12 +13,16 @@ class _SingleBoardListExampleState extends State { @override void initState() { - final column = AFBoardColumnData(id: "1", items: [ - TextItem("a"), - TextItem("b"), - TextItem("c"), - TextItem("d"), - ]); + final column = AFBoardColumnData( + id: "1", + name: "1", + items: [ + TextItem("a"), + TextItem("b"), + TextItem("c"), + TextItem("d"), + ], + ); boardData.addColumn(column); super.initState(); diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board.dart index e07ee39d61..dbcf62671a 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board.dart @@ -205,13 +205,13 @@ class _BoardContentState extends State { return ChangeNotifierProvider.value( key: ValueKey(columnData.id), - value: widget.dataController.columnController(columnData.id), + value: widget.dataController.getColumnController(columnData.id), child: Consumer( builder: (context, value, child) { final boardColumn = AFBoardColumnWidget( margin: _marginFromIndex(columnIndex), itemMargin: widget.config.columnItemPadding, - headerBuilder: widget.headerBuilder, + headerBuilder: _buildHeader, footBuilder: widget.footBuilder, cardBuilder: widget.cardBuilder, dataSource: dataSource, @@ -224,7 +224,6 @@ class _BoardContentState extends State { // columnKeys // .removeWhere((element) => element.columnId == columnData.id); - // columnKeys.add( // ColumnKey( // columnId: columnData.id, @@ -245,6 +244,19 @@ class _BoardContentState extends State { return children; } + Widget? _buildHeader( + BuildContext context, AFBoardColumnHeaderData headerData) { + if (widget.headerBuilder == null) { + return null; + } + return Selector( + selector: (context, controller) => controller.columnData.headerData, + builder: (context, headerData, _) { + return widget.headerBuilder!(context, headerData)!; + }, + ); + } + EdgeInsets _marginFromIndex(int index) { if (widget.dataController.columnDatas.isEmpty) { return widget.config.columnPadding; @@ -273,7 +285,7 @@ class _BoardColumnDataSourceImpl extends AFBoardColumnDataDataSource { @override AFBoardColumnData get columnData => - dataController.columnController(columnId).columnData; + dataController.getColumnController(columnId)!.columnData; @override List get acceptedColumnIds => dataController.columnIds; diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column.dart index d4e5ff8800..a5e20055f6 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column.dart @@ -27,9 +27,9 @@ typedef AFBoardColumnCardBuilder = Widget Function( AFColumnItem item, ); -typedef AFBoardColumnHeaderBuilder = Widget Function( +typedef AFBoardColumnHeaderBuilder = Widget? Function( BuildContext context, - AFBoardColumnData columnData, + AFBoardColumnHeaderData headerData, ); typedef AFBoardColumnFooterBuilder = Widget Function( @@ -125,8 +125,8 @@ class _AFBoardColumnWidgetState extends State { .map((item) => _buildWidget(context, item)) .toList(); - final header = - widget.headerBuilder?.call(context, widget.dataSource.columnData); + final header = widget.headerBuilder + ?.call(context, widget.dataSource.columnData.headerData); final footer = widget.footBuilder?.call(context, widget.dataSource.columnData); diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column_data.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column_data.dart index f26bd16c50..8cf03d96c1 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column_data.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column_data.dart @@ -34,6 +34,13 @@ class AFBoardColumnDataController extends ChangeNotifier with EquatableMixin { UnmodifiableListView get items => UnmodifiableListView(columnData.items); + void updateColumnName(String newName) { + if (columnData.headerData.columnName != newName) { + columnData.headerData.columnName = newName; + notifyListeners(); + } + } + /// Remove the item at [index]. /// * [index] the index of the item you want to remove /// * [notify] the default value of [notify] is true, it will notify the @@ -123,6 +130,20 @@ class AFBoardColumnDataController extends ChangeNotifier with EquatableMixin { notifyListeners(); } + void replaceOrInsertItem(AFColumnItem newItem) { + final index = columnData._items.indexWhere((item) => item.id == newItem.id); + if (index != -1) { + removeAt(index); + + columnData._items.removeAt(index); + columnData._items.insert(index, newItem); + notifyListeners(); + } else { + columnData._items.add(newItem); + notifyListeners(); + } + } + bool _containsItem(AFColumnItem item) { return columnData._items.indexWhere((element) => element.id == item.id) != -1; @@ -133,16 +154,20 @@ class AFBoardColumnDataController extends ChangeNotifier with EquatableMixin { class AFBoardColumnData extends ReoderFlexItem with EquatableMixin { @override final String id; - final String desc; + AFBoardColumnHeaderData headerData; final List _items; final CustomData? customData; AFBoardColumnData({ this.customData, required this.id, - this.desc = "", + required String name, List items = const [], - }) : _items = items; + }) : _items = items, + headerData = AFBoardColumnHeaderData( + columnId: id, + columnName: name, + ); /// Returns the readonly List UnmodifiableListView get items => @@ -156,3 +181,10 @@ class AFBoardColumnData extends ReoderFlexItem with EquatableMixin { return 'Column:[$id]'; } } + +class AFBoardColumnHeaderData { + String columnId; + String columnName; + + AFBoardColumnHeaderData({required this.columnId, required this.columnName}); +} diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_data.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_data.dart index a08bba378f..2cc853d6a5 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_data.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_data.dart @@ -89,10 +89,6 @@ class AFBoardDataController extends ChangeNotifier if (columnIds.isNotEmpty && notify) notifyListeners(); } - AFBoardColumnDataController columnController(String columnId) { - return _columnControllers[columnId]!; - } - AFBoardColumnDataController? getColumnController(String columnId) { final columnController = _columnControllers[columnId]; if (columnController == null) { @@ -129,6 +125,10 @@ class AFBoardDataController extends ChangeNotifier getColumnController(columnId)?.removeWhere((item) => item.id == itemId); } + void updateColumnItem(String columnId, AFColumnItem item) { + getColumnController(columnId)?.replaceOrInsertItem(item); + } + @override @protected void swapColumnItem( @@ -137,15 +137,14 @@ class AFBoardDataController extends ChangeNotifier String toColumnId, int toColumnIndex, ) { - final item = columnController(fromColumnId).removeAt(fromColumnIndex); - - if (columnController(toColumnId).items.length > toColumnIndex) { - assert(columnController(toColumnId).items[toColumnIndex] - is PhantomColumnItem); + final fromColumnController = getColumnController(fromColumnId)!; + final toColumnController = getColumnController(toColumnId)!; + final item = fromColumnController.removeAt(fromColumnIndex); + if (toColumnController.items.length > toColumnIndex) { + assert(toColumnController.items[toColumnIndex] is PhantomColumnItem); } - columnController(toColumnId).replace(toColumnIndex, item); - + toColumnController.replace(toColumnIndex, item); onMoveColumnItemToColumn?.call( fromColumnId, fromColumnIndex, @@ -174,9 +173,12 @@ class AFBoardDataController extends ChangeNotifier @override @protected bool removePhantom(String columnId) { - final columnController = this.columnController(columnId); + final columnController = getColumnController(columnId); + if (columnController == null) { + Log.warn('Can not find the column controller with columnId: $columnId'); + return false; + } final index = columnController.items.indexWhere((item) => item.isPhantom); - final isExist = index != -1; if (isExist) { columnController.removeAt(index); @@ -190,7 +192,7 @@ class AFBoardDataController extends ChangeNotifier @override @protected void updatePhantom(String columnId, int newIndex) { - final columnDataController = columnController(columnId); + final columnDataController = getColumnController(columnId)!; final index = columnDataController.items.indexWhere((item) => item.isPhantom); @@ -208,6 +210,6 @@ class AFBoardDataController extends ChangeNotifier @override @protected void insertPhantom(String columnId, int index, PhantomColumnItem item) { - columnController(columnId).insert(index, item); + getColumnController(columnId)!.insert(index, item); } } diff --git a/frontend/app_flowy/pubspec.lock b/frontend/app_flowy/pubspec.lock index 0f9421a563..b08897239c 100644 --- a/frontend/app_flowy/pubspec.lock +++ b/frontend/app_flowy/pubspec.lock @@ -28,7 +28,7 @@ packages: path: "packages/appflowy_board" relative: true source: path - version: "0.0.4" + version: "0.0.5" appflowy_editor: dependency: "direct main" description: diff --git a/frontend/rust-lib/flowy-grid/src/entities/block_entities.rs b/frontend/rust-lib/flowy-grid/src/entities/block_entities.rs index bb7eec9032..e691ed1830 100644 --- a/frontend/rust-lib/flowy-grid/src/entities/block_entities.rs +++ b/frontend/rust-lib/flowy-grid/src/entities/block_entities.rs @@ -30,7 +30,7 @@ impl BlockPB { } /// [RowPB] Describes a row. Has the id of the parent Block. Has the metadata of the row. -#[derive(Debug, Default, Clone, ProtoBuf)] +#[derive(Debug, Default, Clone, ProtoBuf, Eq, PartialEq)] pub struct RowPB { #[pb(index = 1)] pub block_id: String, diff --git a/frontend/rust-lib/flowy-grid/src/entities/group_entities/group.rs b/frontend/rust-lib/flowy-grid/src/entities/group_entities/group.rs index b769b18154..9cc138bc0f 100644 --- a/frontend/rust-lib/flowy-grid/src/entities/group_entities/group.rs +++ b/frontend/rust-lib/flowy-grid/src/entities/group_entities/group.rs @@ -1,4 +1,5 @@ use crate::entities::{CreateRowParams, FieldType, GridLayout, RowPB}; +use crate::services::group::Group; use flowy_derive::ProtoBuf; use flowy_error::ErrorCode; use flowy_grid_data_model::parser::NotEmptyStr; @@ -82,6 +83,17 @@ pub struct GroupPB { pub rows: Vec, } +impl std::convert::From for GroupPB { + fn from(group: Group) -> Self { + Self { + field_id: group.field_id, + group_id: group.id, + desc: group.name, + rows: group.rows, + } + } +} + #[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] pub struct RepeatedGridGroupConfigurationPB { #[pb(index = 1)] diff --git a/frontend/rust-lib/flowy-grid/src/entities/group_entities/group_changeset.rs b/frontend/rust-lib/flowy-grid/src/entities/group_entities/group_changeset.rs index b7fdda8b3d..21f39775f6 100644 --- a/frontend/rust-lib/flowy-grid/src/entities/group_entities/group_changeset.rs +++ b/frontend/rust-lib/flowy-grid/src/entities/group_entities/group_changeset.rs @@ -1,5 +1,4 @@ use crate::entities::{GroupPB, InsertedRowPB, RowPB}; -use diesel::insertable::ColumnInsertValue::Default; use flowy_derive::ProtoBuf; use flowy_error::ErrorCode; use flowy_grid_data_model::parser::NotEmptyStr; @@ -42,7 +41,17 @@ impl std::fmt::Display for GroupChangesetPB { impl GroupChangesetPB { pub fn is_empty(&self) -> bool { - self.inserted_rows.is_empty() && self.deleted_rows.is_empty() && self.updated_rows.is_empty() + self.group_name.is_none() + && self.inserted_rows.is_empty() + && self.deleted_rows.is_empty() + && self.updated_rows.is_empty() + } + + pub fn new(group_id: String) -> Self { + Self { + group_id, + ..Default::default() + } } pub fn name(group_id: String, name: &str) -> Self { @@ -126,9 +135,16 @@ pub struct GroupViewChangesetPB { #[pb(index = 3)] pub deleted_groups: Vec, + + #[pb(index = 4)] + pub update_groups: Vec, } -impl GroupViewChangesetPB {} +impl GroupViewChangesetPB { + pub fn is_empty(&self) -> bool { + self.inserted_groups.is_empty() && self.deleted_groups.is_empty() && self.update_groups.is_empty() + } +} #[derive(Debug, Default, ProtoBuf)] pub struct InsertedGroupPB { diff --git a/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs b/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs index 485bf6930c..8974cf0539 100644 --- a/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs +++ b/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs @@ -188,8 +188,13 @@ impl GridRevisionEditor { pub async fn replace_field(&self, field_rev: Arc) -> FlowyResult<()> { let field_id = field_rev.id.clone(); let _ = self - .modify(|grid_pad| Ok(grid_pad.replace_field_rev(field_rev)?)) + .modify(|grid_pad| Ok(grid_pad.replace_field_rev(field_rev.clone())?)) .await?; + + match self.view_manager.did_update_field(&field_rev.id).await { + Ok(_) => {} + Err(e) => tracing::error!("View manager update field failed: {:?}", e), + } let _ = self.notify_did_update_grid_field(&field_id).await?; Ok(()) } @@ -263,59 +268,65 @@ impl GridRevisionEditor { } async fn update_field_rev(&self, params: FieldChangesetParams, field_type: FieldType) -> FlowyResult<()> { - self.modify(|grid| { - let deserializer = TypeOptionJsonDeserializer(field_type); + let _ = self + .modify(|grid| { + let deserializer = TypeOptionJsonDeserializer(field_type); + let changeset = grid.modify_field(¶ms.field_id, |field| { + let mut is_changed = None; + if let Some(name) = params.name { + field.name = name; + is_changed = Some(()) + } - let changeset = grid.modify_field(¶ms.field_id, |field| { - let mut is_changed = None; - if let Some(name) = params.name { - field.name = name; - is_changed = Some(()) - } + if let Some(desc) = params.desc { + field.desc = desc; + is_changed = Some(()) + } - if let Some(desc) = params.desc { - field.desc = desc; - is_changed = Some(()) - } + if let Some(field_type) = params.field_type { + field.ty = field_type; + is_changed = Some(()) + } - if let Some(field_type) = params.field_type { - field.ty = field_type; - is_changed = Some(()) - } + if let Some(frozen) = params.frozen { + field.frozen = frozen; + is_changed = Some(()) + } - if let Some(frozen) = params.frozen { - field.frozen = frozen; - is_changed = Some(()) - } + if let Some(visibility) = params.visibility { + field.visibility = visibility; + is_changed = Some(()) + } - if let Some(visibility) = params.visibility { - field.visibility = visibility; - is_changed = Some(()) - } + if let Some(width) = params.width { + field.width = width; + is_changed = Some(()) + } - if let Some(width) = params.width { - field.width = width; - is_changed = Some(()) - } - - if let Some(type_option_data) = params.type_option_data { - match deserializer.deserialize(type_option_data) { - Ok(json_str) => { - let field_type = field.ty; - field.insert_type_option_str(&field_type, json_str); - is_changed = Some(()) - } - Err(err) => { - tracing::error!("Deserialize data to type option json failed: {}", err); + if let Some(type_option_data) = params.type_option_data { + match deserializer.deserialize(type_option_data) { + Ok(json_str) => { + let field_type = field.ty; + field.insert_type_option_str(&field_type, json_str); + is_changed = Some(()) + } + Err(err) => { + tracing::error!("Deserialize data to type option json failed: {}", err); + } } } - } - Ok(is_changed) - })?; - Ok(changeset) - }) - .await + Ok(is_changed) + })?; + Ok(changeset) + }) + .await?; + + match self.view_manager.did_update_field(¶ms.field_id).await { + Ok(_) => {} + Err(e) => tracing::error!("View manager update field failed: {:?}", e), + } + Ok(()) } pub async fn create_block(&self, block_meta_rev: GridBlockMetaRevision) -> FlowyResult<()> { @@ -585,6 +596,7 @@ impl GridRevisionEditor { .move_group_row(row_rev, to_group_id, to_row_id.clone()) .await { + tracing::trace!("Move group row cause row data changed: {:?}", row_changeset); match self.block_manager.update_row(row_changeset).await { Ok(_) => {} Err(e) => { diff --git a/frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs b/frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs index 015756954b..6a04e18815 100644 --- a/frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs +++ b/frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs @@ -59,7 +59,7 @@ impl GridViewRevisionEditor { rev_manager: rev_manager.clone(), view_pad: pad.clone(), }; - let group_service = GroupService::new(configuration_reader, configuration_writer).await; + let group_service = GroupService::new(view_id.clone(), configuration_reader, configuration_writer).await; let user_id = user_id.to_owned(); let did_load_group = AtomicBool::new(false); Ok(Self { @@ -155,7 +155,7 @@ impl GridViewRevisionEditor { } } } - + /// Only call once after grid view editor initialized #[tracing::instrument(level = "trace", skip(self))] pub(crate) async fn load_groups(&self) -> FlowyResult> { let groups = if !self.did_load_group.load(Ordering::SeqCst) { @@ -198,9 +198,10 @@ impl GridViewRevisionEditor { }; let changeset = GroupViewChangesetPB { - view_id: "".to_string(), + view_id: self.view_id.clone(), inserted_groups: vec![inserted_group], deleted_groups: vec![params.from_group_id.clone()], + update_groups: vec![], }; self.notify_did_update_view(changeset).await; @@ -252,10 +253,15 @@ impl GridViewRevisionEditor { }) .await } - + #[tracing::instrument(level = "trace", skip_all, err)] pub(crate) async fn did_update_field(&self, field_id: &str) -> FlowyResult<()> { - if let Some(field_rev) = self.field_delegate.get_field_rev(&field_id).await { - let _ = self.group_service.write().await.did_update_field(&field_rev).await?; + if let Some(field_rev) = self.field_delegate.get_field_rev(field_id).await { + match self.group_service.write().await.did_update_field(&field_rev).await? { + None => {} + Some(changeset) => { + self.notify_did_update_view(changeset).await; + } + } } Ok(()) } @@ -272,7 +278,6 @@ impl GridViewRevisionEditor { .send(); } - #[allow(dead_code)] async fn modify(&self, f: F) -> FlowyResult<()> where F: for<'a> FnOnce(&'a mut GridViewRevisionPad) -> FlowyResult>, diff --git a/frontend/rust-lib/flowy-grid/src/services/grid_view_manager.rs b/frontend/rust-lib/flowy-grid/src/services/grid_view_manager.rs index b93ac2871f..657058b31f 100644 --- a/frontend/rust-lib/flowy-grid/src/services/grid_view_manager.rs +++ b/frontend/rust-lib/flowy-grid/src/services/grid_view_manager.rs @@ -142,10 +142,10 @@ impl GridViewManager { .await; } - if row_changeset.has_changed() { - Some(row_changeset) - } else { + if row_changeset.is_empty() { None + } else { + Some(row_changeset) } } diff --git a/frontend/rust-lib/flowy-grid/src/services/group/configuration.rs b/frontend/rust-lib/flowy-grid/src/services/group/configuration.rs index a462278d2b..5126c9b2fc 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/configuration.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/configuration.rs @@ -1,12 +1,12 @@ +use crate::entities::{GroupPB, GroupViewChangesetPB, InsertedGroupPB}; use crate::services::group::{default_group_configuration, Group}; use flowy_error::{FlowyError, FlowyResult}; use flowy_grid_data_model::revision::{ FieldRevision, FieldTypeRevision, GroupConfigurationContentSerde, GroupConfigurationRevision, GroupRecordRevision, }; -use std::marker::PhantomData; - use indexmap::IndexMap; use lib_infra::future::AFFuture; +use std::marker::PhantomData; use std::sync::Arc; pub trait GroupConfigurationReader: Send + Sync + 'static { @@ -26,6 +26,7 @@ pub trait GroupConfigurationWriter: Send + Sync + 'static { } pub struct GenericGroupConfiguration { + view_id: String, pub configuration: Arc, configuration_content: PhantomData, field_rev: Arc, @@ -39,6 +40,7 @@ where { #[tracing::instrument(level = "trace", skip_all, err)] pub async fn new( + view_id: String, field_rev: Arc, reader: Arc, writer: Arc, @@ -56,6 +58,7 @@ where // let configuration = C::from_configuration_content(&configuration_rev.content)?; Ok(Self { + view_id, field_rev, groups_map: IndexMap::new(), writer, @@ -72,8 +75,18 @@ where self.groups_map.values().cloned().collect() } - pub(crate) async fn merge_groups(&mut self, groups: Vec) -> FlowyResult<()> { - let (group_revs, groups) = merge_groups(&self.configuration.groups, groups); + pub(crate) fn merge_groups(&mut self, groups: Vec) -> FlowyResult> { + let MergeGroupResult { + groups, + inserted_groups, + updated_groups, + } = merge_groups(&self.configuration.groups, groups); + + let group_revs = groups + .iter() + .map(|group| GroupRecordRevision::new(group.id.clone(), group.name.clone())) + .collect(); + self.mut_configuration(move |configuration| { configuration.groups = group_revs; true @@ -82,7 +95,14 @@ where groups.into_iter().for_each(|group| { self.groups_map.insert(group.id.clone(), group); }); - Ok(()) + + let changeset = make_group_view_changeset(self.view_id.clone(), inserted_groups, updated_groups); + tracing::trace!("Group changeset: {:?}", changeset); + if changeset.is_empty() { + Ok(None) + } else { + Ok(Some(changeset)) + } } #[allow(dead_code)] @@ -101,7 +121,7 @@ where Ok(()) } - pub(crate) fn with_mut_groups(&mut self, mut each: impl FnMut(&mut Group)) { + pub(crate) fn iter_mut_groups(&mut self, mut each: impl FnMut(&mut Group)) { self.groups_map.iter_mut().for_each(|(_, group)| { each(group); }) @@ -189,33 +209,82 @@ where } } -fn merge_groups(old_group_revs: &[GroupRecordRevision], groups: Vec) -> (Vec, Vec) { - if old_group_revs.is_empty() { - let new_groups = groups - .iter() - .map(|group| GroupRecordRevision::new(group.id.clone())) - .collect(); - return (new_groups, groups); +fn merge_groups(old_groups: &[GroupRecordRevision], groups: Vec) -> MergeGroupResult { + let mut merge_result = MergeGroupResult::new(); + if old_groups.is_empty() { + merge_result.groups = groups; + return merge_result; } + // group_map is a helper map is used to filter out the new groups. let mut group_map: IndexMap = IndexMap::new(); groups.into_iter().for_each(|group| { group_map.insert(group.id.clone(), group); }); - // Inert - let mut sorted_groups: Vec = vec![]; - for group_rev in old_group_revs { + // The group is ordered in old groups. Add them before adding the new groups + for group_rev in old_groups { if let Some(group) = group_map.remove(&group_rev.group_id) { - sorted_groups.push(group); + if group.name == group_rev.name { + merge_result.add_group(group); + } else { + merge_result.add_updated_group(group); + } } } - sorted_groups.extend(group_map.into_values().collect::>()); - let new_group_revs = sorted_groups - .iter() - .map(|group| GroupRecordRevision::new(group.id.clone())) - .collect::>(); - tracing::trace!("group revs: {}, groups: {}", new_group_revs.len(), sorted_groups.len()); - (new_group_revs, sorted_groups) + // Find out the new groups + let new_groups = group_map.into_values().collect::>(); + for (index, group) in new_groups.into_iter().enumerate() { + merge_result.add_insert_group(index, group); + } + merge_result +} + +struct MergeGroupResult { + groups: Vec, + inserted_groups: Vec, + updated_groups: Vec, +} + +impl MergeGroupResult { + fn new() -> Self { + Self { + groups: vec![], + inserted_groups: vec![], + updated_groups: vec![], + } + } + + fn add_updated_group(&mut self, group: Group) { + self.groups.push(group.clone()); + self.updated_groups.push(group); + } + + fn add_group(&mut self, group: Group) { + self.groups.push(group.clone()); + } + + fn add_insert_group(&mut self, index: usize, group: Group) { + self.groups.push(group.clone()); + let inserted_group = InsertedGroupPB { + group: GroupPB::from(group), + index: index as i32, + }; + self.inserted_groups.push(inserted_group); + } +} + +fn make_group_view_changeset( + view_id: String, + inserted_groups: Vec, + updated_group: Vec, +) -> GroupViewChangesetPB { + let changeset = GroupViewChangesetPB { + view_id, + inserted_groups, + deleted_groups: vec![], + update_groups: updated_group.into_iter().map(GroupPB::from).collect(), + }; + changeset } diff --git a/frontend/rust-lib/flowy-grid/src/services/group/controller.rs b/frontend/rust-lib/flowy-grid/src/services/group/controller.rs index 7cf7897dfc..1dae47c0db 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/controller.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/controller.rs @@ -1,4 +1,4 @@ -use crate::entities::{GroupChangesetPB, RowPB}; +use crate::entities::{GroupChangesetPB, GroupViewChangesetPB, RowPB}; use crate::services::cell::{decode_any_cell_data, CellBytesParser}; use crate::services::group::action::GroupAction; use crate::services::group::configuration::GenericGroupConfiguration; @@ -61,7 +61,7 @@ pub trait GroupControllerSharedOperation: Send + Sync { fn move_group_row(&mut self, context: MoveGroupRowContext) -> FlowyResult>; - fn did_update_field(&mut self, field_rev: &FieldRevision) -> FlowyResult<()>; + fn did_update_field(&mut self, field_rev: &FieldRevision) -> FlowyResult>; } /// C: represents the group configuration that impl [GroupConfigurationSerde] @@ -91,7 +91,7 @@ where let field_type_rev = field_rev.ty; let type_option = field_rev.get_type_option_entry::(field_type_rev); let groups = G::generate_groups(&field_rev.id, &configuration, &type_option); - let _ = configuration.merge_groups(groups).await?; + let _ = configuration.merge_groups(groups)?; let default_group = Group::new( DEFAULT_GROUP_ID.to_owned(), field_rev.id.clone(), @@ -114,6 +114,9 @@ impl GroupControllerSharedOperation for GenericGroupController, TypeOptionType = T>, + Self: GroupAction, { fn field_id(&self) -> &str { @@ -179,7 +182,8 @@ where if let Some(cell_rev) = row_rev.cells.get(&self.field_id) { let cell_bytes = decode_any_cell_data(cell_rev.data.clone(), field_rev); let cell_data = cell_bytes.parser::

()?; - Ok(self.add_row_if_match(row_rev, &cell_data)) + let changesets = self.add_row_if_match(row_rev, &cell_data); + Ok(changesets) } else { Ok(vec![]) } @@ -209,8 +213,12 @@ where } } - fn did_update_field(&mut self, field_rev: &FieldRevision) -> FlowyResult<()> { - todo!() + fn did_update_field(&mut self, field_rev: &FieldRevision) -> FlowyResult> { + let field_type_rev = field_rev.ty; + let type_option = field_rev.get_type_option_entry::(field_type_rev); + let groups = G::generate_groups(&field_rev.id, &self.configuration, &type_option); + let changeset = self.configuration.merge_groups(groups)?; + Ok(changeset) } } diff --git a/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/multi_select_controller.rs b/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/multi_select_controller.rs index 0c1a7117d4..fe90e1b462 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/multi_select_controller.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/multi_select_controller.rs @@ -27,24 +27,30 @@ impl GroupAction for MultiSelectGroupController { fn add_row_if_match(&mut self, row_rev: &RowRevision, cell_data: &Self::CellDataType) -> Vec { let mut changesets = vec![]; - self.configuration.with_mut_groups(|group| { - add_row(group, &mut changesets, cell_data, row_rev); + self.configuration.iter_mut_groups(|group| { + if let Some(changeset) = add_row(group, cell_data, row_rev) { + changesets.push(changeset); + } }); changesets } fn remove_row_if_match(&mut self, row_rev: &RowRevision, cell_data: &Self::CellDataType) -> Vec { let mut changesets = vec![]; - self.configuration.with_mut_groups(|group| { - remove_row(group, &mut changesets, cell_data, row_rev); + self.configuration.iter_mut_groups(|group| { + if let Some(changeset) = remove_row(group, cell_data, row_rev) { + changesets.push(changeset); + } }); changesets } fn move_row(&mut self, cell_data: &Self::CellDataType, mut context: MoveGroupRowContext) -> Vec { let mut group_changeset = vec![]; - self.configuration.with_mut_groups(|group| { - move_select_option_row(group, &mut group_changeset, cell_data, &mut context); + self.configuration.iter_mut_groups(|group| { + if let Some(changeset) = move_select_option_row(group, cell_data, &mut context) { + group_changeset.push(changeset); + } }); group_changeset } diff --git a/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/single_select_controller.rs b/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/single_select_controller.rs index ad4d296fab..d774ab083f 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/single_select_controller.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/single_select_controller.rs @@ -27,24 +27,30 @@ impl GroupAction for SingleSelectGroupController { fn add_row_if_match(&mut self, row_rev: &RowRevision, cell_data: &Self::CellDataType) -> Vec { let mut changesets = vec![]; - self.configuration.with_mut_groups(|group| { - add_row(group, &mut changesets, cell_data, row_rev); + self.configuration.iter_mut_groups(|group| { + if let Some(changeset) = add_row(group, cell_data, row_rev) { + changesets.push(changeset); + } }); changesets } fn remove_row_if_match(&mut self, row_rev: &RowRevision, cell_data: &Self::CellDataType) -> Vec { let mut changesets = vec![]; - self.configuration.with_mut_groups(|group| { - remove_row(group, &mut changesets, cell_data, row_rev); + self.configuration.iter_mut_groups(|group| { + if let Some(changeset) = remove_row(group, cell_data, row_rev) { + changesets.push(changeset); + } }); changesets } fn move_row(&mut self, cell_data: &Self::CellDataType, mut context: MoveGroupRowContext) -> Vec { let mut group_changeset = vec![]; - self.configuration.with_mut_groups(|group| { - move_select_option_row(group, &mut group_changeset, cell_data, &mut context); + self.configuration.iter_mut_groups(|group| { + if let Some(changeset) = move_select_option_row(group, cell_data, &mut context) { + group_changeset.push(changeset); + } }); group_changeset } diff --git a/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/util.rs b/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/util.rs index 9a1658c19f..349f7391a6 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/util.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/util.rs @@ -11,47 +11,56 @@ pub type SelectOptionGroupConfiguration = GenericGroupConfiguration, cell_data: &SelectOptionCellDataPB, row_rev: &RowRevision, -) { +) -> Option { + let mut changeset = GroupChangesetPB::new(group.id.clone()); cell_data.select_options.iter().for_each(|option| { if option.id == group.id { if !group.contains_row(&row_rev.id) { let row_pb = RowPB::from(row_rev); - changesets.push(GroupChangesetPB::insert( - group.id.clone(), - vec![InsertedRowPB::new(row_pb.clone())], - )); + changeset.inserted_rows.push(InsertedRowPB::new(row_pb.clone())); group.add_row(row_pb); } } else if group.contains_row(&row_rev.id) { - changesets.push(GroupChangesetPB::delete(group.id.clone(), vec![row_rev.id.clone()])); + changeset.deleted_rows.push(row_rev.id.clone()); group.remove_row(&row_rev.id); } }); + + if changeset.is_empty() { + None + } else { + Some(changeset) + } } pub fn remove_row( group: &mut Group, - changesets: &mut Vec, cell_data: &SelectOptionCellDataPB, row_rev: &RowRevision, -) { +) -> Option { + let mut changeset = GroupChangesetPB::new(group.id.clone()); cell_data.select_options.iter().for_each(|option| { if option.id == group.id && group.contains_row(&row_rev.id) { - changesets.push(GroupChangesetPB::delete(group.id.clone(), vec![row_rev.id.clone()])); + changeset.deleted_rows.push(row_rev.id.clone()); group.remove_row(&row_rev.id); } }); + + if changeset.is_empty() { + None + } else { + Some(changeset) + } } pub fn move_select_option_row( group: &mut Group, - group_changeset: &mut Vec, _cell_data: &SelectOptionCellDataPB, context: &mut MoveGroupRowContext, -) { +) -> Option { + let mut changeset = GroupChangesetPB::new(group.id.clone()); let MoveGroupRowContext { row_rev, row_changeset, @@ -68,7 +77,7 @@ pub fn move_select_option_row( // Remove the row in which group contains it if from_index.is_some() { - group_changeset.push(GroupChangesetPB::delete(group.id.clone(), vec![row_rev.id.clone()])); + changeset.deleted_rows.push(row_rev.id.clone()); tracing::debug!("Group:{} remove row:{}", group.id, row_rev.id); group.remove_row(&row_rev.id); } @@ -78,7 +87,7 @@ pub fn move_select_option_row( let mut inserted_row = InsertedRowPB::new(row_pb.clone()); match to_index { None => { - group_changeset.push(GroupChangesetPB::insert(group.id.clone(), vec![inserted_row])); + changeset.inserted_rows.push(inserted_row); tracing::debug!("Group:{} append row:{}", group.id, row_rev.id); group.add_row(row_pb); } @@ -91,7 +100,7 @@ pub fn move_select_option_row( tracing::debug!("Group:{} append row:{}", group.id, row_rev.id); group.add_row(row_pb); } - group_changeset.push(GroupChangesetPB::insert(group.id.clone(), vec![inserted_row])); + changeset.inserted_rows.push(inserted_row); } } @@ -100,6 +109,12 @@ pub fn move_select_option_row( tracing::debug!("Mark row:{} belong to group:{}", row_rev.id, group.id); let cell_rev = insert_select_option_cell(group.id.clone(), field_rev); row_changeset.cell_by_field_id.insert(field_rev.id.clone(), cell_rev); + changeset.updated_rows.push(RowPB::from(*row_rev)); } } + if changeset.is_empty() { + None + } else { + Some(changeset) + } } diff --git a/frontend/rust-lib/flowy-grid/src/services/group/entities.rs b/frontend/rust-lib/flowy-grid/src/services/group/entities.rs index dd4171afb0..d8c4169eaf 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/entities.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/entities.rs @@ -1,33 +1,22 @@ -use crate::entities::{GroupPB, RowPB}; +use crate::entities::RowPB; -#[derive(Clone)] +#[derive(Clone, PartialEq, Eq)] pub struct Group { pub id: String, pub field_id: String, - pub desc: String, - rows: Vec, + pub name: String, + pub(crate) rows: Vec, /// [content] is used to determine which group the cell belongs to. pub content: String, } -impl std::convert::From for GroupPB { - fn from(group: Group) -> Self { - Self { - field_id: group.field_id, - group_id: group.id, - desc: group.desc, - rows: group.rows, - } - } -} - impl Group { - pub fn new(id: String, field_id: String, desc: String, content: String) -> Self { + pub fn new(id: String, field_id: String, name: String, content: String) -> Self { Self { id, field_id, - desc, + name, rows: vec![], content, } diff --git a/frontend/rust-lib/flowy-grid/src/services/group/group_service.rs b/frontend/rust-lib/flowy-grid/src/services/group/group_service.rs index a6ae504c05..c7d67f65d6 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/group_service.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/group_service.rs @@ -1,4 +1,4 @@ -use crate::entities::{FieldType, GroupChangesetPB}; +use crate::entities::{FieldType, GroupChangesetPB, GroupViewChangesetPB}; use crate::services::group::configuration::GroupConfigurationReader; use crate::services::group::controller::{GroupController, MoveGroupRowContext}; use crate::services::group::{ @@ -15,18 +15,20 @@ use std::future::Future; use std::sync::Arc; pub(crate) struct GroupService { + view_id: String, configuration_reader: Arc, configuration_writer: Arc, group_controller: Option>, } impl GroupService { - pub(crate) async fn new(configuration_reader: R, configuration_writer: W) -> Self + pub(crate) async fn new(view_id: String, configuration_reader: R, configuration_writer: W) -> Self where R: GroupConfigurationReader, W: GroupConfigurationWriter, { Self { + view_id, configuration_reader: Arc::new(configuration_reader), configuration_writer: Arc::new(configuration_writer), group_controller: None, @@ -36,8 +38,8 @@ impl GroupService { pub(crate) async fn groups(&self) -> Vec { self.group_controller .as_ref() - .and_then(|group_controller| Some(group_controller.groups())) - .unwrap_or(vec![]) + .map(|group_controller| group_controller.groups()) + .unwrap_or_default() } pub(crate) async fn get_group(&self, group_id: &str) -> Option<(usize, Group)> { @@ -170,9 +172,13 @@ impl GroupService { } } - pub(crate) async fn did_update_field(&mut self, field_rev: &FieldRevision) -> FlowyResult<()> { + #[tracing::instrument(level = "trace", name = "group_did_update_field", skip(self, field_rev), err)] + pub(crate) async fn did_update_field( + &mut self, + field_rev: &FieldRevision, + ) -> FlowyResult> { match self.group_controller.as_mut() { - None => Ok(()), + None => Ok(None), Some(group_controller) => group_controller.did_update_field(field_rev), } } @@ -196,6 +202,7 @@ impl GroupService { } FieldType::SingleSelect => { let configuration = SelectOptionGroupConfiguration::new( + self.view_id.clone(), field_rev.clone(), self.configuration_reader.clone(), self.configuration_writer.clone(), @@ -206,6 +213,7 @@ impl GroupService { } FieldType::MultiSelect => { let configuration = SelectOptionGroupConfiguration::new( + self.view_id.clone(), field_rev.clone(), self.configuration_reader.clone(), self.configuration_writer.clone(), @@ -216,6 +224,7 @@ impl GroupService { } FieldType::Checkbox => { let configuration = CheckboxGroupConfiguration::new( + self.view_id.clone(), field_rev.clone(), self.configuration_reader.clone(), self.configuration_writer.clone(), diff --git a/frontend/rust-lib/flowy-grid/tests/grid/group_test/script.rs b/frontend/rust-lib/flowy-grid/tests/grid/group_test/script.rs index 94ef4dff1b..3a5ae9455b 100644 --- a/frontend/rust-lib/flowy-grid/tests/grid/group_test/script.rs +++ b/frontend/rust-lib/flowy-grid/tests/grid/group_test/script.rs @@ -1,9 +1,11 @@ use crate::grid::grid_editor::GridEditorTest; use flowy_grid::entities::{ - CreateRowParams, FieldType, GridLayout, GroupPB, MoveGroupParams, MoveGroupRowParams, RowPB, + CreateRowParams, FieldChangesetParams, FieldType, GridLayout, GroupPB, MoveGroupParams, MoveGroupRowParams, RowPB, }; use flowy_grid::services::cell::insert_select_option_cell; use flowy_grid_data_model::revision::RowChangeset; +use std::time::Duration; +use tokio::time::interval; pub enum GroupScript { AssertGroupRowCount { @@ -42,6 +44,9 @@ pub enum GroupScript { from_group_index: usize, to_group_index: usize, }, + UpdateField { + changeset: FieldChangesetParams, + }, } pub struct GridGroupTest { @@ -156,6 +161,12 @@ impl GridGroupTest { } => { let group = self.group_at_index(group_index).await; assert_eq!(group.group_id, group_pb.group_id); + assert_eq!(group.desc, group_pb.desc); + } + GroupScript::UpdateField { changeset } => { + self.editor.update_field(changeset).await.unwrap(); + let mut interval = interval(Duration::from_millis(130)); + interval.tick().await; } } } diff --git a/frontend/rust-lib/flowy-grid/tests/grid/group_test/test.rs b/frontend/rust-lib/flowy-grid/tests/grid/group_test/test.rs index bebf499773..d6a9839f77 100644 --- a/frontend/rust-lib/flowy-grid/tests/grid/group_test/test.rs +++ b/frontend/rust-lib/flowy-grid/tests/grid/group_test/test.rs @@ -1,5 +1,6 @@ use crate::grid::group_test::script::GridGroupTest; use crate::grid::group_test::script::GroupScript::*; +use flowy_grid::entities::FieldChangesetParams; #[tokio::test] async fn group_init_test() { @@ -314,3 +315,25 @@ async fn group_move_group_test() { ]; test.run_scripts(scripts).await; } + +#[tokio::test] +async fn group_update_field_test() { + let mut test = GridGroupTest::new().await; + let mut group = test.group_at_index(0).await; + let changeset = FieldChangesetParams { + field_id: group.field_id.clone(), + grid_id: test.grid_id.clone(), + name: Some("ABC".to_string()), + ..Default::default() + }; + + // group.desc = "ABC".to_string(); + let scripts = vec![ + UpdateField { changeset }, + AssertGroup { + group_index: 0, + expected_group: group, + }, + ]; + test.run_scripts(scripts).await; +} diff --git a/shared-lib/flowy-grid-data-model/src/revision/grid_block.rs b/shared-lib/flowy-grid-data-model/src/revision/grid_block.rs index 5464d83877..ba113810f6 100644 --- a/shared-lib/flowy-grid-data-model/src/revision/grid_block.rs +++ b/shared-lib/flowy-grid-data-model/src/revision/grid_block.rs @@ -59,8 +59,8 @@ impl RowChangeset { } } - pub fn has_changed(&self) -> bool { - self.height.is_some() || self.visibility.is_some() || !self.cell_by_field_id.is_empty() + pub fn is_empty(&self) -> bool { + self.height.is_none() && self.visibility.is_none() && self.cell_by_field_id.is_empty() } } diff --git a/shared-lib/flowy-grid-data-model/src/revision/group_rev.rs b/shared-lib/flowy-grid-data-model/src/revision/group_rev.rs index 97d45295fe..ce378c0ffb 100644 --- a/shared-lib/flowy-grid-data-model/src/revision/group_rev.rs +++ b/shared-lib/flowy-grid-data-model/src/revision/group_rev.rs @@ -110,6 +110,9 @@ impl GroupConfigurationContentSerde for SelectOptionGroupConfigurationRevision { pub struct GroupRecordRevision { pub group_id: String, + #[serde(default)] + pub name: String, + #[serde(default = "DEFAULT_GROUP_RECORD_VISIBILITY")] pub visible: bool, } @@ -117,9 +120,10 @@ pub struct GroupRecordRevision { const DEFAULT_GROUP_RECORD_VISIBILITY: fn() -> bool = || true; impl GroupRecordRevision { - pub fn new(group_id: String) -> Self { + pub fn new(group_id: String, group_name: String) -> Self { Self { group_id, + name: group_name, visible: true, } } From 2c37bf806e46a14a1f7f009c251c329d19c62672 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Wed, 24 Aug 2022 17:23:45 +0800 Subject: [PATCH 028/106] feat: add error tips when loading image fail --- .../example/assets/example.json | 15 +++++----- .../src/render/image/image_node_builder.dart | 5 +++- .../src/render/image/image_node_widget.dart | 30 +++++++++++++++++-- .../src/render/image/image_upload_widget.dart | 12 ++++++-- .../selection_menu/selection_menu_widget.dart | 6 ++-- .../delete_text_handler.dart | 8 +++-- 6 files changed, 58 insertions(+), 18 deletions(-) diff --git a/frontend/app_flowy/packages/appflowy_editor/example/assets/example.json b/frontend/app_flowy/packages/appflowy_editor/example/assets/example.json index c6b27d9ae1..b522a68594 100644 --- a/frontend/app_flowy/packages/appflowy_editor/example/assets/example.json +++ b/frontend/app_flowy/packages/appflowy_editor/example/assets/example.json @@ -59,13 +59,6 @@ } ] }, - { - "type": "image", - "attributes": { - "image_src": "https://s1.ax1x.com/2022/08/24/vgAJED.png", - "align": "center" - } - }, { "type": "text", "delta": [ @@ -129,6 +122,14 @@ "heading": "h3" } }, + { + "type": "image", + "attributes": { + "image_src": "https://s1.ax1x.com/2022/08/24/vgAJED.png", + "align": "left", + "width": 300 + } + }, { "type": "text", "delta": [ diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_builder.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_builder.dart index f62378fc1a..796e96c250 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_builder.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_builder.dart @@ -11,7 +11,10 @@ class ImageNodeBuilder extends NodeWidgetBuilder { Widget build(NodeWidgetContext context) { final src = context.node.attributes['image_src']; final align = context.node.attributes['align']; - final width = context.node.attributes['width']; + double? width; + if (context.node.attributes.containsKey('width')) { + width = context.node.attributes['width'].toDouble(); + } return ImageNodeWidget( key: context.node.key, src: src, diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_widget.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_widget.dart index 110cff01b3..2cc0916b66 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_widget.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_widget.dart @@ -1,4 +1,5 @@ import 'package:appflowy_editor/src/infra/flowy_svg.dart'; +import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart'; import 'package:flutter/material.dart'; class ImageNodeWidget extends StatefulWidget { @@ -55,7 +56,12 @@ class _ImageNodeWidgetState extends State { @override Widget build(BuildContext context) { // only support network image. - return _buildNetworkImage(context); + + return Container( + width: defaultMaxTextNodeWidth, + padding: const EdgeInsets.only(top: 8, bottom: 8), + child: _buildNetworkImage(context), + ); } Widget _buildNetworkImage(BuildContext context) { @@ -77,8 +83,13 @@ class _ImageNodeWidgetState extends State { final networkImage = Image.network( widget.src, width: _imageWidth == null ? null : _imageWidth! - _distance, + gaplessPlayback: true, loadingBuilder: (context, child, loadingProgress) => loadingProgress == null ? child : _buildLoading(context), + errorBuilder: (context, error, stackTrace) { + _imageWidth ??= defaultMaxTextNodeWidth; + return _buildError(context); + }, ); if (_imageWidth == null) { _imageStream = networkImage.image.resolve(const ImageConfiguration()) @@ -127,8 +138,7 @@ class _ImageNodeWidgetState extends State { Widget _buildLoading(BuildContext context) { return SizedBox( - width: _imageWidth, - height: 300, + height: 150, child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ @@ -145,6 +155,20 @@ class _ImageNodeWidgetState extends State { ); } + Widget _buildError(BuildContext context) { + return Container( + height: 100, + width: _imageWidth, + alignment: Alignment.center, + padding: const EdgeInsets.only(top: 8.0, bottom: 8.0), + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(4.0)), + border: Border.all(width: 1, color: Colors.black), + ), + child: const Text('Could not load the image'), + ); + } + Widget _buildEdgeGesture( BuildContext context, { double? top, diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_upload_widget.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_upload_widget.dart index c7353532ab..a8728341df 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_upload_widget.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_upload_widget.dart @@ -8,6 +8,7 @@ import 'package:appflowy_editor/src/render/selection_menu/selection_menu_service import 'package:flutter/material.dart'; OverlayEntry? _imageUploadMenu; +EditorState? _editorState; void showImageUploadMenu( EditorState editorState, SelectionMenuService menuService, @@ -23,11 +24,11 @@ void showImageUploadMenu( child: Material( child: ImageUploadMenu( onSubmitted: (text) { - _dismissImageUploadMenu(); + // _dismissImageUploadMenu(); editorState.insertImageNode(text); }, onUpload: (text) { - _dismissImageUploadMenu(); + // _dismissImageUploadMenu(); editorState.insertImageNode(text); }, ), @@ -36,11 +37,18 @@ void showImageUploadMenu( }); Overlay.of(context)?.insert(_imageUploadMenu!); + + editorState.service.selectionService.currentSelection + .addListener(_dismissImageUploadMenu); } void _dismissImageUploadMenu() { _imageUploadMenu?.remove(); _imageUploadMenu = null; + + _editorState?.service.selectionService.currentSelection + .removeListener(_dismissImageUploadMenu); + _editorState = null; } class ImageUploadMenu extends StatefulWidget { diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_widget.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_widget.dart index f73251081f..1553085349 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_widget.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_widget.dart @@ -205,8 +205,10 @@ class _SelectionMenuWidgetState extends State { if (event.logicalKey == LogicalKeyboardKey.enter) { if (0 <= _selectedIndex && _selectedIndex < _showingItems.length) { _deleteLastCharacters(length: keyword.length + 1); - _showingItems[_selectedIndex] - .handler(widget.editorState, widget.menuService, context); + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + _showingItems[_selectedIndex] + .handler(widget.editorState, widget.menuService, context); + }); return KeyEventResult.handled; } } else if (event.logicalKey == LogicalKeyboardKey.escape) { diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/delete_text_handler.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/delete_text_handler.dart index 24049dbe1b..d2a3d51e64 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/delete_text_handler.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/delete_text_handler.dart @@ -34,9 +34,9 @@ KeyEventResult _handleBackspace(EditorState editorState, RawKeyEvent event) { } else { // 2. non-style // find previous text node. - while (textNode.previous != null) { - if (textNode.previous is TextNode) { - final previous = textNode.previous as TextNode; + var previous = textNode.previous; + while (previous != null) { + if (previous is TextNode) { transactionBuilder ..mergeText(previous, textNode) ..deleteNode(textNode) @@ -47,6 +47,8 @@ KeyEventResult _handleBackspace(EditorState editorState, RawKeyEvent event) { ), ); break; + } else { + previous = previous.previous; } } } From ba5c1804ce955ed0359a661b5da7a07335a5a678 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Wed, 24 Aug 2022 18:02:00 +0800 Subject: [PATCH 029/106] fix: the text enterd after link doesn't need to be linked --- .../src/operation/transaction_builder.dart | 8 ++- .../lib/src/render/rich_text/quoted_text.dart | 53 +++++++++---------- .../lib/src/service/input_service.dart | 4 ++ 3 files changed, 35 insertions(+), 30 deletions(-) diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/operation/transaction_builder.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/operation/transaction_builder.dart index 4f6de6e9b0..12c13bf2e5 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/operation/transaction_builder.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/operation/transaction_builder.dart @@ -116,11 +116,17 @@ class TransactionBuilder { /// Optionally, you may specify formatting attributes that are applied to the inserted string. /// By default, the formatting attributes before the insert position will be used. insertText(TextNode node, int index, String content, - [Attributes? attributes]) { + {Attributes? attributes, Attributes? removedAttributes}) { var newAttributes = attributes; if (index != 0 && attributes == null) { newAttributes = node.delta.slice(max(index - 1, 0), index).first.attributes; + if (newAttributes != null) { + newAttributes = Attributes.from(newAttributes); + if (removedAttributes != null) { + newAttributes.addAll(removedAttributes); + } + } } textEdit( node, diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/quoted_text.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/quoted_text.dart index 3391729779..78c6653904 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/quoted_text.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/quoted_text.dart @@ -56,36 +56,31 @@ class _QuotedTextNodeWidgetState extends State @override Widget build(BuildContext context) { return SizedBox( - width: defaultMaxTextNodeWidth, - child: Padding( - padding: EdgeInsets.only(bottom: defaultLinePadding), - child: IntrinsicHeight( - child: Row( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - FlowySvg( - key: iconKey, - width: _iconWidth, - padding: EdgeInsets.only(right: _iconRightPadding), - name: 'quote', + width: defaultMaxTextNodeWidth, + child: Padding( + padding: EdgeInsets.only(bottom: defaultLinePadding), + child: IntrinsicHeight( + child: Row( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + FlowySvg( + key: iconKey, + width: _iconWidth, + padding: EdgeInsets.only(right: _iconRightPadding), + name: 'quote', + ), + Expanded( + child: FlowyRichText( + key: _richTextKey, + placeholderText: 'Quote', + textNode: widget.textNode, + editorState: widget.editorState, ), - Expanded( - child: FlowyRichText( - key: _richTextKey, - placeholderText: 'Quote', - textNode: widget.textNode, - editorState: widget.editorState, - ), - ), - ], - ), + ), + ], ), - )); - } - - double get _quoteHeight { - final lines = - widget.textNode.toRawString().characters.where((c) => c == '\n').length; - return (lines + 1) * _iconWidth; + ), + ), + ); } } diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/input_service.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/input_service.dart index a92fae1b95..96f0777544 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/input_service.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/input_service.dart @@ -1,4 +1,5 @@ import 'package:appflowy_editor/src/infra/log.dart'; +import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -149,6 +150,9 @@ class _AppFlowyInputState extends State textNode, delta.insertionOffset, delta.textInserted, + removedAttributes: { + StyleKey.href: null, + }, ) ..commit(); } else { From a896637eab991a9f6c176e78890f5b4cd5bb6016 Mon Sep 17 00:00:00 2001 From: appflowy Date: Wed, 24 Aug 2022 21:06:10 +0800 Subject: [PATCH 030/106] fix: reload card content --- .../plugins/board/application/board_bloc.dart | 5 +++- .../app_flowy/lib/plugins/board/board.dart | 2 +- .../card/board_select_option_cell.dart | 16 +++++------ .../plugins/board/presentation/card/card.dart | 8 +++++- .../cell/cell_service/cell_data_loader.dart | 28 +++++++++++-------- .../cell/cell_service/context_builder.dart | 3 +- .../board_column/board_column_data.dart | 2 -- .../flowy-grid/src/services/grid_editor.rs | 22 +++++++++++---- 8 files changed, 54 insertions(+), 32 deletions(-) diff --git a/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart b/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart index 7c04dec814..422f1dff2b 100644 --- a/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart +++ b/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart @@ -317,6 +317,9 @@ class GroupControllerDelegateImpl extends GroupControllerDelegate { @override void updateRow(String groupId, RowPB row) { - controller.updateColumnItem(groupId, BoardColumnItem(row: row)); + // workaround: fix the board card reload timing issue. + Future.delayed(const Duration(milliseconds: 300), () { + controller.updateColumnItem(groupId, BoardColumnItem(row: row)); + }); } } diff --git a/frontend/app_flowy/lib/plugins/board/board.dart b/frontend/app_flowy/lib/plugins/board/board.dart index 213cc8bc3c..c55d7f2e17 100644 --- a/frontend/app_flowy/lib/plugins/board/board.dart +++ b/frontend/app_flowy/lib/plugins/board/board.dart @@ -31,7 +31,7 @@ class BoardPluginBuilder implements PluginBuilder { class BoardPluginConfig implements PluginConfig { @override - bool get creatable => true; + bool get creatable => false; } class BoardPlugin extends Plugin { diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/board_select_option_cell.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/board_select_option_cell.dart index 373bb3c850..a51a36f99f 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/board_select_option_cell.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/board_select_option_cell.dart @@ -35,19 +35,17 @@ class _BoardSelectOptionCellState extends State { child: BlocBuilder( builder: (context, state) { final children = state.selectedOptions - .map((option) => SelectOptionTag.fromOption( - context: context, - option: option, - )) + .map( + (option) => SelectOptionTag.fromOption( + context: context, + option: option, + ), + ) .toList(); return Align( alignment: Alignment.centerLeft, child: AbsorbPointer( - child: Wrap( - children: children, - spacing: 4, - runSpacing: 2, - ), + child: Wrap(children: children, spacing: 4, runSpacing: 2), ), ); }, diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart index a5c7b7ba2c..226a9e8241 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart @@ -42,7 +42,7 @@ class _BoardCardState extends State { _cardBloc = BoardCardBloc( gridId: widget.gridId, dataController: widget.dataController, - ); + )..add(const BoardCardEvent.initial()); super.initState(); } @@ -79,6 +79,12 @@ class _BoardCardState extends State { }, ).toList(); } + + @override + Future dispose() async { + _cardBloc.close(); + super.dispose(); + } } class _CardMoreOption extends StatelessWidget with CardAccessory { diff --git a/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/cell_data_loader.dart b/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/cell_data_loader.dart index c4b3430199..a6a1ba43a9 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/cell_data_loader.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/cell_data_loader.dart @@ -24,18 +24,21 @@ class GridCellDataLoader { Future loadData() { final fut = service.getCell(cellId: cellId); return fut.then( - (result) => result.fold((GridCellPB cell) { - try { - return parser.parserData(cell.data); - } catch (e, s) { - Log.error('$parser parser cellData failed, $e'); - Log.error('Stack trace \n $s'); + (result) => result.fold( + (GridCellPB cell) { + try { + return parser.parserData(cell.data); + } catch (e, s) { + Log.error('$parser parser cellData failed, $e'); + Log.error('Stack trace \n $s'); + return null; + } + }, + (err) { + Log.error(err); return null; - } - }, (err) { - Log.error(err); - return null; - }), + }, + ), ); } } @@ -58,7 +61,8 @@ class DateCellDataParser implements IGridCellDataParser { } } -class SelectOptionCellDataParser implements IGridCellDataParser { +class SelectOptionCellDataParser + implements IGridCellDataParser { @override SelectOptionCellDataPB? parserData(List data) { if (data.isEmpty) { diff --git a/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/context_builder.dart b/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/context_builder.dart index 1068cbf36b..f3ed73b449 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/context_builder.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/context_builder.dart @@ -279,8 +279,9 @@ class IGridCellController extends Equatable { _loadDataOperation?.cancel(); _loadDataOperation = Timer(const Duration(milliseconds: 10), () { _cellDataLoader.loadData().then((data) { - _cellDataNotifier?.value = data; + Log.debug('$fieldId CellData: Did Get cell data'); _cellsCache.insert(_cacheKey, GridCell(object: data)); + _cellDataNotifier?.value = data; }); }); } diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column_data.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column_data.dart index 8cf03d96c1..0015ebd479 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column_data.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column_data.dart @@ -133,8 +133,6 @@ class AFBoardColumnDataController extends ChangeNotifier with EquatableMixin { void replaceOrInsertItem(AFColumnItem newItem) { final index = columnData._items.indexWhere((item) => item.id == newItem.id); if (index != -1) { - removeAt(index); - columnData._items.removeAt(index); columnData._items.insert(index, newItem); notifyListeners(); diff --git a/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs b/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs index 8974cf0539..cdc6cf6e6b 100644 --- a/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs +++ b/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs @@ -582,7 +582,7 @@ impl GridRevisionEditor { pub async fn move_group_row(&self, params: MoveGroupRowParams) -> FlowyResult<()> { let MoveGroupRowParams { - view_id: _, + view_id, from_row_id, to_group_id, to_row_id, @@ -597,10 +597,22 @@ impl GridRevisionEditor { .await { tracing::trace!("Move group row cause row data changed: {:?}", row_changeset); - match self.block_manager.update_row(row_changeset).await { - Ok(_) => {} - Err(e) => { - tracing::error!("Apply row changeset error:{:?}", e); + + let cell_changesets = row_changeset + .cell_by_field_id + .into_iter() + .map(|(field_id, cell_rev)| CellChangesetPB { + grid_id: view_id.clone(), + row_id: row_changeset.row_id.clone(), + field_id, + content: cell_rev.data, + }) + .collect::>(); + + for cell_changeset in cell_changesets { + match self.block_manager.update_cell(cell_changeset).await { + Ok(_) => {} + Err(e) => tracing::error!("Apply cell changeset error:{:?}", e), } } } From 778b55d44e75fe14bc56a8fbe25a16f2857f3e5d Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Wed, 24 Aug 2022 21:08:22 +0800 Subject: [PATCH 031/106] feat: implemnt export editor state --- .../appflowy_editor/example/lib/main.dart | 25 +++++++++++++++++++ .../flutter/generated_plugin_registrant.cc | 4 +++ .../linux/flutter/generated_plugins.cmake | 1 + .../Flutter/GeneratedPluginRegistrant.swift | 4 +++ .../example/macos/Podfile.lock | 12 +++++++++ .../appflowy_editor/example/pubspec.yaml | 1 + .../flutter/generated_plugin_registrant.cc | 3 +++ .../windows/flutter/generated_plugins.cmake | 1 + .../lib/src/document/node.dart | 3 ++- .../lib/src/document/state_tree.dart | 6 +++++ 10 files changed, 59 insertions(+), 1 deletion(-) diff --git a/frontend/app_flowy/packages/appflowy_editor/example/lib/main.dart b/frontend/app_flowy/packages/appflowy_editor/example/lib/main.dart index 539bb74425..e5aae9432d 100644 --- a/frontend/app_flowy/packages/appflowy_editor/example/lib/main.dart +++ b/frontend/app_flowy/packages/appflowy_editor/example/lib/main.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -8,6 +9,7 @@ import 'expandable_floating_action_button.dart'; import 'plugin/youtube_link_node_widget.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:path_provider/path_provider.dart'; void main() { runApp(const MyApp()); @@ -58,6 +60,7 @@ class MyHomePage extends StatefulWidget { class _MyHomePageState extends State { final editorKey = GlobalKey(); int page = 0; + EditorState? _editorState; @override Widget build(BuildContext context) { @@ -103,6 +106,7 @@ class _MyHomePageState extends State { ..handler = (message) { debugPrint(message); }; + _editorState = editorState; return _buildAppFlowyEditor(editorState); } else { return const Center( @@ -146,6 +150,21 @@ class _MyHomePageState extends State { ); } + void _exportDocument(EditorState editorState) async { + // await FileSaver.instance.saveAs(String name, Uint8List bytes, String ext, MimeType); + final document = editorState.document.toJson(); + debugPrint(document.toString()); + final json = jsonEncode(document); + debugPrint(json); + + final directory = await getTemporaryDirectory(); + final path = directory.path; + debugPrint(path); + + final file = File('$path/temp.json'); + await file.writeAsString(json); + } + Widget _buildExpandableFab() { return ExpandableFab( distance: 112.0, @@ -177,6 +196,12 @@ class _MyHomePageState extends State { }, icon: const Icon(Icons.text_fields), ), + ActionButton( + onPressed: () { + _exportDocument(_editorState!); + }, + icon: const Icon(Icons.print), + ), ], ); } diff --git a/frontend/app_flowy/packages/appflowy_editor/example/linux/flutter/generated_plugin_registrant.cc b/frontend/app_flowy/packages/appflowy_editor/example/linux/flutter/generated_plugin_registrant.cc index 00fd3bc03f..45a19e066d 100644 --- a/frontend/app_flowy/packages/appflowy_editor/example/linux/flutter/generated_plugin_registrant.cc +++ b/frontend/app_flowy/packages/appflowy_editor/example/linux/flutter/generated_plugin_registrant.cc @@ -6,10 +6,14 @@ #include "generated_plugin_registrant.h" +#include #include #include void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) file_saver_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "FileSaverPlugin"); + file_saver_plugin_register_with_registrar(file_saver_registrar); g_autoptr(FlPluginRegistrar) rich_clipboard_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "RichClipboardPlugin"); rich_clipboard_plugin_register_with_registrar(rich_clipboard_linux_registrar); diff --git a/frontend/app_flowy/packages/appflowy_editor/example/linux/flutter/generated_plugins.cmake b/frontend/app_flowy/packages/appflowy_editor/example/linux/flutter/generated_plugins.cmake index 0342e3868a..10140a75b4 100644 --- a/frontend/app_flowy/packages/appflowy_editor/example/linux/flutter/generated_plugins.cmake +++ b/frontend/app_flowy/packages/appflowy_editor/example/linux/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + file_saver rich_clipboard_linux url_launcher_linux ) diff --git a/frontend/app_flowy/packages/appflowy_editor/example/macos/Flutter/GeneratedPluginRegistrant.swift b/frontend/app_flowy/packages/appflowy_editor/example/macos/Flutter/GeneratedPluginRegistrant.swift index cc167443dc..ac60ac6ec9 100644 --- a/frontend/app_flowy/packages/appflowy_editor/example/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/frontend/app_flowy/packages/appflowy_editor/example/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,11 +5,15 @@ import FlutterMacOS import Foundation +import file_saver +import path_provider_macos import rich_clipboard_macos import url_launcher_macos import wakelock_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + FileSaverPlugin.register(with: registry.registrar(forPlugin: "FileSaverPlugin")) + PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) RichClipboardPlugin.register(with: registry.registrar(forPlugin: "RichClipboardPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) WakelockMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockMacosPlugin")) diff --git a/frontend/app_flowy/packages/appflowy_editor/example/macos/Podfile.lock b/frontend/app_flowy/packages/appflowy_editor/example/macos/Podfile.lock index d3a1dd3611..c56e297a68 100644 --- a/frontend/app_flowy/packages/appflowy_editor/example/macos/Podfile.lock +++ b/frontend/app_flowy/packages/appflowy_editor/example/macos/Podfile.lock @@ -1,5 +1,9 @@ PODS: + - file_saver (0.0.1): + - FlutterMacOS - FlutterMacOS (1.0.0) + - path_provider_macos (0.0.1): + - FlutterMacOS - rich_clipboard_macos (0.0.1): - FlutterMacOS - url_launcher_macos (0.0.1): @@ -8,14 +12,20 @@ PODS: - FlutterMacOS DEPENDENCIES: + - file_saver (from `Flutter/ephemeral/.symlinks/plugins/file_saver/macos`) - FlutterMacOS (from `Flutter/ephemeral`) + - path_provider_macos (from `Flutter/ephemeral/.symlinks/plugins/path_provider_macos/macos`) - rich_clipboard_macos (from `Flutter/ephemeral/.symlinks/plugins/rich_clipboard_macos/macos`) - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) - wakelock_macos (from `Flutter/ephemeral/.symlinks/plugins/wakelock_macos/macos`) EXTERNAL SOURCES: + file_saver: + :path: Flutter/ephemeral/.symlinks/plugins/file_saver/macos FlutterMacOS: :path: Flutter/ephemeral + path_provider_macos: + :path: Flutter/ephemeral/.symlinks/plugins/path_provider_macos/macos rich_clipboard_macos: :path: Flutter/ephemeral/.symlinks/plugins/rich_clipboard_macos/macos url_launcher_macos: @@ -24,7 +34,9 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/wakelock_macos/macos SPEC CHECKSUMS: + file_saver: 44e6fbf666677faf097302460e214e977fdd977b FlutterMacOS: 57701585bf7de1b3fc2bb61f6378d73bbdea8424 + path_provider_macos: 3c0c3b4b0d4a76d2bf989a913c2de869c5641a19 rich_clipboard_macos: 43364b66b9dc69d203eb8dd6d758e2d12e02723c url_launcher_macos: 597e05b8e514239626bcf4a850fcf9ef5c856ec3 wakelock_macos: bc3f2a9bd8d2e6c89fee1e1822e7ddac3bd004a9 diff --git a/frontend/app_flowy/packages/appflowy_editor/example/pubspec.yaml b/frontend/app_flowy/packages/appflowy_editor/example/pubspec.yaml index 4e2e2d68ab..593d9ff45f 100644 --- a/frontend/app_flowy/packages/appflowy_editor/example/pubspec.yaml +++ b/frontend/app_flowy/packages/appflowy_editor/example/pubspec.yaml @@ -40,6 +40,7 @@ dependencies: video_player: ^2.4.5 pod_player: 0.0.8 flutter_inappwebview: ^5.4.3+7 + file_saver: ^0.1.1 dev_dependencies: flutter_test: diff --git a/frontend/app_flowy/packages/appflowy_editor/example/windows/flutter/generated_plugin_registrant.cc b/frontend/app_flowy/packages/appflowy_editor/example/windows/flutter/generated_plugin_registrant.cc index 4f7884874d..391ef56268 100644 --- a/frontend/app_flowy/packages/appflowy_editor/example/windows/flutter/generated_plugin_registrant.cc +++ b/frontend/app_flowy/packages/appflowy_editor/example/windows/flutter/generated_plugin_registrant.cc @@ -6,9 +6,12 @@ #include "generated_plugin_registrant.h" +#include #include void RegisterPlugins(flutter::PluginRegistry* registry) { + FileSaverPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FileSaverPlugin")); UrlLauncherWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("UrlLauncherWindows")); } diff --git a/frontend/app_flowy/packages/appflowy_editor/example/windows/flutter/generated_plugins.cmake b/frontend/app_flowy/packages/appflowy_editor/example/windows/flutter/generated_plugins.cmake index 88b22e5c77..047111654d 100644 --- a/frontend/app_flowy/packages/appflowy_editor/example/windows/flutter/generated_plugins.cmake +++ b/frontend/app_flowy/packages/appflowy_editor/example/windows/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + file_saver url_launcher_windows ) diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/document/node.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/document/node.dart index a0c4e33a70..909e7dd494 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/document/node.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/document/node.dart @@ -163,7 +163,8 @@ class Node extends ChangeNotifier with LinkedListEntry { 'type': type, }; if (children.isNotEmpty) { - map['children'] = children.map((node) => node.toJson()); + map['children'] = + (children.map((node) => node.toJson())).toList(growable: false); } if (_attributes.isNotEmpty) { map['attributes'] = _attributes; diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/document/state_tree.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/document/state_tree.dart index 5bf49c0048..a17b2fbf98 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/document/state_tree.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/document/state_tree.dart @@ -33,6 +33,12 @@ class StateTree { return StateTree(root: root); } + Map toJson() { + return { + 'document': root.toJson(), + }; + } + Node? nodeAtPath(Path path) { return root.childAtPath(path); } From 8a8791b880ad1ad57213b878feb972ad30ea873a Mon Sep 17 00:00:00 2001 From: appflowy Date: Wed, 24 Aug 2022 23:08:57 +0800 Subject: [PATCH 032/106] chore: adjust ui --- .../card/board_checkbox_cell.dart | 19 +++++++++----- .../presentation/card/board_date_cell.dart | 15 ++++++++--- .../presentation/card/board_number_cell.dart | 16 ++++++++---- .../card/board_select_option_cell.dart | 13 +++++++--- .../presentation/card/board_text_cell.dart | 11 ++++---- .../presentation/card/board_url_cell.dart | 26 ++++++++++++------- .../plugins/board/presentation/card/card.dart | 2 +- .../board/presentation/card/define.dart | 3 +++ .../cell/cell_service/context_builder.dart | 1 - .../example/lib/multi_board_list_example.dart | 4 +-- 10 files changed, 73 insertions(+), 37 deletions(-) create mode 100644 frontend/app_flowy/lib/plugins/board/presentation/card/define.dart diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/board_checkbox_cell.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/board_checkbox_cell.dart index c816964d3c..25aacb5678 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/board_checkbox_cell.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/board_checkbox_cell.dart @@ -5,6 +5,8 @@ import 'package:flowy_infra_ui/style_widget/icon_button.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'define.dart'; + class BoardCheckboxCell extends StatefulWidget { final GridCellControllerBuilder cellControllerBuilder; @@ -38,12 +40,17 @@ class _BoardCheckboxCellState extends State { final icon = state.isSelected ? svgWidget('editor/editor_check') : svgWidget('editor/editor_uncheck'); - return Align( - alignment: Alignment.centerLeft, - child: FlowyIconButton( - iconPadding: EdgeInsets.zero, - icon: icon, - width: 20, + return Padding( + padding: EdgeInsets.symmetric( + vertical: BoardSizes.cardCellVPading, + ), + child: Align( + alignment: Alignment.centerLeft, + child: FlowyIconButton( + iconPadding: EdgeInsets.zero, + icon: icon, + width: 20, + ), ), ); }, diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/board_date_cell.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/board_date_cell.dart index 4a52d82116..7c7386696d 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/board_date_cell.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/board_date_cell.dart @@ -1,9 +1,12 @@ import 'package:app_flowy/plugins/board/application/card/board_date_cell_bloc.dart'; import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart'; +import 'package:flowy_infra/theme.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'define.dart'; + class BoardDateCell extends StatefulWidget { final GridCellControllerBuilder cellControllerBuilder; @@ -40,9 +43,15 @@ class _BoardDateCellState extends State { } else { return Align( alignment: Alignment.centerLeft, - child: FlowyText.regular( - state.dateStr, - fontSize: 14, + child: Padding( + padding: EdgeInsets.symmetric( + vertical: BoardSizes.cardCellVPading, + ), + child: FlowyText.regular( + state.dateStr, + fontSize: 13, + color: context.read().shader3, + ), ), ); } diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/board_number_cell.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/board_number_cell.dart index 096592583e..965c06e643 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/board_number_cell.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/board_number_cell.dart @@ -4,6 +4,8 @@ import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'define.dart'; + class BoardNumberCell extends StatefulWidget { final GridCellControllerBuilder cellControllerBuilder; @@ -38,11 +40,15 @@ class _BoardNumberCellState extends State { if (state.content.isEmpty) { return const SizedBox(); } else { - return Align( - alignment: Alignment.centerLeft, - child: FlowyText.regular( - state.content, - fontSize: 14, + return Padding( + padding: + EdgeInsets.symmetric(vertical: BoardSizes.cardCellVPading), + child: Align( + alignment: Alignment.centerLeft, + child: FlowyText.medium( + state.content, + fontSize: 14, + ), ), ); } diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/board_select_option_cell.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/board_select_option_cell.dart index a51a36f99f..9865875e44 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/board_select_option_cell.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/board_select_option_cell.dart @@ -4,6 +4,8 @@ import 'package:app_flowy/plugins/grid/presentation/widgets/cell/select_option_c import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'define.dart'; + class BoardSelectOptionCell extends StatefulWidget { final GridCellControllerBuilder cellControllerBuilder; @@ -42,10 +44,13 @@ class _BoardSelectOptionCellState extends State { ), ) .toList(); - return Align( - alignment: Alignment.centerLeft, - child: AbsorbPointer( - child: Wrap(children: children, spacing: 4, runSpacing: 2), + return Padding( + padding: EdgeInsets.symmetric(vertical: BoardSizes.cardCellVPading), + child: Align( + alignment: Alignment.centerLeft, + child: AbsorbPointer( + child: Wrap(children: children, spacing: 4, runSpacing: 2), + ), ), ); }, diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/board_text_cell.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/board_text_cell.dart index 2da156ded8..89b193edc5 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/board_text_cell.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/board_text_cell.dart @@ -4,6 +4,8 @@ import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'define.dart'; + class BoardTextCell extends StatefulWidget { final GridCellControllerBuilder cellControllerBuilder; const BoardTextCell({required this.cellControllerBuilder, Key? key}) @@ -37,11 +39,10 @@ class _BoardTextCellState extends State { } else { return Align( alignment: Alignment.centerLeft, - child: ConstrainedBox( - constraints: BoxConstraints.loose( - const Size(double.infinity, 100), - ), - child: FlowyText.regular( + child: Padding( + padding: + EdgeInsets.symmetric(vertical: BoardSizes.cardCellVPading), + child: FlowyText.medium( state.content, fontSize: 14, ), diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/board_url_cell.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/board_url_cell.dart index 31cca41e6a..5940e71788 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/board_url_cell.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/board_url_cell.dart @@ -4,6 +4,8 @@ import 'package:flowy_infra/theme.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'define.dart'; + class BoardUrlCell extends StatefulWidget { final GridCellControllerBuilder cellControllerBuilder; @@ -38,16 +40,20 @@ class _BoardUrlCellState extends State { if (state.content.isEmpty) { return const SizedBox(); } else { - return Align( - alignment: Alignment.centerLeft, - child: RichText( - textAlign: TextAlign.left, - text: TextSpan( - text: state.content, - style: TextStyle( - color: theme.main2, - fontSize: 14, - decoration: TextDecoration.underline, + return Padding( + padding: + EdgeInsets.symmetric(vertical: BoardSizes.cardCellVPading), + child: Align( + alignment: Alignment.centerLeft, + child: RichText( + textAlign: TextAlign.left, + text: TextSpan( + text: state.content, + style: TextStyle( + color: theme.main2, + fontSize: 14, + decoration: TextDecoration.underline, + ), ), ), ), diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart index 226a9e8241..ac65d5e5f7 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart @@ -73,7 +73,7 @@ class _BoardCardState extends State { (cellId) { final child = widget.cellBuilder.buildCell(cellId); return Padding( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 5), + padding: const EdgeInsets.symmetric(horizontal: 6), child: child, ); }, diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/define.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/define.dart new file mode 100644 index 0000000000..ced0fefb83 --- /dev/null +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/define.dart @@ -0,0 +1,3 @@ +class BoardSizes { + static double get cardCellVPading => 4; +} diff --git a/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/context_builder.dart b/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/context_builder.dart index f3ed73b449..95c708e0e2 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/context_builder.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/context_builder.dart @@ -279,7 +279,6 @@ class IGridCellController extends Equatable { _loadDataOperation?.cancel(); _loadDataOperation = Timer(const Duration(milliseconds: 10), () { _cellDataLoader.loadData().then((data) { - Log.debug('$fieldId CellData: Did Get cell data'); _cellsCache.insert(_cacheKey, GridCell(object: data)); _cellDataNotifier?.value = data; }); diff --git a/frontend/app_flowy/packages/appflowy_board/example/lib/multi_board_list_example.dart b/frontend/app_flowy/packages/appflowy_board/example/lib/multi_board_list_example.dart index 5df7ce54ff..854936ea29 100644 --- a/frontend/app_flowy/packages/appflowy_board/example/lib/multi_board_list_example.dart +++ b/frontend/app_flowy/packages/appflowy_board/example/lib/multi_board_list_example.dart @@ -114,7 +114,7 @@ class _MultiBoardListExampleState extends State { return Align( alignment: Alignment.centerLeft, child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 40), + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 60), child: Text(item.s), ), ); @@ -124,7 +124,7 @@ class _MultiBoardListExampleState extends State { return Align( alignment: Alignment.centerLeft, child: Padding( - padding: const EdgeInsets.all(20), + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 60), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ From 50e772edff961ad5f07bc81ac2ad5dd85e67de31 Mon Sep 17 00:00:00 2001 From: appflowy Date: Thu, 25 Aug 2022 09:46:26 +0800 Subject: [PATCH 033/106] chore: adjust header, card padding ui --- .../board/presentation/board_page.dart | 57 ++++++++++++++++--- .../plugins/board/presentation/card/card.dart | 2 +- .../presentation/card/card_container.dart | 2 +- .../appflowy_board/lib/src/widgets/board.dart | 8 ++- .../lib/src/widgets/styled_widgets/card.dart | 16 +++--- .../src/widgets/styled_widgets/footer.dart | 5 +- .../src/widgets/styled_widgets/header.dart | 20 +++++-- .../flowy_infra_ui/lib/style_widget/text.dart | 21 +++++-- .../flowy-grid/tests/grid/group_test/test.rs | 2 +- 9 files changed, 100 insertions(+), 33 deletions(-) diff --git a/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart b/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart index eb47e8c134..26c44e52bf 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart @@ -9,6 +9,9 @@ import 'package:app_flowy/plugins/grid/application/row/row_data_controller.dart' import 'package:app_flowy/plugins/grid/presentation/widgets/cell/cell_builder.dart'; import 'package:app_flowy/plugins/grid/presentation/widgets/row/row_detail.dart'; import 'package:appflowy_board/appflowy_board.dart'; +import 'package:flowy_infra/image.dart'; +import 'package:flowy_infra/theme.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flowy_infra_ui/widget/error_page.dart'; import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/block_entities.pb.dart'; @@ -81,21 +84,47 @@ class BoardContent extends StatelessWidget { Widget _buildHeader( BuildContext context, AFBoardColumnHeaderData headerData) { return AppFlowyColumnHeader( - icon: const Icon(Icons.lightbulb_circle), - title: Text(headerData.columnName), - addIcon: const Icon(Icons.add, size: 20), - moreIcon: const Icon(Icons.more_horiz, size: 20), + // icon: const Icon(Icons.lightbulb_circle), + title: Flexible( + fit: FlexFit.tight, + child: FlowyText.medium( + headerData.columnName, + fontSize: 14, + overflow: TextOverflow.clip, + color: context.read().textColor, + ), + ), + // addIcon: const Icon(Icons.add, size: 20), + moreIcon: SizedBox( + width: 20, + height: 20, + child: svgWidget( + 'grid/details', + color: context.read().iconColor, + ), + ), height: 50, - margin: config.columnItemPadding, + margin: config.headerPadding, ); } Widget _buildFooter(BuildContext context, AFBoardColumnData columnData) { return AppFlowyColumnFooter( - icon: const Icon(Icons.add, size: 20), - title: const Text('New'), + icon: SizedBox( + height: 20, + width: 20, + child: svgWidget( + "home/add", + color: context.read().iconColor, + ), + ), + title: FlowyText.medium( + "New", + fontSize: 14, + color: context.read().textColor, + ), height: 50, - margin: config.columnItemPadding, + margin: config.footerPadding, onAddButtonClick: () { context.read().add(BoardEvent.createRow(columnData.id)); }); @@ -124,6 +153,8 @@ class BoardContent extends StatelessWidget { return AppFlowyColumnItemCard( key: ObjectKey(item), + margin: config.cardPadding, + decoration: _makeBoxDecoration(context), child: BoardCard( gridId: gridId, isEditing: isEditing, @@ -143,6 +174,16 @@ class BoardContent extends StatelessWidget { ); } + BoxDecoration _makeBoxDecoration(BuildContext context) { + final theme = context.read(); + final borderSide = BorderSide(color: theme.shader6, width: 1.0); + return BoxDecoration( + color: theme.surface, + border: Border.fromBorderSide(borderSide), + borderRadius: const BorderRadius.all(Radius.circular(6)), + ); + } + void _openCard(String gridId, GridFieldCache fieldCache, RowPB rowPB, GridRowCache rowCache, BuildContext context) { final rowInfo = RowInfo( diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart index ac65d5e5f7..7966f9e212 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart @@ -92,7 +92,7 @@ class _CardMoreOption extends StatelessWidget with CardAccessory { @override Widget build(BuildContext context) { - return svgWidget('home/details', color: context.read().iconColor); + return svgWidget('grid/details', color: context.read().iconColor); } @override diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/card_container.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/card_container.dart index abca27e5c5..4b8c0548d5 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/card_container.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/card_container.dart @@ -116,7 +116,7 @@ class _CardEnterRegion extends StatelessWidget { .onEnter = false, child: IntrinsicHeight( child: Stack( - alignment: AlignmentDirectional.center, + alignment: AlignmentDirectional.topEnd, fit: StackFit.expand, children: children, )), diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board.dart index dbcf62671a..dc7ae1c02f 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board.dart @@ -12,12 +12,18 @@ class AFBoardConfig { final double cornerRadius; final EdgeInsets columnPadding; final EdgeInsets columnItemPadding; + final EdgeInsets footerPadding; + final EdgeInsets headerPadding; + final EdgeInsets cardPadding; final Color columnBackgroundColor; const AFBoardConfig({ this.cornerRadius = 6.0, this.columnPadding = const EdgeInsets.symmetric(horizontal: 8), - this.columnItemPadding = const EdgeInsets.symmetric(horizontal: 10), + this.columnItemPadding = const EdgeInsets.symmetric(horizontal: 12), + this.footerPadding = const EdgeInsets.symmetric(horizontal: 12), + this.headerPadding = const EdgeInsets.symmetric(horizontal: 16), + this.cardPadding = const EdgeInsets.symmetric(horizontal: 3, vertical: 4), this.columnBackgroundColor = Colors.transparent, }); } diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/styled_widgets/card.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/styled_widgets/card.dart index 323964c75f..77cfc1cb13 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/styled_widgets/card.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/styled_widgets/card.dart @@ -2,16 +2,17 @@ import 'package:flutter/material.dart'; class AppFlowyColumnItemCard extends StatefulWidget { final Widget? child; - final Color backgroundColor; - final double cornerRadius; final EdgeInsets margin; final BoxConstraints boxConstraints; + final BoxDecoration decoration; const AppFlowyColumnItemCard({ this.child, - this.cornerRadius = 0.0, this.margin = const EdgeInsets.all(4), - this.backgroundColor = Colors.white, + this.decoration = const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.zero, + ), this.boxConstraints = const BoxConstraints(minHeight: 40), Key? key, }) : super(key: key); @@ -24,14 +25,11 @@ class _AppFlowyColumnItemCardState extends State { @override Widget build(BuildContext context) { return Padding( - padding: const EdgeInsets.all(4), + padding: widget.margin, child: Container( clipBehavior: Clip.hardEdge, constraints: widget.boxConstraints, - decoration: BoxDecoration( - color: widget.backgroundColor, - borderRadius: BorderRadius.circular(widget.cornerRadius), - ), + decoration: widget.decoration, child: widget.child, ), ); diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/styled_widgets/footer.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/styled_widgets/footer.dart index 7f5655fe60..c877e4fe4d 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/styled_widgets/footer.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/styled_widgets/footer.dart @@ -12,7 +12,7 @@ class AppFlowyColumnFooter extends StatefulWidget { const AppFlowyColumnFooter({ this.icon, this.title, - this.margin = EdgeInsets.zero, + this.margin = const EdgeInsets.symmetric(horizontal: 12), required this.height, this.onAddButtonClick, Key? key, @@ -30,12 +30,13 @@ class _AppFlowyColumnFooterState extends State { child: SizedBox( height: widget.height, child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 10), + padding: widget.margin, child: Row( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.center, children: [ if (widget.icon != null) widget.icon!, + const SizedBox(width: 8), if (widget.title != null) widget.title!, ], ), diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/styled_widgets/header.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/styled_widgets/header.dart index fdebc7ef21..88f52c9134 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/styled_widgets/header.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/styled_widgets/header.dart @@ -45,15 +45,25 @@ class _AppFlowyColumnHeaderState extends State { } if (widget.moreIcon != null) { - children.add(const Spacer()); + // children.add(const Spacer()); children.add( - IconButton(onPressed: widget.onMoreButtonClick, icon: widget.moreIcon!), + IconButton( + onPressed: widget.onMoreButtonClick, + icon: widget.moreIcon!, + padding: const EdgeInsets.all(4), + constraints: const BoxConstraints(), + ), ); } if (widget.addIcon != null) { children.add( - IconButton(onPressed: widget.onAddButtonClick, icon: widget.addIcon!), + IconButton( + onPressed: widget.onAddButtonClick, + icon: widget.addIcon!, + padding: const EdgeInsets.all(4), + constraints: const BoxConstraints(), + ), ); } @@ -61,9 +71,7 @@ class _AppFlowyColumnHeaderState extends State { height: widget.height, child: Padding( padding: widget.margin, - child: Row( - children: children, - ), + child: Row(children: children), ), ); } diff --git a/frontend/app_flowy/packages/flowy_infra_ui/lib/style_widget/text.dart b/frontend/app_flowy/packages/flowy_infra_ui/lib/style_widget/text.dart index 74cf7e4c31..bad452cbe4 100644 --- a/frontend/app_flowy/packages/flowy_infra_ui/lib/style_widget/text.dart +++ b/frontend/app_flowy/packages/flowy_infra_ui/lib/style_widget/text.dart @@ -20,18 +20,31 @@ class FlowyText extends StatelessWidget { }) : super(key: key); const FlowyText.semibold(this.title, - {Key? key, this.fontSize = 16, TextOverflow? overflow, this.color, this.textAlign}) + {Key? key, + this.fontSize = 16, + TextOverflow? overflow, + this.color, + this.textAlign}) : fontWeight = FontWeight.w600, overflow = overflow ?? TextOverflow.ellipsis, super(key: key); - const FlowyText.medium(this.title, {Key? key, this.fontSize = 16, TextOverflow? overflow, this.color, this.textAlign}) + const FlowyText.medium(this.title, + {Key? key, + this.fontSize = 16, + TextOverflow? overflow, + this.color, + this.textAlign}) : fontWeight = FontWeight.w500, overflow = overflow ?? TextOverflow.ellipsis, super(key: key); const FlowyText.regular(this.title, - {Key? key, this.fontSize = 16, TextOverflow? overflow, this.color, this.textAlign}) + {Key? key, + this.fontSize = 16, + TextOverflow? overflow, + this.color, + this.textAlign}) : fontWeight = FontWeight.w400, overflow = overflow ?? TextOverflow.ellipsis, super(key: key); @@ -40,9 +53,9 @@ class FlowyText extends StatelessWidget { Widget build(BuildContext context) { final theme = context.watch(); return Text(title, - overflow: overflow, softWrap: false, textAlign: textAlign, + overflow: overflow, style: TextStyle( color: color ?? theme.textColor, fontWeight: fontWeight, diff --git a/frontend/rust-lib/flowy-grid/tests/grid/group_test/test.rs b/frontend/rust-lib/flowy-grid/tests/grid/group_test/test.rs index d6a9839f77..4a4d45f952 100644 --- a/frontend/rust-lib/flowy-grid/tests/grid/group_test/test.rs +++ b/frontend/rust-lib/flowy-grid/tests/grid/group_test/test.rs @@ -319,7 +319,7 @@ async fn group_move_group_test() { #[tokio::test] async fn group_update_field_test() { let mut test = GridGroupTest::new().await; - let mut group = test.group_at_index(0).await; + let group = test.group_at_index(0).await; let changeset = FieldChangesetParams { field_id: group.field_id.clone(), grid_id: test.grid_id.clone(), From befc40ba71b3096aa57a2ba337ff251592d40dbb Mon Sep 17 00:00:00 2001 From: appflowy Date: Thu, 25 Aug 2022 10:00:54 +0800 Subject: [PATCH 034/106] fix: clip the text when out of bound --- .../widgets/cell/select_option_cell/extension.dart | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/extension.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/extension.dart index 6fdd8bf6f8..6b937a3c9b 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/extension.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/extension.dart @@ -91,8 +91,13 @@ class SelectOptionTag extends StatelessWidget { Widget build(BuildContext context) { return ChoiceChip( pressElevation: 1, - label: - FlowyText.medium(name, fontSize: 12, overflow: TextOverflow.ellipsis), + label: Flexible( + child: FlowyText.medium( + name, + fontSize: 12, + overflow: TextOverflow.clip, + ), + ), selectedColor: color, backgroundColor: color, labelPadding: const EdgeInsets.symmetric(horizontal: 6), From 311c6ae94afbe941c1e68bd61a00465b0e608acf Mon Sep 17 00:00:00 2001 From: appflowy Date: Thu, 25 Aug 2022 11:12:58 +0800 Subject: [PATCH 035/106] chore: hide more button on card title --- .../plugins/board/presentation/board_page.dart | 17 ++++++++--------- .../presentation/card/board_checkbox_cell.dart | 4 +--- .../presentation/card/board_date_cell.dart | 2 +- .../presentation/card/board_number_cell.dart | 2 +- .../card/board_select_option_cell.dart | 3 ++- .../presentation/card/board_text_cell.dart | 2 +- .../presentation/card/board_url_cell.dart | 2 +- .../presentation/card/card_container.dart | 18 ++++++++++++++++++ .../board/presentation/card/define.dart | 2 +- .../cell/select_option_cell/extension.dart | 10 ++++------ 10 files changed, 38 insertions(+), 24 deletions(-) diff --git a/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart b/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart index 26c44e52bf..3d109309d7 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart @@ -84,7 +84,6 @@ class BoardContent extends StatelessWidget { Widget _buildHeader( BuildContext context, AFBoardColumnHeaderData headerData) { return AppFlowyColumnHeader( - // icon: const Icon(Icons.lightbulb_circle), title: Flexible( fit: FlexFit.tight, child: FlowyText.medium( @@ -95,14 +94,14 @@ class BoardContent extends StatelessWidget { ), ), // addIcon: const Icon(Icons.add, size: 20), - moreIcon: SizedBox( - width: 20, - height: 20, - child: svgWidget( - 'grid/details', - color: context.read().iconColor, - ), - ), + // moreIcon: SizedBox( + // width: 20, + // height: 20, + // child: svgWidget( + // 'grid/details', + // color: context.read().iconColor, + // ), + // ), height: 50, margin: config.headerPadding, ); diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/board_checkbox_cell.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/board_checkbox_cell.dart index 25aacb5678..8578f956d6 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/board_checkbox_cell.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/board_checkbox_cell.dart @@ -41,9 +41,7 @@ class _BoardCheckboxCellState extends State { ? svgWidget('editor/editor_check') : svgWidget('editor/editor_uncheck'); return Padding( - padding: EdgeInsets.symmetric( - vertical: BoardSizes.cardCellVPading, - ), + padding: EdgeInsets.zero, child: Align( alignment: Alignment.centerLeft, child: FlowyIconButton( diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/board_date_cell.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/board_date_cell.dart index 7c7386696d..8896a32732 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/board_date_cell.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/board_date_cell.dart @@ -45,7 +45,7 @@ class _BoardDateCellState extends State { alignment: Alignment.centerLeft, child: Padding( padding: EdgeInsets.symmetric( - vertical: BoardSizes.cardCellVPading, + vertical: BoardSizes.cardCellVPadding, ), child: FlowyText.regular( state.dateStr, diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/board_number_cell.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/board_number_cell.dart index 965c06e643..33fb4a9b4b 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/board_number_cell.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/board_number_cell.dart @@ -42,7 +42,7 @@ class _BoardNumberCellState extends State { } else { return Padding( padding: - EdgeInsets.symmetric(vertical: BoardSizes.cardCellVPading), + EdgeInsets.symmetric(vertical: BoardSizes.cardCellVPadding), child: Align( alignment: Alignment.centerLeft, child: FlowyText.medium( diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/board_select_option_cell.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/board_select_option_cell.dart index 9865875e44..6cba08c636 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/board_select_option_cell.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/board_select_option_cell.dart @@ -45,7 +45,8 @@ class _BoardSelectOptionCellState extends State { ) .toList(); return Padding( - padding: EdgeInsets.symmetric(vertical: BoardSizes.cardCellVPading), + padding: + EdgeInsets.symmetric(vertical: BoardSizes.cardCellVPadding), child: Align( alignment: Alignment.centerLeft, child: AbsorbPointer( diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/board_text_cell.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/board_text_cell.dart index 89b193edc5..6f6e35429a 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/board_text_cell.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/board_text_cell.dart @@ -41,7 +41,7 @@ class _BoardTextCellState extends State { alignment: Alignment.centerLeft, child: Padding( padding: - EdgeInsets.symmetric(vertical: BoardSizes.cardCellVPading), + EdgeInsets.symmetric(vertical: BoardSizes.cardCellVPadding), child: FlowyText.medium( state.content, fontSize: 14, diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/board_url_cell.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/board_url_cell.dart index 5940e71788..f687e7efeb 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/board_url_cell.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/board_url_cell.dart @@ -42,7 +42,7 @@ class _BoardUrlCellState extends State { } else { return Padding( padding: - EdgeInsets.symmetric(vertical: BoardSizes.cardCellVPading), + EdgeInsets.symmetric(vertical: BoardSizes.cardCellVPadding), child: Align( alignment: Alignment.centerLeft, child: RichText( diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/card_container.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/card_container.dart index 4b8c0548d5..0e0a7287ae 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/card_container.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/card_container.dart @@ -74,6 +74,7 @@ class CardAccessoryContainer extends StatelessWidget { width: 26, height: 26, padding: const EdgeInsets.all(3), + decoration: _makeBoxDecoration(context), child: accessory, ), ); @@ -88,6 +89,23 @@ class CardAccessoryContainer extends StatelessWidget { } } +BoxDecoration _makeBoxDecoration(BuildContext context) { + final theme = context.read(); + final borderSide = BorderSide(color: theme.shader6, width: 1.0); + return BoxDecoration( + color: theme.surface, + border: Border.fromBorderSide(borderSide), + boxShadow: [ + BoxShadow( + color: theme.shader6, + spreadRadius: 0, + blurRadius: 2, + offset: Offset.zero) + ], + borderRadius: const BorderRadius.all(Radius.circular(6)), + ); +} + class _CardEnterRegion extends StatelessWidget { final Widget child; final List accessories; diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/define.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/define.dart index ced0fefb83..c2cff2ee0f 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/define.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/define.dart @@ -1,3 +1,3 @@ class BoardSizes { - static double get cardCellVPading => 4; + static double get cardCellVPadding => 4; } diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/extension.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/extension.dart index 6b937a3c9b..f045984e66 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/extension.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/extension.dart @@ -91,12 +91,10 @@ class SelectOptionTag extends StatelessWidget { Widget build(BuildContext context) { return ChoiceChip( pressElevation: 1, - label: Flexible( - child: FlowyText.medium( - name, - fontSize: 12, - overflow: TextOverflow.clip, - ), + label: FlowyText.medium( + name, + fontSize: 12, + overflow: TextOverflow.clip, ), selectedColor: color, backgroundColor: color, From c827f9b15645d6790e4befbe449434760301a882 Mon Sep 17 00:00:00 2001 From: appflowy Date: Thu, 25 Aug 2022 12:36:56 +0800 Subject: [PATCH 036/106] chore: fix card refresh issue --- .../app_flowy/lib/plugins/board/application/board_bloc.dart | 5 +---- .../application/card/board_select_option_cell_bloc.dart | 1 - .../grid/application/cell/cell_service/context_builder.dart | 5 ++++- frontend/rust-lib/flowy-grid/src/event_handler.rs | 2 +- 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart b/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart index 422f1dff2b..7c04dec814 100644 --- a/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart +++ b/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart @@ -317,9 +317,6 @@ class GroupControllerDelegateImpl extends GroupControllerDelegate { @override void updateRow(String groupId, RowPB row) { - // workaround: fix the board card reload timing issue. - Future.delayed(const Duration(milliseconds: 300), () { - controller.updateColumnItem(groupId, BoardColumnItem(row: row)); - }); + controller.updateColumnItem(groupId, BoardColumnItem(row: row)); } } diff --git a/frontend/app_flowy/lib/plugins/board/application/card/board_select_option_cell_bloc.dart b/frontend/app_flowy/lib/plugins/board/application/card/board_select_option_cell_bloc.dart index df36033cfa..1b70710a35 100644 --- a/frontend/app_flowy/lib/plugins/board/application/card/board_select_option_cell_bloc.dart +++ b/frontend/app_flowy/lib/plugins/board/application/card/board_select_option_cell_bloc.dart @@ -68,7 +68,6 @@ class BoardSelectOptionCellState with _$BoardSelectOptionCellState { factory BoardSelectOptionCellState.initial( GridSelectOptionCellController context) { final data = context.getCellData(); - return BoardSelectOptionCellState( selectedOptions: data?.selectOptions ?? [], ); diff --git a/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/context_builder.dart b/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/context_builder.dart index 95c708e0e2..8ab486a48c 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/context_builder.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/context_builder.dart @@ -190,7 +190,10 @@ class IGridCellController extends Equatable { /// cell display: $12 _cellListener?.start(onCellChanged: (result) { result.fold( - (_) => _loadData(), + (_) { + _cellsCache.remove(fieldId); + _loadData(); + }, (err) => Log.error(err), ); }); diff --git a/frontend/rust-lib/flowy-grid/src/event_handler.rs b/frontend/rust-lib/flowy-grid/src/event_handler.rs index a9060aa87e..4b525c233f 100644 --- a/frontend/rust-lib/flowy-grid/src/event_handler.rs +++ b/frontend/rust-lib/flowy-grid/src/event_handler.rs @@ -269,7 +269,7 @@ pub(crate) async fn create_table_row_handler( data_result(row) } -// #[tracing::instrument(level = "debug", skip_all, err)] +#[tracing::instrument(level = "trace", skip_all, err)] pub(crate) async fn get_cell_handler( data: Data, manager: AppData>, From 14874772b18ee4758e6030585ae0cfc3136bedfb Mon Sep 17 00:00:00 2001 From: appflowy Date: Thu, 25 Aug 2022 13:44:58 +0800 Subject: [PATCH 037/106] chore: insert default group at index 0 --- .../card/board_checkbox_cell.dart | 2 - .../entities/group_entities/configuration.rs | 8 +-- .../src/services/group/configuration.rs | 51 +++++++++++++++---- .../src/services/group/controller.rs | 45 +++++++++++----- .../flowy-grid/src/services/group/entities.rs | 4 ++ .../src/revision/group_rev.rs | 32 ++++++++---- 6 files changed, 104 insertions(+), 38 deletions(-) diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/board_checkbox_cell.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/board_checkbox_cell.dart index 8578f956d6..522a23fe78 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/board_checkbox_cell.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/board_checkbox_cell.dart @@ -5,8 +5,6 @@ import 'package:flowy_infra_ui/style_widget/icon_button.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'define.dart'; - class BoardCheckboxCell extends StatefulWidget { final GridCellControllerBuilder cellControllerBuilder; diff --git a/frontend/rust-lib/flowy-grid/src/entities/group_entities/configuration.rs b/frontend/rust-lib/flowy-grid/src/entities/group_entities/configuration.rs index cb5503727b..19f5b27a9b 100644 --- a/frontend/rust-lib/flowy-grid/src/entities/group_entities/configuration.rs +++ b/frontend/rust-lib/flowy-grid/src/entities/group_entities/configuration.rs @@ -1,5 +1,5 @@ use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; -use flowy_grid_data_model::revision::{GroupRecordRevision, SelectOptionGroupConfigurationRevision}; +use flowy_grid_data_model::revision::{GroupRevision, SelectOptionGroupConfigurationRevision}; #[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] pub struct UrlGroupConfigurationPB { @@ -36,10 +36,10 @@ pub struct GroupRecordPB { visible: bool, } -impl std::convert::From for GroupRecordPB { - fn from(rev: GroupRecordRevision) -> Self { +impl std::convert::From for GroupRecordPB { + fn from(rev: GroupRevision) -> Self { Self { - group_id: rev.group_id, + group_id: rev.id, visible: rev.visible, } } diff --git a/frontend/rust-lib/flowy-grid/src/services/group/configuration.rs b/frontend/rust-lib/flowy-grid/src/services/group/configuration.rs index 5126c9b2fc..546afb8a32 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/configuration.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/configuration.rs @@ -2,10 +2,11 @@ use crate::entities::{GroupPB, GroupViewChangesetPB, InsertedGroupPB}; use crate::services::group::{default_group_configuration, Group}; use flowy_error::{FlowyError, FlowyResult}; use flowy_grid_data_model::revision::{ - FieldRevision, FieldTypeRevision, GroupConfigurationContentSerde, GroupConfigurationRevision, GroupRecordRevision, + FieldRevision, FieldTypeRevision, GroupConfigurationContentSerde, GroupConfigurationRevision, GroupRevision, }; use indexmap::IndexMap; use lib_infra::future::AFFuture; +use std::fmt::Formatter; use std::marker::PhantomData; use std::sync::Arc; @@ -25,6 +26,15 @@ pub trait GroupConfigurationWriter: Send + Sync + 'static { ) -> AFFuture>; } +impl std::fmt::Display for GenericGroupConfiguration { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + self.groups_map.iter().for_each(|(_, group)| { + let _ = f.write_fmt(format_args!("Group:{} has {} rows \n", group.id, group.rows.len())); + }); + Ok(()) + } +} + pub struct GenericGroupConfiguration { view_id: String, pub configuration: Arc, @@ -84,12 +94,31 @@ where let group_revs = groups .iter() - .map(|group| GroupRecordRevision::new(group.id.clone(), group.name.clone())) - .collect(); + .map(|group| GroupRevision::new(group.id.clone(), group.name.clone())) + .collect::>(); self.mut_configuration(move |configuration| { - configuration.groups = group_revs; - true + let mut is_changed = false; + for new_group_rev in group_revs { + match configuration + .groups + .iter() + .position(|group_rev| group_rev.id == new_group_rev.id) + { + None => { + configuration.groups.push(new_group_rev); + is_changed = true; + } + Some(pos) => { + let removed_group = configuration.groups.remove(pos); + if removed_group != new_group_rev { + is_changed = true; + } + configuration.groups.insert(pos, new_group_rev); + } + } + } + is_changed })?; groups.into_iter().for_each(|group| { @@ -139,8 +168,8 @@ where self.groups_map.swap_indices(from_index, to_index); self.mut_configuration(|configuration| { - let from_index = configuration.groups.iter().position(|group| group.group_id == from_id); - let to_index = configuration.groups.iter().position(|group| group.group_id == to_id); + let from_index = configuration.groups.iter().position(|group| group.id == from_id); + let to_index = configuration.groups.iter().position(|group| group.id == to_id); if let (Some(from), Some(to)) = (from_index, to_index) { configuration.groups.swap(from, to); } @@ -183,10 +212,10 @@ where fn mut_configuration_group( &mut self, group_id: &str, - mut_groups_fn: impl Fn(&mut GroupRecordRevision), + mut_groups_fn: impl Fn(&mut GroupRevision), ) -> FlowyResult<()> { self.mut_configuration(|configuration| { - match configuration.groups.iter_mut().find(|group| group.group_id == group_id) { + match configuration.groups.iter_mut().find(|group| group.id == group_id) { None => false, Some(group_rev) => { mut_groups_fn(group_rev); @@ -209,7 +238,7 @@ where } } -fn merge_groups(old_groups: &[GroupRecordRevision], groups: Vec) -> MergeGroupResult { +fn merge_groups(old_groups: &[GroupRevision], groups: Vec) -> MergeGroupResult { let mut merge_result = MergeGroupResult::new(); if old_groups.is_empty() { merge_result.groups = groups; @@ -224,7 +253,7 @@ fn merge_groups(old_groups: &[GroupRecordRevision], groups: Vec) -> Merge // The group is ordered in old groups. Add them before adding the new groups for group_rev in old_groups { - if let Some(group) = group_map.remove(&group_rev.group_id) { + if let Some(group) = group_map.remove(&group_rev.id) { if group.name == group_rev.name { merge_result.add_group(group); } else { diff --git a/frontend/rust-lib/flowy-grid/src/services/group/controller.rs b/frontend/rust-lib/flowy-grid/src/services/group/controller.rs index 1dae47c0db..fac034f6f5 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/controller.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/controller.rs @@ -124,7 +124,11 @@ where } fn groups(&self) -> Vec { - self.configuration.clone_groups() + let mut groups = self.configuration.clone_groups(); + if self.default_group.is_empty() == false { + groups.insert(0, self.default_group.clone()); + } + groups } fn get_group(&self, group_id: &str) -> Option<(usize, Group)> { @@ -132,25 +136,28 @@ where Some((group.0, group.1.clone())) } + #[tracing::instrument(level = "trace", skip_all, fields(row_count=%row_revs.len(), group_result))] fn fill_groups(&mut self, row_revs: &[Arc], field_rev: &FieldRevision) -> FlowyResult> { + // let mut ungrouped_rows = vec![]; for row_rev in row_revs { if let Some(cell_rev) = row_rev.cells.get(&self.field_id) { - let mut group_rows: Vec = vec![]; + let mut grouped_rows: Vec = vec![]; let cell_bytes = decode_any_cell_data(cell_rev.data.clone(), field_rev); let cell_data = cell_bytes.parser::

()?; for group in self.configuration.groups() { if self.can_group(&group.content, &cell_data) { - group_rows.push(GroupRow { + grouped_rows.push(GroupedRow { row: row_rev.into(), group_id: group.id.clone(), }); } } - if group_rows.is_empty() { + if grouped_rows.is_empty() { + // ungrouped_rows.push(RowPB::from(row_rev)); self.default_group.add_row(row_rev.into()); } else { - for group_row in group_rows { + for group_row in grouped_rows { if let Some(group) = self.configuration.get_mut_group(&group_row.group_id) { group.add_row(group_row.row); } @@ -161,13 +168,27 @@ where } } - let default_group = self.default_group.clone(); - let mut groups: Vec = self.configuration.clone_groups(); - if !default_group.number_of_row() == 0 { - groups.push(default_group); - } + // if !ungrouped_rows.is_empty() { + // let default_group_rev = GroupRevision::default_group(gen_grid_group_id(), format!("No {}", field_rev.name)); + // let default_group = Group::new( + // default_group_rev.id.clone(), + // field_rev.id.clone(), + // default_group_rev.name.clone(), + // "".to_owned(), + // ); + // } - Ok(groups) + tracing::Span::current().record( + "group_result", + &format!( + "{}, default_group has {} rows", + self.configuration, + self.default_group.rows.len() + ) + .as_str(), + ); + + Ok(self.groups()) } fn move_group(&mut self, from_group_id: &str, to_group_id: &str) -> FlowyResult<()> { @@ -222,7 +243,7 @@ where } } -struct GroupRow { +struct GroupedRow { row: RowPB, group_id: String, } diff --git a/frontend/rust-lib/flowy-grid/src/services/group/entities.rs b/frontend/rust-lib/flowy-grid/src/services/group/entities.rs index d8c4169eaf..f4d0ee1652 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/entities.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/entities.rs @@ -59,4 +59,8 @@ impl Group { pub fn number_of_row(&self) -> usize { self.rows.len() } + + pub fn is_empty(&self) -> bool { + self.rows.is_empty() + } } diff --git a/shared-lib/flowy-grid-data-model/src/revision/group_rev.rs b/shared-lib/flowy-grid-data-model/src/revision/group_rev.rs index ce378c0ffb..04b571fd76 100644 --- a/shared-lib/flowy-grid-data-model/src/revision/group_rev.rs +++ b/shared-lib/flowy-grid-data-model/src/revision/group_rev.rs @@ -14,7 +14,7 @@ pub struct GroupConfigurationRevision { pub id: String, pub field_id: String, pub field_type_rev: FieldTypeRevision, - pub groups: Vec, + pub groups: Vec, pub content: String, } @@ -106,24 +106,38 @@ impl GroupConfigurationContentSerde for SelectOptionGroupConfigurationRevision { } } -#[derive(Clone, Debug, Default, Serialize, Deserialize)] -pub struct GroupRecordRevision { - pub group_id: String, +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)] +pub struct GroupRevision { + pub id: String, #[serde(default)] pub name: String, - #[serde(default = "DEFAULT_GROUP_RECORD_VISIBILITY")] + #[serde(skip, default = "IS_DEFAULT_GROUP")] + pub is_default: bool, + + #[serde(default = "GROUP_REV_VISIBILITY")] pub visible: bool, } -const DEFAULT_GROUP_RECORD_VISIBILITY: fn() -> bool = || true; +const GROUP_REV_VISIBILITY: fn() -> bool = || true; +const IS_DEFAULT_GROUP: fn() -> bool = || false; -impl GroupRecordRevision { - pub fn new(group_id: String, group_name: String) -> Self { +impl GroupRevision { + pub fn new(id: String, group_name: String) -> Self { Self { - group_id, + id, name: group_name, + is_default: false, + visible: true, + } + } + + pub fn default_group(id: String, group_name: String) -> Self { + Self { + id, + name: group_name, + is_default: true, visible: true, } } From 90db76811c64557f81ec542de667f2d0cb84d4fb Mon Sep 17 00:00:00 2001 From: appflowy Date: Thu, 25 Aug 2022 14:08:49 +0800 Subject: [PATCH 038/106] chore: update board template --- frontend/rust-lib/flowy-grid/src/util.rs | 112 +++++++++++++++++++++++ 1 file changed, 112 insertions(+) diff --git a/frontend/rust-lib/flowy-grid/src/util.rs b/frontend/rust-lib/flowy-grid/src/util.rs index 90bf2f2a26..128c42ae04 100644 --- a/frontend/rust-lib/flowy-grid/src/util.rs +++ b/frontend/rust-lib/flowy-grid/src/util.rs @@ -34,6 +34,118 @@ pub fn make_default_grid() -> BuildGridContext { } pub fn make_default_board() -> BuildGridContext { + let mut grid_builder = GridBuilder::new(); + // text + let text_field = FieldBuilder::new(RichTextTypeOptionBuilder::default()) + .name("Description") + .visibility(true) + .primary(true) + .build(); + let text_field_id = text_field.id.clone(); + grid_builder.add_field(text_field); + + // single select + let to_do_option = SelectOptionPB::with_color("To Do", SelectOptionColorPB::Purple); + let doing_option = SelectOptionPB::with_color("Doing", SelectOptionColorPB::Orange); + let done_option = SelectOptionPB::with_color("Done", SelectOptionColorPB::Yellow); + let single_select_type_option = SingleSelectTypeOptionBuilder::default() + .add_option(to_do_option.clone()) + .add_option(doing_option.clone()) + .add_option(done_option.clone()); + let single_select_field = FieldBuilder::new(single_select_type_option) + .name("Status") + .visibility(true) + .build(); + let single_select_field_id = single_select_field.id.clone(); + grid_builder.add_field(single_select_field); + + // MultiSelect + let work_option = SelectOptionPB::with_color("Work", SelectOptionColorPB::Aqua); + let travel_option = SelectOptionPB::with_color("Travel", SelectOptionColorPB::Green); + let fun_option = SelectOptionPB::with_color("Fun", SelectOptionColorPB::Lime); + let health_option = SelectOptionPB::with_color("Health", SelectOptionColorPB::Pink); + let multi_select_type_option = MultiSelectTypeOptionBuilder::default() + .add_option(travel_option.clone()) + .add_option(work_option.clone()) + .add_option(fun_option.clone()) + .add_option(health_option.clone()); + let multi_select_field = FieldBuilder::new(multi_select_type_option) + .name("Tags") + .visibility(true) + .build(); + let multi_select_field_id = multi_select_field.id.clone(); + grid_builder.add_field(multi_select_field); + + for i in 0..3 { + let mut row_builder = RowRevisionBuilder::new(grid_builder.block_id(), grid_builder.field_revs()); + row_builder.insert_select_option_cell(&single_select_field_id, to_do_option.id.clone()); + match i { + 0 => { + row_builder.insert_text_cell(&text_field_id, "Update AppFlowy Website".to_string()); + row_builder.insert_select_option_cell(&multi_select_field_id, work_option.id.clone()); + } + 1 => { + row_builder.insert_text_cell(&text_field_id, "Learn French".to_string()); + row_builder.insert_select_option_cell(&multi_select_field_id, travel_option.id.clone()); + } + + 2 => { + row_builder.insert_text_cell(&text_field_id, "Exercise 4x/week".to_string()); + row_builder.insert_select_option_cell(&multi_select_field_id, fun_option.id.clone()); + } + _ => {} + } + let row = row_builder.build(); + grid_builder.add_row(row); + } + + for i in 0..3 { + let mut row_builder = RowRevisionBuilder::new(grid_builder.block_id(), grid_builder.field_revs()); + row_builder.insert_select_option_cell(&single_select_field_id, doing_option.id.clone()); + match i { + 0 => { + row_builder.insert_text_cell(&text_field_id, "Learn how to swim".to_string()); + row_builder.insert_select_option_cell(&multi_select_field_id, fun_option.id.clone()); + } + 1 => { + row_builder.insert_text_cell(&text_field_id, "Meditate 10 mins each day".to_string()); + row_builder.insert_select_option_cell(&multi_select_field_id, health_option.id.clone()); + } + + 2 => { + row_builder.insert_text_cell(&text_field_id, "Write atomic essays ".to_string()); + row_builder.insert_select_option_cell(&multi_select_field_id, fun_option.id.clone()); + } + _ => {} + } + let row = row_builder.build(); + grid_builder.add_row(row); + } + + for i in 0..2 { + let mut row_builder = RowRevisionBuilder::new(grid_builder.block_id(), grid_builder.field_revs()); + row_builder.insert_select_option_cell(&single_select_field_id, done_option.id.clone()); + match i { + 0 => { + row_builder.insert_text_cell(&text_field_id, "Publish an article".to_string()); + row_builder.insert_select_option_cell(&multi_select_field_id, work_option.id.clone()); + } + 1 => { + row_builder.insert_text_cell(&text_field_id, "Visit Chicago".to_string()); + row_builder.insert_select_option_cell(&multi_select_field_id, travel_option.id.clone()); + } + + _ => {} + } + let row = row_builder.build(); + grid_builder.add_row(row); + } + + grid_builder.build() +} + +#[allow(dead_code)] +pub fn make_default_board2() -> BuildGridContext { let mut grid_builder = GridBuilder::new(); // text let text_field = FieldBuilder::new(RichTextTypeOptionBuilder::default()) From d0f476dc72fd984eb661df0f40e70493497b4643 Mon Sep 17 00:00:00 2001 From: appflowy Date: Thu, 25 Aug 2022 14:38:13 +0800 Subject: [PATCH 039/106] chore: add max height constraint for board text cell --- .../presentation/card/board_text_cell.dart | 11 +++-- .../flowy_infra_ui/lib/style_widget/text.dart | 47 +++++++------------ 2 files changed, 23 insertions(+), 35 deletions(-) diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/board_text_cell.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/board_text_cell.dart index 6f6e35429a..8f481dd996 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/board_text_cell.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/board_text_cell.dart @@ -40,11 +40,12 @@ class _BoardTextCellState extends State { return Align( alignment: Alignment.centerLeft, child: Padding( - padding: - EdgeInsets.symmetric(vertical: BoardSizes.cardCellVPadding), - child: FlowyText.medium( - state.content, - fontSize: 14, + padding: EdgeInsets.symmetric( + vertical: BoardSizes.cardCellVPadding, + ), + child: ConstrainedBox( + constraints: const BoxConstraints(maxHeight: 120), + child: FlowyText.medium(state.content, fontSize: 14), ), ), ); diff --git a/frontend/app_flowy/packages/flowy_infra_ui/lib/style_widget/text.dart b/frontend/app_flowy/packages/flowy_infra_ui/lib/style_widget/text.dart index bad452cbe4..4b285a9137 100644 --- a/frontend/app_flowy/packages/flowy_infra_ui/lib/style_widget/text.dart +++ b/frontend/app_flowy/packages/flowy_infra_ui/lib/style_widget/text.dart @@ -4,15 +4,16 @@ import 'package:provider/provider.dart'; class FlowyText extends StatelessWidget { final String title; - final TextOverflow overflow; + final TextOverflow? overflow; final double fontSize; final FontWeight fontWeight; final TextAlign? textAlign; final Color? color; + const FlowyText( this.title, { Key? key, - this.overflow = TextOverflow.ellipsis, + this.overflow = TextOverflow.clip, this.fontSize = 16, this.fontWeight = FontWeight.w400, this.textAlign, @@ -20,47 +21,33 @@ class FlowyText extends StatelessWidget { }) : super(key: key); const FlowyText.semibold(this.title, - {Key? key, - this.fontSize = 16, - TextOverflow? overflow, - this.color, - this.textAlign}) + {Key? key, this.fontSize = 16, this.overflow, this.color, this.textAlign}) : fontWeight = FontWeight.w600, - overflow = overflow ?? TextOverflow.ellipsis, super(key: key); const FlowyText.medium(this.title, - {Key? key, - this.fontSize = 16, - TextOverflow? overflow, - this.color, - this.textAlign}) + {Key? key, this.fontSize = 16, this.overflow, this.color, this.textAlign}) : fontWeight = FontWeight.w500, - overflow = overflow ?? TextOverflow.ellipsis, super(key: key); const FlowyText.regular(this.title, - {Key? key, - this.fontSize = 16, - TextOverflow? overflow, - this.color, - this.textAlign}) + {Key? key, this.fontSize = 16, this.overflow, this.color, this.textAlign}) : fontWeight = FontWeight.w400, - overflow = overflow ?? TextOverflow.ellipsis, super(key: key); @override Widget build(BuildContext context) { final theme = context.watch(); - return Text(title, - softWrap: false, - textAlign: textAlign, - overflow: overflow, - style: TextStyle( - color: color ?? theme.textColor, - fontWeight: fontWeight, - fontSize: fontSize, - fontFamily: 'Mulish', - )); + return Text( + title, + textAlign: textAlign, + overflow: overflow ?? TextOverflow.clip, + style: TextStyle( + color: color ?? theme.textColor, + fontWeight: fontWeight, + fontSize: fontSize, + fontFamily: 'Mulish', + ), + ); } } From c43d0790dcc8a1797f5176289c268c63f99935c3 Mon Sep 17 00:00:00 2001 From: appflowy Date: Thu, 25 Aug 2022 14:58:08 +0800 Subject: [PATCH 040/106] chore: enable create board --- CHANGELOG.md | 10 ++++++++++ frontend/app_flowy/lib/plugins/board/board.dart | 2 +- .../card/board_select_option_cell.dart | 3 +-- .../plugins/board/presentation/card/define.dart | 2 +- .../selection_type_option/select_option.rs | 9 +++++++++ frontend/rust-lib/flowy-grid/src/util.rs | 15 ++++++++++++--- 6 files changed, 34 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ee49d36f3..94b4766806 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Release Notes +## Version 0.0.5 - beta.1 - 2022-08-25 + +New features +- Board-view database + - Group by single select + - drag and drop cards + - insert / delete cards + + + ## Version 0.0.4 - 2022-06-06 - Drag to adjust the width of a column - Upgrade to Flutter 3.0 diff --git a/frontend/app_flowy/lib/plugins/board/board.dart b/frontend/app_flowy/lib/plugins/board/board.dart index c55d7f2e17..213cc8bc3c 100644 --- a/frontend/app_flowy/lib/plugins/board/board.dart +++ b/frontend/app_flowy/lib/plugins/board/board.dart @@ -31,7 +31,7 @@ class BoardPluginBuilder implements PluginBuilder { class BoardPluginConfig implements PluginConfig { @override - bool get creatable => false; + bool get creatable => true; } class BoardPlugin extends Plugin { diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/board_select_option_cell.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/board_select_option_cell.dart index 6cba08c636..e10eda4cec 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/board_select_option_cell.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/board_select_option_cell.dart @@ -45,8 +45,7 @@ class _BoardSelectOptionCellState extends State { ) .toList(); return Padding( - padding: - EdgeInsets.symmetric(vertical: BoardSizes.cardCellVPadding), + padding: EdgeInsets.only(top: BoardSizes.cardCellVPadding), child: Align( alignment: Alignment.centerLeft, child: AbsorbPointer( diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/define.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/define.dart index c2cff2ee0f..5fc55743db 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/define.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/define.dart @@ -1,3 +1,3 @@ class BoardSizes { - static double get cardCellVPadding => 4; + static double get cardCellVPadding => 6; } diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/select_option.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/select_option.rs index 13da2e8359..9270f73684 100644 --- a/frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/select_option.rs +++ b/frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/select_option.rs @@ -157,6 +157,9 @@ pub fn select_option_color_from_index(index: usize) -> SelectOptionColorPB { pub struct SelectOptionIds(Vec); impl SelectOptionIds { + pub fn new() -> Self { + Self(vec![]) + } pub fn into_inner(self) -> Vec { self.0 } @@ -181,6 +184,12 @@ impl std::convert::From for SelectOptionIds { } } +impl ToString for SelectOptionIds { + fn to_string(&self) -> String { + self.0.join(SELECTION_IDS_SEPARATOR) + } +} + impl std::convert::From> for SelectOptionIds { fn from(s: Option) -> Self { match s { diff --git a/frontend/rust-lib/flowy-grid/src/util.rs b/frontend/rust-lib/flowy-grid/src/util.rs index 128c42ae04..65f6440c1f 100644 --- a/frontend/rust-lib/flowy-grid/src/util.rs +++ b/frontend/rust-lib/flowy-grid/src/util.rs @@ -86,7 +86,10 @@ pub fn make_default_board() -> BuildGridContext { } 1 => { row_builder.insert_text_cell(&text_field_id, "Learn French".to_string()); - row_builder.insert_select_option_cell(&multi_select_field_id, travel_option.id.clone()); + let mut options = SelectOptionIds::new(); + options.push(fun_option.id.clone()); + options.push(travel_option.id.clone()); + row_builder.insert_select_option_cell(&multi_select_field_id, options.to_string()); } 2 => { @@ -114,7 +117,10 @@ pub fn make_default_board() -> BuildGridContext { 2 => { row_builder.insert_text_cell(&text_field_id, "Write atomic essays ".to_string()); - row_builder.insert_select_option_cell(&multi_select_field_id, fun_option.id.clone()); + let mut options = SelectOptionIds::new(); + options.push(fun_option.id.clone()); + options.push(work_option.id.clone()); + row_builder.insert_select_option_cell(&multi_select_field_id, options.to_string()); } _ => {} } @@ -132,7 +138,10 @@ pub fn make_default_board() -> BuildGridContext { } 1 => { row_builder.insert_text_cell(&text_field_id, "Visit Chicago".to_string()); - row_builder.insert_select_option_cell(&multi_select_field_id, travel_option.id.clone()); + let mut options = SelectOptionIds::new(); + options.push(travel_option.id.clone()); + options.push(fun_option.id.clone()); + row_builder.insert_select_option_cell(&multi_select_field_id, options.to_string()); } _ => {} From 02283038d5ec1906cd57aa683b84687c7c383745 Mon Sep 17 00:00:00 2001 From: appflowy Date: Thu, 25 Aug 2022 16:17:19 +0800 Subject: [PATCH 041/106] chore: hide cell --- .../board/presentation/board_page.dart | 19 +++++++--- .../card/board_checkbox_cell.dart | 2 + .../presentation/card/board_date_cell.dart | 2 + .../presentation/card/board_number_cell.dart | 2 + .../card/board_select_option_cell.dart | 38 +++++++++++-------- .../presentation/card/board_text_cell.dart | 8 +++- .../presentation/card/board_url_cell.dart | 2 + .../plugins/board/presentation/card/card.dart | 4 +- .../presentation/card/card_cell_builder.dart | 9 ++++- .../example/lib/multi_board_list_example.dart | 6 +-- .../lib/single_board_list_example.dart | 5 ++- .../widgets/board_column/board_column.dart | 3 +- 12 files changed, 70 insertions(+), 30 deletions(-) diff --git a/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart b/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart index 3d109309d7..45e9b574df 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart @@ -69,7 +69,11 @@ class BoardContent extends StatelessWidget { dataController: context.read().boardController, headerBuilder: _buildHeader, footBuilder: _buildFooter, - cardBuilder: (_, data) => _buildCard(context, data), + cardBuilder: (_, column, columnItem) => _buildCard( + context, + column, + columnItem, + ), columnConstraints: const BoxConstraints.tightFor(width: 240), config: AFBoardConfig( columnBackgroundColor: HexColor.fromHex('#F7F8FC'), @@ -129,12 +133,16 @@ class BoardContent extends StatelessWidget { }); } - Widget _buildCard(BuildContext context, AFColumnItem item) { - final rowPB = (item as BoardColumnItem).row; + Widget _buildCard( + BuildContext context, + AFBoardColumnData column, + AFColumnItem columnItem, + ) { + final rowPB = (columnItem as BoardColumnItem).row; final rowCache = context.read().getRowCache(rowPB.blockId); /// Return placeholder widget if the rowCache is null. - if (rowCache == null) return SizedBox(key: ObjectKey(item)); + if (rowCache == null) return SizedBox(key: ObjectKey(columnItem)); final fieldCache = context.read().fieldCache; final gridId = context.read().gridId; @@ -151,11 +159,12 @@ class BoardContent extends StatelessWidget { ); return AppFlowyColumnItemCard( - key: ObjectKey(item), + key: ObjectKey(columnItem), margin: config.cardPadding, decoration: _makeBoxDecoration(context), child: BoardCard( gridId: gridId, + groupId: column.id, isEditing: isEditing, cellBuilder: cellBuilder, dataController: cardController, diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/board_checkbox_cell.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/board_checkbox_cell.dart index 522a23fe78..ab78a46765 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/board_checkbox_cell.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/board_checkbox_cell.dart @@ -6,9 +6,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class BoardCheckboxCell extends StatefulWidget { + final String groupId; final GridCellControllerBuilder cellControllerBuilder; const BoardCheckboxCell({ + required this.groupId, required this.cellControllerBuilder, Key? key, }) : super(key: key); diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/board_date_cell.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/board_date_cell.dart index 8896a32732..dd6fe621df 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/board_date_cell.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/board_date_cell.dart @@ -8,9 +8,11 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'define.dart'; class BoardDateCell extends StatefulWidget { + final String groupId; final GridCellControllerBuilder cellControllerBuilder; const BoardDateCell({ + required this.groupId, required this.cellControllerBuilder, Key? key, }) : super(key: key); diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/board_number_cell.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/board_number_cell.dart index 33fb4a9b4b..a651f9f3aa 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/board_number_cell.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/board_number_cell.dart @@ -7,9 +7,11 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'define.dart'; class BoardNumberCell extends StatefulWidget { + final String groupId; final GridCellControllerBuilder cellControllerBuilder; const BoardNumberCell({ + required this.groupId, required this.cellControllerBuilder, Key? key, }) : super(key: key); diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/board_select_option_cell.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/board_select_option_cell.dart index e10eda4cec..f1da64579e 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/board_select_option_cell.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/board_select_option_cell.dart @@ -7,9 +7,11 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'define.dart'; class BoardSelectOptionCell extends StatefulWidget { + final String groupId; final GridCellControllerBuilder cellControllerBuilder; const BoardSelectOptionCell({ + required this.groupId, required this.cellControllerBuilder, Key? key, }) : super(key: key); @@ -36,23 +38,29 @@ class _BoardSelectOptionCellState extends State { value: _cellBloc, child: BlocBuilder( builder: (context, state) { - final children = state.selectedOptions - .map( - (option) => SelectOptionTag.fromOption( - context: context, - option: option, + if (state.selectedOptions + .where((element) => element.id == widget.groupId) + .isNotEmpty) { + return const SizedBox(); + } else { + final children = state.selectedOptions + .map( + (option) => SelectOptionTag.fromOption( + context: context, + option: option, + ), + ) + .toList(); + return Padding( + padding: EdgeInsets.only(top: BoardSizes.cardCellVPadding), + child: Align( + alignment: Alignment.centerLeft, + child: AbsorbPointer( + child: Wrap(children: children, spacing: 4, runSpacing: 2), ), - ) - .toList(); - return Padding( - padding: EdgeInsets.only(top: BoardSizes.cardCellVPadding), - child: Align( - alignment: Alignment.centerLeft, - child: AbsorbPointer( - child: Wrap(children: children, spacing: 4, runSpacing: 2), ), - ), - ); + ); + } }, ), ); diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/board_text_cell.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/board_text_cell.dart index 8f481dd996..d7c4e67bc1 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/board_text_cell.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/board_text_cell.dart @@ -7,9 +7,13 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'define.dart'; class BoardTextCell extends StatefulWidget { + final String groupId; final GridCellControllerBuilder cellControllerBuilder; - const BoardTextCell({required this.cellControllerBuilder, Key? key}) - : super(key: key); + const BoardTextCell({ + required this.groupId, + required this.cellControllerBuilder, + Key? key, + }) : super(key: key); @override State createState() => _BoardTextCellState(); diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/board_url_cell.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/board_url_cell.dart index f687e7efeb..a70414baf4 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/board_url_cell.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/board_url_cell.dart @@ -7,9 +7,11 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'define.dart'; class BoardUrlCell extends StatefulWidget { + final String groupId; final GridCellControllerBuilder cellControllerBuilder; const BoardUrlCell({ + required this.groupId, required this.cellControllerBuilder, Key? key, }) : super(key: key); diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart index 7966f9e212..0fcce45fa9 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart @@ -14,6 +14,7 @@ typedef OnEndEditing = void Function(String rowId); class BoardCard extends StatefulWidget { final String gridId; + final String groupId; final bool isEditing; final CardDataController dataController; final BoardCellBuilder cellBuilder; @@ -22,6 +23,7 @@ class BoardCard extends StatefulWidget { const BoardCard({ required this.gridId, + required this.groupId, required this.isEditing, required this.dataController, required this.cellBuilder, @@ -71,7 +73,7 @@ class _BoardCardState extends State { List _makeCells(BuildContext context, GridCellMap cellMap) { return cellMap.values.map( (cellId) { - final child = widget.cellBuilder.buildCell(cellId); + final child = widget.cellBuilder.buildCell(widget.groupId, cellId); return Padding( padding: const EdgeInsets.symmetric(horizontal: 6), child: child, diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/card_cell_builder.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/card_cell_builder.dart index 10ae0db680..83dbf584e8 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/card_cell_builder.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/card_cell_builder.dart @@ -19,7 +19,7 @@ class BoardCellBuilder { BoardCellBuilder(this.delegate); - Widget buildCell(GridCellIdentifier cellId) { + Widget buildCell(String groupId, GridCellIdentifier cellId) { final cellControllerBuilder = GridCellControllerBuilder( delegate: delegate, cellId: cellId, @@ -30,36 +30,43 @@ class BoardCellBuilder { switch (cellId.fieldType) { case FieldType.Checkbox: return BoardCheckboxCell( + groupId: groupId, cellControllerBuilder: cellControllerBuilder, key: key, ); case FieldType.DateTime: return BoardDateCell( + groupId: groupId, cellControllerBuilder: cellControllerBuilder, key: key, ); case FieldType.SingleSelect: return BoardSelectOptionCell( + groupId: groupId, cellControllerBuilder: cellControllerBuilder, key: key, ); case FieldType.MultiSelect: return BoardSelectOptionCell( + groupId: groupId, cellControllerBuilder: cellControllerBuilder, key: key, ); case FieldType.Number: return BoardNumberCell( + groupId: groupId, cellControllerBuilder: cellControllerBuilder, key: key, ); case FieldType.RichText: return BoardTextCell( + groupId: groupId, cellControllerBuilder: cellControllerBuilder, key: key, ); case FieldType.URL: return BoardUrlCell( + groupId: groupId, cellControllerBuilder: cellControllerBuilder, key: key, ); diff --git a/frontend/app_flowy/packages/appflowy_board/example/lib/multi_board_list_example.dart b/frontend/app_flowy/packages/appflowy_board/example/lib/multi_board_list_example.dart index 854936ea29..e571a0559b 100644 --- a/frontend/app_flowy/packages/appflowy_board/example/lib/multi_board_list_example.dart +++ b/frontend/app_flowy/packages/appflowy_board/example/lib/multi_board_list_example.dart @@ -94,10 +94,10 @@ class _MultiBoardListExampleState extends State { margin: config.columnItemPadding, ); }, - cardBuilder: (context, item) { + cardBuilder: (context, column, columnItem) { return AppFlowyColumnItemCard( - key: ObjectKey(item), - child: _buildCard(item), + key: ObjectKey(columnItem), + child: _buildCard(columnItem), ); }, columnConstraints: const BoxConstraints.tightFor(width: 240), diff --git a/frontend/app_flowy/packages/appflowy_board/example/lib/single_board_list_example.dart b/frontend/app_flowy/packages/appflowy_board/example/lib/single_board_list_example.dart index 4dda616621..f22c562343 100644 --- a/frontend/app_flowy/packages/appflowy_board/example/lib/single_board_list_example.dart +++ b/frontend/app_flowy/packages/appflowy_board/example/lib/single_board_list_example.dart @@ -32,8 +32,9 @@ class _SingleBoardListExampleState extends State { Widget build(BuildContext context) { return AFBoard( dataController: boardData, - cardBuilder: (context, item) { - return _RowWidget(item: item as TextItem, key: ObjectKey(item)); + cardBuilder: (context, column, columnItem) { + return _RowWidget( + item: columnItem as TextItem, key: ObjectKey(columnItem)); }, ); } diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column.dart index a5e20055f6..ce053b5c79 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column.dart @@ -24,6 +24,7 @@ typedef OnColumnInserted = void Function(String listId, int insertedIndex); typedef AFBoardColumnCardBuilder = Widget Function( BuildContext context, + AFBoardColumnData columnData, AFColumnItem item, ); @@ -207,7 +208,7 @@ class _AFBoardColumnWidgetState extends State { passthroughPhantomContext: item.phantomContext, ); } else { - return widget.cardBuilder(context, item); + return widget.cardBuilder(context, widget.dataSource.columnData, item); } } } From 00351158080f09c2011e4a5d9fac81cedeb2f95f Mon Sep 17 00:00:00 2001 From: "Nathan.fooo" <86001920+appflowy@users.noreply.github.com> Date: Thu, 25 Aug 2022 16:24:57 +0800 Subject: [PATCH 042/106] Update CHANGELOG.md --- CHANGELOG.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 94b4766806..9f5b42c8b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,10 +4,11 @@ New features - Board-view database - - Group by single select - - drag and drop cards - - insert / delete cards - + - Group by single select + - drag and drop cards + - insert / delete cards + +![Aug-25-2022 16-22-38](https://user-images.githubusercontent.com/86001920/186614248-23186dfe-410e-427a-8cc6-865b1f79e074.gif) ## Version 0.0.4 - 2022-06-06 From 8cf1fee81a0ada5270649b97f0240977a5b813f0 Mon Sep 17 00:00:00 2001 From: appflowy Date: Thu, 25 Aug 2022 16:50:15 +0800 Subject: [PATCH 043/106] chore: update version --- frontend/Makefile.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/Makefile.toml b/frontend/Makefile.toml index 337b9efd76..f6e8194efa 100644 --- a/frontend/Makefile.toml +++ b/frontend/Makefile.toml @@ -22,7 +22,7 @@ CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true CARGO_MAKE_CRATE_FS_NAME = "dart_ffi" CARGO_MAKE_CRATE_NAME = "dart-ffi" LIB_NAME = "dart_ffi" -CURRENT_APP_VERSION = "0.0.4" +CURRENT_APP_VERSION = "0.0.5" FEATURES = "flutter" PRODUCT_NAME = "AppFlowy" # CRATE_TYPE: https://doc.rust-lang.org/reference/linkage.html From 96fcedf7717d3f93748d3075cb0ef87bef61ca87 Mon Sep 17 00:00:00 2001 From: Annie Date: Thu, 25 Aug 2022 19:38:16 +0800 Subject: [PATCH 044/106] chore: update changelog --- CHANGELOG.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f5b42c8b9..6522dc971f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Release Notes -## Version 0.0.5 - beta.1 - 2022-08-25 +## Version 0.0.5 - beta.1 - 08/25/2022 New features - Board-view database @@ -11,7 +11,7 @@ New features ![Aug-25-2022 16-22-38](https://user-images.githubusercontent.com/86001920/186614248-23186dfe-410e-427a-8cc6-865b1f79e074.gif) -## Version 0.0.4 - 2022-06-06 +## Version 0.0.4 - 06/06/2022 - Drag to adjust the width of a column - Upgrade to Flutter 3.0 - Native support for M1 chip @@ -23,7 +23,7 @@ New features - Fixed some bugs -## Version 0.0.4 - beta.3 - 2022-05-02 +## Version 0.0.4 - beta.3 - 05/02/2022 - Drag to reorder app/ view/ field - Row record open as a page - Auto resize the height of the row in the grid @@ -38,7 +38,7 @@ New features - Fixed some bugs -## Version 0.0.4 - beta.2 - 2022-04-11 +## Version 0.0.4 - beta.2 - 04/11/2022 - Support properties: Text, Number, Date, Checkbox, Select, Multi-select - Insert / delete rows @@ -46,7 +46,7 @@ New features - Edit property ![](https://user-images.githubusercontent.com/12026239/162753644-bf2f4e7a-2367-4d48-87e6-35e244e83a5b.png) -## Version 0.0.4 - beta.1 - 2022-04-08 +## Version 0.0.4 - beta.1 - 04/08/2022 v0.0.4 - beta.1 is pre-release New features @@ -55,7 +55,7 @@ New features - hide / delete columns - insert rows -## Version 0.0.3 - 2022-02-23 +## Version 0.0.3 - 02/23/2022 v0.0.3 is production ready, available on Linux, macOS, and Windows New features From b7c21df3b20f3c0f9ef1054e9d87d1b7006b64b2 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Thu, 25 Aug 2022 20:21:33 +0800 Subject: [PATCH 045/106] feat: implement simple export function --- .../appflowy_editor/example/lib/main.dart | 207 +++++++----------- .../appflowy_editor/example/pubspec.yaml | 1 + 2 files changed, 85 insertions(+), 123 deletions(-) diff --git a/frontend/app_flowy/packages/appflowy_editor/example/lib/main.dart b/frontend/app_flowy/packages/appflowy_editor/example/lib/main.dart index e5aae9432d..35dd8161db 100644 --- a/frontend/app_flowy/packages/appflowy_editor/example/lib/main.dart +++ b/frontend/app_flowy/packages/appflowy_editor/example/lib/main.dart @@ -4,12 +4,11 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'expandable_floating_action_button.dart'; -// import 'plugin/image_node_widget.dart'; -import 'plugin/youtube_link_node_widget.dart'; +import 'package:path_provider/path_provider.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:path_provider/path_provider.dart'; + +import 'expandable_floating_action_button.dart'; void main() { runApp(const MyApp()); @@ -18,20 +17,10 @@ void main() { class MyApp extends StatelessWidget { const MyApp({Key? key}) : super(key: key); - // This widget is the root of your application. @override Widget build(BuildContext context) { return MaterialApp( theme: ThemeData( - // This is the theme of your application. - // - // Try running your application with "flutter run". You'll see the - // application has a blue toolbar. Then, without quitting the app, try - // changing the primarySwatch below to Colors.green and then invoke - // "hot reload" (press "r" in the console where you ran "flutter run", - // or simply save your changes to "hot reload" in a Flutter IDE). - // Notice that the counter didn't reset back to zero; the application - // is not restarted. primarySwatch: Colors.blue, ), home: const MyHomePage(title: 'AppFlowyEditor Example'), @@ -41,16 +30,6 @@ class MyApp extends StatelessWidget { class MyHomePage extends StatefulWidget { const MyHomePage({Key? key, required this.title}) : super(key: key); - - // This widget is the home page of your application. It is stateful, meaning - // that it has a State object (defined below) that contains fields that affect - // how it looks. - - // This class is the configuration for the state. It holds the values (in this - // case the title) provided by the parent (in this case the App widget) and - // used by the build method of the State. Fields in a Widget subclass are - // always marked "final". - final String title; @override @@ -58,56 +37,60 @@ class MyHomePage extends StatefulWidget { } class _MyHomePageState extends State { - final editorKey = GlobalKey(); - int page = 0; - EditorState? _editorState; + int _pageIndex = 0; + late EditorState _editorState; + Future? _jsonString; @override Widget build(BuildContext context) { return Scaffold( body: Container( alignment: Alignment.topCenter, - child: _buildBody(), + child: _buildEditor(context), ), floatingActionButton: _buildExpandableFab(), ); } - Widget _buildBody() { - if (page == 0) { - return _buildAppFlowyEditorWithExample(); - } else if (page == 1) { - return _buildAppFlowyEditorWithEmptyDocument(); - } else if (page == 2) { - return _buildAppFlowyEditorWithBigDocument(); + Widget _buildEditor(BuildContext context) { + if (_jsonString != null) { + return _buildEditorWithJsonPath(_jsonString!); } - return Container(); + if (_pageIndex == 0) { + return _buildEditorWithJsonPath( + rootBundle.loadString('assets/example.json')); + } else if (_pageIndex == 1) { + return _buildEditorWithJsonPath( + rootBundle.loadString('assets/big_document.json')); + } else if (_pageIndex == 2) { + return _buildEditorWithEmptyDocument(); + } + throw UnimplementedError(); } - Widget _buildAppFlowyEditorWithEmptyDocument() { - final editorState = EditorState.empty(); - final editor = AppFlowyEditor( - editorState: editorState, - keyEventHandlers: const [], - customBuilders: const {}, - ); - return editor; - } - - Widget _buildAppFlowyEditorWithExample() { + Widget _buildEditorWithJsonPath(Future jsonString) { return FutureBuilder( - future: rootBundle.loadString('assets/example.json'), - builder: (context, snapshot) { + future: jsonString, + builder: (_, snapshot) { if (snapshot.hasData) { - final data = Map.from(json.decode(snapshot.data!)); - final editorState = EditorState(document: StateTree.fromJson(data)); - editorState.logConfiguration + _editorState = EditorState( + document: StateTree.fromJson( + Map.from( + json.decode(snapshot.data!), + ), + ), + ); + _editorState.logConfiguration ..level = LogLevel.all ..handler = (message) { debugPrint(message); }; - _editorState = editorState; - return _buildAppFlowyEditor(editorState); + return Container( + padding: const EdgeInsets.only(left: 20, right: 20), + child: AppFlowyEditor( + editorState: _editorState, + ), + ); } else { return const Center( child: CircularProgressIndicator(), @@ -117,52 +100,17 @@ class _MyHomePageState extends State { ); } - Widget _buildAppFlowyEditorWithBigDocument() { - return FutureBuilder( - future: rootBundle.loadString('assets/big_document.json'), - builder: (context, snapshot) { - if (snapshot.hasData) { - final data = Map.from(json.decode(snapshot.data!)); - return _buildAppFlowyEditor(EditorState( - document: StateTree.fromJson(data), - )); - } else { - return const Center( - child: CircularProgressIndicator(), - ); - } - }, + Widget _buildEditorWithEmptyDocument() { + _editorState = EditorState.empty(); + _editorState.logConfiguration + ..level = LogLevel.all + ..handler = (message) { + debugPrint(message); + }; + final editor = AppFlowyEditor( + editorState: _editorState, ); - } - - Widget _buildAppFlowyEditor(EditorState editorState) { - return Container( - padding: const EdgeInsets.only(left: 20, right: 20), - child: AppFlowyEditor( - key: editorKey, - editorState: editorState, - keyEventHandlers: const [], - customBuilders: { - // 'image': ImageNodeBuilder(), - 'youtube_link': YouTubeLinkNodeBuilder() - }, - ), - ); - } - - void _exportDocument(EditorState editorState) async { - // await FileSaver.instance.saveAs(String name, Uint8List bytes, String ext, MimeType); - final document = editorState.document.toJson(); - debugPrint(document.toString()); - final json = jsonEncode(document); - debugPrint(json); - - final directory = await getTemporaryDirectory(); - final path = directory.path; - debugPrint(path); - - final file = File('$path/temp.json'); - await file.writeAsString(json); + return editor; } Widget _buildExpandableFab() { @@ -170,39 +118,52 @@ class _MyHomePageState extends State { distance: 112.0, children: [ ActionButton( - onPressed: () { - if (page == 0) return; - setState(() { - page = 0; - }); - }, - icon: const Icon(Icons.note_add), + icon: const Icon(Icons.abc), + onPressed: () => _switchToPage(0), ), ActionButton( - icon: const Icon(Icons.document_scanner), - onPressed: () { - if (page == 1) return; - setState(() { - page = 1; - }); - }, + icon: const Icon(Icons.abc), + onPressed: () => _switchToPage(1), ), ActionButton( - onPressed: () { - if (page == 2) return; - setState(() { - page = 2; - }); - }, - icon: const Icon(Icons.text_fields), + icon: const Icon(Icons.abc), + onPressed: () => _switchToPage(2), ), ActionButton( - onPressed: () { - _exportDocument(_editorState!); - }, icon: const Icon(Icons.print), + onPressed: () => _exportDocument(_editorState), + ), + ActionButton( + icon: const Icon(Icons.import_export), + onPressed: () => _importDocument(), ), ], ); } + + void _exportDocument(EditorState editorState) async { + final document = editorState.document.toJson(); + final json = jsonEncode(document); + final directory = await getTemporaryDirectory(); + final path = directory.path; + final file = File('$path/editor.json'); + await file.writeAsString(json); + } + + void _importDocument() async { + final directory = await getTemporaryDirectory(); + final path = directory.path; + final file = File('$path/editor.json'); + setState(() { + _jsonString = file.readAsString(); + }); + } + + void _switchToPage(int pageIndex) { + if (pageIndex != _pageIndex) { + setState(() { + _pageIndex = pageIndex; + }); + } + } } diff --git a/frontend/app_flowy/packages/appflowy_editor/example/pubspec.yaml b/frontend/app_flowy/packages/appflowy_editor/example/pubspec.yaml index 593d9ff45f..9f2fae24bd 100644 --- a/frontend/app_flowy/packages/appflowy_editor/example/pubspec.yaml +++ b/frontend/app_flowy/packages/appflowy_editor/example/pubspec.yaml @@ -41,6 +41,7 @@ dependencies: pod_player: 0.0.8 flutter_inappwebview: ^5.4.3+7 file_saver: ^0.1.1 + path_provider: ^2.0.11 dev_dependencies: flutter_test: From 2b725f8f7110872b0f6adb67a5172f814e2dc478 Mon Sep 17 00:00:00 2001 From: appflowy Date: Fri, 26 Aug 2022 08:32:25 +0800 Subject: [PATCH 046/106] chore: add remove out of bound guard --- .../board/presentation/card/board_select_option_cell.dart | 3 +++ .../lib/src/widgets/board_column/board_column_data.dart | 4 ++++ .../lib/src/widgets/reorder_flex/drag_target.dart | 2 +- 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/board_select_option_cell.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/board_select_option_cell.dart index f1da64579e..faed598d09 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/board_select_option_cell.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/board_select_option_cell.dart @@ -37,6 +37,9 @@ class _BoardSelectOptionCellState extends State { return BlocProvider.value( value: _cellBloc, child: BlocBuilder( + // buildWhen: (previous, current) { + // return previous.selectedOptions != current.selectedOptions; + // }, builder: (context, state) { if (state.selectedOptions .where((element) => element.id == widget.groupId) diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column_data.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column_data.dart index 0015ebd479..bc442acd2a 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column_data.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column_data.dart @@ -121,6 +121,10 @@ class AFBoardColumnDataController extends ChangeNotifier with EquatableMixin { columnData._items.add(newItem); Log.debug('[$AFBoardColumnDataController] $columnData add $newItem'); } else { + if (index >= columnData._items.length) { + return; + } + final removedItem = columnData._items.removeAt(index); columnData._items.insert(index, newItem); Log.debug( diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_target.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_target.dart index 8217d2736f..a091e9711a 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_target.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_target.dart @@ -140,7 +140,7 @@ class _ReorderDragTargetState widget.insertAnimationController, widget.deleteAnimationController, ) ?? - LongPressDraggable( + Draggable( maxSimultaneousDrags: 1, data: widget.dragTargetData, ignoringFeedbackSemantics: false, From d3c13d325ee8c4336db576cc6d8c0c466c433f8f Mon Sep 17 00:00:00 2001 From: appflowy Date: Fri, 26 Aug 2022 08:47:41 +0800 Subject: [PATCH 047/106] chore: fix card animation issue --- .../card/board_checkbox_cell.dart | 17 ++++++------ .../presentation/card/board_date_cell.dart | 16 ++++------- .../presentation/card/board_number_cell.dart | 17 +++++------- .../card/board_select_option_cell.dart | 18 +++++-------- .../presentation/card/board_text_cell.dart | 14 +++------- .../presentation/card/board_url_cell.dart | 27 ++++++++----------- .../plugins/board/presentation/card/card.dart | 2 +- 7 files changed, 41 insertions(+), 70 deletions(-) diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/board_checkbox_cell.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/board_checkbox_cell.dart index ab78a46765..f832d3749d 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/board_checkbox_cell.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/board_checkbox_cell.dart @@ -36,19 +36,18 @@ class _BoardCheckboxCellState extends State { return BlocProvider.value( value: _cellBloc, child: BlocBuilder( + buildWhen: (previous, current) => + previous.isSelected != current.isSelected, builder: (context, state) { final icon = state.isSelected ? svgWidget('editor/editor_check') : svgWidget('editor/editor_uncheck'); - return Padding( - padding: EdgeInsets.zero, - child: Align( - alignment: Alignment.centerLeft, - child: FlowyIconButton( - iconPadding: EdgeInsets.zero, - icon: icon, - width: 20, - ), + return Align( + alignment: Alignment.centerLeft, + child: FlowyIconButton( + iconPadding: EdgeInsets.zero, + icon: icon, + width: 20, ), ); }, diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/board_date_cell.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/board_date_cell.dart index dd6fe621df..47472a0f9f 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/board_date_cell.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/board_date_cell.dart @@ -5,8 +5,6 @@ import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'define.dart'; - class BoardDateCell extends StatefulWidget { final String groupId; final GridCellControllerBuilder cellControllerBuilder; @@ -39,21 +37,17 @@ class _BoardDateCellState extends State { return BlocProvider.value( value: _cellBloc, child: BlocBuilder( + buildWhen: (previous, current) => previous.dateStr != current.dateStr, builder: (context, state) { if (state.dateStr.isEmpty) { return const SizedBox(); } else { return Align( alignment: Alignment.centerLeft, - child: Padding( - padding: EdgeInsets.symmetric( - vertical: BoardSizes.cardCellVPadding, - ), - child: FlowyText.regular( - state.dateStr, - fontSize: 13, - color: context.read().shader3, - ), + child: FlowyText.regular( + state.dateStr, + fontSize: 13, + color: context.read().shader3, ), ); } diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/board_number_cell.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/board_number_cell.dart index a651f9f3aa..0f4aca6b61 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/board_number_cell.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/board_number_cell.dart @@ -4,8 +4,6 @@ import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'define.dart'; - class BoardNumberCell extends StatefulWidget { final String groupId; final GridCellControllerBuilder cellControllerBuilder; @@ -38,19 +36,16 @@ class _BoardNumberCellState extends State { return BlocProvider.value( value: _cellBloc, child: BlocBuilder( + buildWhen: (previous, current) => previous.content != current.content, builder: (context, state) { if (state.content.isEmpty) { return const SizedBox(); } else { - return Padding( - padding: - EdgeInsets.symmetric(vertical: BoardSizes.cardCellVPadding), - child: Align( - alignment: Alignment.centerLeft, - child: FlowyText.medium( - state.content, - fontSize: 14, - ), + return Align( + alignment: Alignment.centerLeft, + child: FlowyText.medium( + state.content, + fontSize: 14, ), ); } diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/board_select_option_cell.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/board_select_option_cell.dart index faed598d09..f75de47651 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/board_select_option_cell.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/board_select_option_cell.dart @@ -4,8 +4,6 @@ import 'package:app_flowy/plugins/grid/presentation/widgets/cell/select_option_c import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'define.dart'; - class BoardSelectOptionCell extends StatefulWidget { final String groupId; final GridCellControllerBuilder cellControllerBuilder; @@ -37,9 +35,8 @@ class _BoardSelectOptionCellState extends State { return BlocProvider.value( value: _cellBloc, child: BlocBuilder( - // buildWhen: (previous, current) { - // return previous.selectedOptions != current.selectedOptions; - // }, + buildWhen: (previous, current) => + previous.selectedOptions != current.selectedOptions, builder: (context, state) { if (state.selectedOptions .where((element) => element.id == widget.groupId) @@ -54,13 +51,10 @@ class _BoardSelectOptionCellState extends State { ), ) .toList(); - return Padding( - padding: EdgeInsets.only(top: BoardSizes.cardCellVPadding), - child: Align( - alignment: Alignment.centerLeft, - child: AbsorbPointer( - child: Wrap(children: children, spacing: 4, runSpacing: 2), - ), + return Align( + alignment: Alignment.centerLeft, + child: AbsorbPointer( + child: Wrap(children: children, spacing: 4, runSpacing: 2), ), ); } diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/board_text_cell.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/board_text_cell.dart index d7c4e67bc1..deea60e793 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/board_text_cell.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/board_text_cell.dart @@ -4,8 +4,6 @@ import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'define.dart'; - class BoardTextCell extends StatefulWidget { final String groupId; final GridCellControllerBuilder cellControllerBuilder; @@ -37,20 +35,16 @@ class _BoardTextCellState extends State { return BlocProvider.value( value: _cellBloc, child: BlocBuilder( + buildWhen: (previous, current) => previous.content != current.content, builder: (context, state) { if (state.content.isEmpty) { return const SizedBox(); } else { return Align( alignment: Alignment.centerLeft, - child: Padding( - padding: EdgeInsets.symmetric( - vertical: BoardSizes.cardCellVPadding, - ), - child: ConstrainedBox( - constraints: const BoxConstraints(maxHeight: 120), - child: FlowyText.medium(state.content, fontSize: 14), - ), + child: ConstrainedBox( + constraints: const BoxConstraints(maxHeight: 120), + child: FlowyText.medium(state.content, fontSize: 14), ), ); } diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/board_url_cell.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/board_url_cell.dart index a70414baf4..40cdec7c2f 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/board_url_cell.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/board_url_cell.dart @@ -4,8 +4,6 @@ import 'package:flowy_infra/theme.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'define.dart'; - class BoardUrlCell extends StatefulWidget { final String groupId; final GridCellControllerBuilder cellControllerBuilder; @@ -38,24 +36,21 @@ class _BoardUrlCellState extends State { return BlocProvider.value( value: _cellBloc, child: BlocBuilder( + buildWhen: (previous, current) => previous.content != current.content, builder: (context, state) { if (state.content.isEmpty) { return const SizedBox(); } else { - return Padding( - padding: - EdgeInsets.symmetric(vertical: BoardSizes.cardCellVPadding), - child: Align( - alignment: Alignment.centerLeft, - child: RichText( - textAlign: TextAlign.left, - text: TextSpan( - text: state.content, - style: TextStyle( - color: theme.main2, - fontSize: 14, - decoration: TextDecoration.underline, - ), + return Align( + alignment: Alignment.centerLeft, + child: RichText( + textAlign: TextAlign.left, + text: TextSpan( + text: state.content, + style: TextStyle( + color: theme.main2, + fontSize: 14, + decoration: TextDecoration.underline, ), ), ), diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart index 0fcce45fa9..65c7d3dade 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart @@ -75,7 +75,7 @@ class _BoardCardState extends State { (cellId) { final child = widget.cellBuilder.buildCell(widget.groupId, cellId); return Padding( - padding: const EdgeInsets.symmetric(horizontal: 6), + padding: const EdgeInsets.only(left: 4, right: 4, top: 6), child: child, ); }, From 9ce7fcb7046974889fa6482572270cb43e6fd4a9 Mon Sep 17 00:00:00 2001 From: appflowy Date: Fri, 26 Aug 2022 09:41:10 +0800 Subject: [PATCH 048/106] chore: edit create card --- .../app_flowy/lib/plugins/board/application/board_bloc.dart | 5 ----- .../lib/plugins/board/application/board_data_controller.dart | 2 +- .../plugins/grid/presentation/widgets/row/row_detail.dart | 2 +- 3 files changed, 2 insertions(+), 7 deletions(-) diff --git a/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart b/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart index 7c04dec814..65e185a089 100644 --- a/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart +++ b/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart @@ -171,9 +171,6 @@ class BoardBloc extends Bloc { boardController.addColumns(columns); initializeGroups(groups); }, - onRowsChanged: (List rowInfos, RowsChangedReason reason) { - add(BoardEvent.didReceiveRows(rowInfos)); - }, onDeletedGroup: (groupIds) { // }, @@ -223,8 +220,6 @@ class BoardEvent with _$BoardEvent { const factory BoardEvent.createRow(String groupId) = _CreateRow; const factory BoardEvent.endEditRow(String rowId) = _EndEditRow; const factory BoardEvent.didReceiveError(FlowyError error) = _DidReceiveError; - const factory BoardEvent.didReceiveRows(List rowInfos) = - _DidReceiveRows; const factory BoardEvent.didReceiveGridUpdate( GridPB grid, ) = _DidReceiveGridUpdate; diff --git a/frontend/app_flowy/lib/plugins/board/application/board_data_controller.dart b/frontend/app_flowy/lib/plugins/board/application/board_data_controller.dart index 31b2594497..79f53093f1 100644 --- a/frontend/app_flowy/lib/plugins/board/application/board_data_controller.dart +++ b/frontend/app_flowy/lib/plugins/board/application/board_data_controller.dart @@ -60,7 +60,7 @@ class BoardDataController { required OnGridChanged onGridChanged, OnFieldsChanged? onFieldsChanged, required DidLoadGroups didLoadGroups, - required OnRowsChanged onRowsChanged, + OnRowsChanged? onRowsChanged, required OnUpdatedGroup onUpdatedGroup, required OnDeletedGroup onDeletedGroup, required OnInsertedGroup onInsertedGroup, diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/row_detail.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/row_detail.dart index a98fcfa688..d1ea79b6d1 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/row_detail.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/row_detail.dart @@ -35,7 +35,7 @@ class RowDetailPage extends StatefulWidget with FlowyOverlayDelegate { void show(BuildContext context) async { final windowSize = MediaQuery.of(context).size; - final size = windowSize * 0.7; + final size = windowSize * 0.5; FlowyOverlay.of(context).insertWithRect( widget: OverlayContainer( child: this, From 2b2bae80ef0d2cc964b178a2bd965bfc6251b438 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Fri, 26 Aug 2022 10:53:20 +0800 Subject: [PATCH 049/106] chore: update example project and example.json --- .../example/assets/example.json | 318 ++++-------------- .../appflowy_editor/example/lib/main.dart | 49 +-- .../flutter/generated_plugin_registrant.cc | 4 - .../linux/flutter/generated_plugins.cmake | 1 - .../Flutter/GeneratedPluginRegistrant.swift | 2 - .../example/macos/Podfile.lock | 6 - .../appflowy_editor/example/pubspec.yaml | 1 - .../flutter/generated_plugin_registrant.cc | 3 - .../windows/flutter/generated_plugins.cmake | 1 - 9 files changed, 97 insertions(+), 288 deletions(-) diff --git a/frontend/app_flowy/packages/appflowy_editor/example/assets/example.json b/frontend/app_flowy/packages/appflowy_editor/example/assets/example.json index b522a68594..48184a6511 100644 --- a/frontend/app_flowy/packages/appflowy_editor/example/assets/example.json +++ b/frontend/app_flowy/packages/appflowy_editor/example/assets/example.json @@ -1,276 +1,102 @@ { "document": { "type": "editor", - "attributes": {}, "children": [ { "type": "image", "attributes": { - "image_src": "https://images.squarespace-cdn.com/content/v1/617f6f16b877c06711e87373/c3f23723-37f4-44d7-9c5d-6e2a53064ae7/Asset+10.png", + "image_src": "https://s1.ax1x.com/2022/08/26/v2sSbR.jpg", "align": "center" } }, { "type": "text", + "attributes": { "subtype": "heading", "heading": "h1" }, "delta": [ + { "insert": "👋 " }, + { "insert": "Welcome to ", "attributes": { "bold": true } }, { - "insert": "🌶 Read Me" - } - ], - "attributes": { - "subtype": "heading", - "heading": "h1" - } - }, - { - "type": "text", - "delta": [ - { - "insert": "👋 Welcome to FlowyEditor" - } - ], - "attributes": { - "subtype": "heading", - "heading": "h2" - } - }, - { - "type": "text", - "delta": [ - { - "insert": "To be honest, we are still in the alpha stage. There are still many functions that need to be completed. And we are developing more features. Please give us a star if the " - }, - { - "insert": "FlowyEditor", + "insert": "AppFlowy Editor", "attributes": { - "href": "https://github.com/AppFlowy-IO/AppFlowy" + "href": "appflowy.io", + "italic": true, + "bold": true + } + } + ] + }, + { "type": "text", "delta": [] }, + { + "type": "text", + "delta": [ + { "insert": "AppFlowy Editor is a " }, + { "insert": "highly customizable", "attributes": { "bold": true } }, + { "insert": " " }, + { "insert": "rich-text editor", "attributes": { "italic": true } }, + { "insert": " for " }, + { "insert": "Flutter", "attributes": { "underline": true } } + ] + }, + { + "type": "text", + "attributes": { "checkbox": true, "subtype": "checkbox" }, + "delta": [{ "insert": "Customizable" }] + }, + { + "type": "text", + "attributes": { "checkbox": true, "subtype": "checkbox" }, + "delta": [{ "insert": "Test-covered" }] + }, + { + "type": "text", + "attributes": { "checkbox": false, "subtype": "checkbox" }, + "delta": [{ "insert": "more to come!" }] + }, + { "type": "text", "delta": [] }, + { + "type": "text", + "attributes": { "subtype": "quote" }, + "delta": [{ "insert": "Here is an exmaple you can give it a try" }] + }, + { "type": "text", "delta": [] }, + { + "type": "text", + "delta": [ + { "insert": "You can also use " }, + { + "insert": "AppFlowy Editor", + "attributes": { + "italic": true, + "bold": true, + "backgroundColor": "0x6000BCF0" } }, + { "insert": " as a component to build your own app." } + ] + }, + { "type": "text", "delta": [] }, + { + "type": "text", + "attributes": { "subtype": "bulleted-list" }, + "delta": [{ "insert": "Use / to insert blocks" }] + }, + { + "type": "text", + "attributes": { "subtype": "bulleted-list" }, + "delta": [ { - "insert": " helps you. 😊😊😊" + "insert": "Select text to trigger to the toolbar to format your notes." } ] }, + { "type": "text", "delta": [] }, { "type": "text", "delta": [ { - "insert": "Since the FlowyEditor are a community-driven open source editor, we very welcome and appreciate every pull request submissions from everyone.😄😄😄" + "insert": "If you have questions or feedback, please submit an issue on Github or join the community along with 1000+ builders!" } ] - }, - { - "type": "text", - "delta": [ - { - "insert": "Here are the basics:" - } - ], - "attributes": { - "subtype": "heading", - "heading": "h3" - } - }, - { - "type": "text", - "delta": [ - { "insert": "Click " }, - { "insert": "anywhere", "attributes": { "underline": true } }, - { "insert": " and just typing." } - ] - }, - { - "type": "text", - "delta": [ - { - "insert": "Hit" - }, - { - "insert": " / ", - "attributes": { "backgroundColor": "0xFFFFFF00" } - }, - { - "insert": "to see all the types of content you can add - headers, bulleted lists, checkboxes, etc." - } - ] - }, - { - "type": "text", - "delta": [ - { - "insert": "Highlight", - "attributes": { "backgroundColor": "0xFF00BCFB" } - }, - { - "insert": " any text, and use the menu that pops up to " - }, - { "insert": "style", "attributes": { "bold": true } }, - { "insert": " your ", "attributes": { "italic": true } }, - { "insert": "writing", "attributes": { "strikethrough": true } }, - { "insert": "." } - ] - }, - { - "type": "text", - "delta": [ - { - "insert": "Here are the plugins:" - } - ], - "attributes": { - "subtype": "heading", - "heading": "h3" - } - }, - { - "type": "image", - "attributes": { - "image_src": "https://s1.ax1x.com/2022/08/24/vgAJED.png", - "align": "left", - "width": 300 - } - }, - { - "type": "text", - "delta": [ - { - "insert": "Hello world" - } - ], - "attributes": { - "subtype": "checkbox", - "checkbox": false - } - }, - { - "type": "text", - "delta": [ - { - "insert": "Hello world" - } - ], - "attributes": { - "subtype": "checkbox", - "checkbox": false - } - }, - { - "type": "text", - "delta": [ - { - "insert": "Hello world" - } - ], - "attributes": { - "subtype": "checkbox", - "checkbox": false - } - }, - { - "type": "text", - "delta": [ - { - "insert": "Hello world" - } - ], - "attributes": { - "subtype": "bulleted-list" - } - }, - { - "type": "text", - "delta": [ - { - "insert": "Hello world" - } - ], - "attributes": { - "subtype": "bulleted-list" - } - }, - { - "type": "text", - "delta": [ - { - "insert": "Hello " - }, - { - "insert": "world", - "attributes": { "bold": true } - } - ], - "attributes": { - "subtype": "bulleted-list" - } - }, - { - "type": "text", - "delta": [ - { - "insert": "Hello world" - } - ], - "attributes": { - "subtype": "quote" - } - }, - { - "type": "text", - "delta": [ - { - "insert": "Hello world" - } - ], - "attributes": { - "subtype": "quote" - } - }, - { - "type": "text", - "delta": [ - { - "insert": "Hello world" - } - ], - "attributes": { - "subtype": "quote" - } - }, - { - "type": "text", - "delta": [ - { - "insert": "Hello world" - } - ], - "attributes": { - "subtype": "number-list", - "number": 1 - } - }, - { - "type": "text", - "delta": [ - { - "insert": "Hello world" - } - ], - "attributes": { - "subtype": "number-list", - "number": 2 - } - }, - { - "type": "text", - "delta": [ - { - "insert": "Hello world" - } - ], - "attributes": { - "subtype": "number-list", - "number": 3 - } } ] } diff --git a/frontend/app_flowy/packages/appflowy_editor/example/lib/main.dart b/frontend/app_flowy/packages/appflowy_editor/example/lib/main.dart index 35dd8161db..e72739e246 100644 --- a/frontend/app_flowy/packages/appflowy_editor/example/lib/main.dart +++ b/frontend/app_flowy/packages/appflowy_editor/example/lib/main.dart @@ -20,6 +20,7 @@ class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( + debugShowCheckedModeBanner: false, theme: ThemeData( primarySwatch: Colors.blue, ), @@ -54,21 +55,27 @@ class _MyHomePageState extends State { Widget _buildEditor(BuildContext context) { if (_jsonString != null) { - return _buildEditorWithJsonPath(_jsonString!); + return _buildEditorWithJsonString(_jsonString!); } if (_pageIndex == 0) { - return _buildEditorWithJsonPath( - rootBundle.loadString('assets/example.json')); + return _buildEditorWithJsonString( + rootBundle.loadString('assets/example.json'), + ); } else if (_pageIndex == 1) { - return _buildEditorWithJsonPath( - rootBundle.loadString('assets/big_document.json')); + return _buildEditorWithJsonString( + rootBundle.loadString('assets/big_document.json'), + ); } else if (_pageIndex == 2) { - return _buildEditorWithEmptyDocument(); + return _buildEditorWithJsonString( + Future.value( + jsonEncode(EditorState.empty().document.toJson()), + ), + ); } throw UnimplementedError(); } - Widget _buildEditorWithJsonPath(Future jsonString) { + Widget _buildEditorWithJsonString(Future jsonString) { return FutureBuilder( future: jsonString, builder: (_, snapshot) { @@ -86,7 +93,7 @@ class _MyHomePageState extends State { debugPrint(message); }; return Container( - padding: const EdgeInsets.only(left: 20, right: 20), + padding: const EdgeInsets.all(20), child: AppFlowyEditor( editorState: _editorState, ), @@ -100,19 +107,6 @@ class _MyHomePageState extends State { ); } - Widget _buildEditorWithEmptyDocument() { - _editorState = EditorState.empty(); - _editorState.logConfiguration - ..level = LogLevel.all - ..handler = (message) { - debugPrint(message); - }; - final editor = AppFlowyEditor( - editorState: _editorState, - ); - return editor; - } - Widget _buildExpandableFab() { return ExpandableFab( distance: 112.0, @@ -130,9 +124,8 @@ class _MyHomePageState extends State { onPressed: () => _switchToPage(2), ), ActionButton( - icon: const Icon(Icons.print), - onPressed: () => _exportDocument(_editorState), - ), + icon: const Icon(Icons.print), + onPressed: () => {_exportDocument(_editorState)}), ActionButton( icon: const Icon(Icons.import_export), onPressed: () => _importDocument(), @@ -148,6 +141,14 @@ class _MyHomePageState extends State { final path = directory.path; final file = File('$path/editor.json'); await file.writeAsString(json); + + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('The document is saved to the ${file.path}'), + ), + ); + } } void _importDocument() async { diff --git a/frontend/app_flowy/packages/appflowy_editor/example/linux/flutter/generated_plugin_registrant.cc b/frontend/app_flowy/packages/appflowy_editor/example/linux/flutter/generated_plugin_registrant.cc index 45a19e066d..00fd3bc03f 100644 --- a/frontend/app_flowy/packages/appflowy_editor/example/linux/flutter/generated_plugin_registrant.cc +++ b/frontend/app_flowy/packages/appflowy_editor/example/linux/flutter/generated_plugin_registrant.cc @@ -6,14 +6,10 @@ #include "generated_plugin_registrant.h" -#include #include #include void fl_register_plugins(FlPluginRegistry* registry) { - g_autoptr(FlPluginRegistrar) file_saver_registrar = - fl_plugin_registry_get_registrar_for_plugin(registry, "FileSaverPlugin"); - file_saver_plugin_register_with_registrar(file_saver_registrar); g_autoptr(FlPluginRegistrar) rich_clipboard_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "RichClipboardPlugin"); rich_clipboard_plugin_register_with_registrar(rich_clipboard_linux_registrar); diff --git a/frontend/app_flowy/packages/appflowy_editor/example/linux/flutter/generated_plugins.cmake b/frontend/app_flowy/packages/appflowy_editor/example/linux/flutter/generated_plugins.cmake index 10140a75b4..0342e3868a 100644 --- a/frontend/app_flowy/packages/appflowy_editor/example/linux/flutter/generated_plugins.cmake +++ b/frontend/app_flowy/packages/appflowy_editor/example/linux/flutter/generated_plugins.cmake @@ -3,7 +3,6 @@ # list(APPEND FLUTTER_PLUGIN_LIST - file_saver rich_clipboard_linux url_launcher_linux ) diff --git a/frontend/app_flowy/packages/appflowy_editor/example/macos/Flutter/GeneratedPluginRegistrant.swift b/frontend/app_flowy/packages/appflowy_editor/example/macos/Flutter/GeneratedPluginRegistrant.swift index ac60ac6ec9..08b7c3b866 100644 --- a/frontend/app_flowy/packages/appflowy_editor/example/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/frontend/app_flowy/packages/appflowy_editor/example/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,14 +5,12 @@ import FlutterMacOS import Foundation -import file_saver import path_provider_macos import rich_clipboard_macos import url_launcher_macos import wakelock_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { - FileSaverPlugin.register(with: registry.registrar(forPlugin: "FileSaverPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) RichClipboardPlugin.register(with: registry.registrar(forPlugin: "RichClipboardPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) diff --git a/frontend/app_flowy/packages/appflowy_editor/example/macos/Podfile.lock b/frontend/app_flowy/packages/appflowy_editor/example/macos/Podfile.lock index c56e297a68..1fcb47735c 100644 --- a/frontend/app_flowy/packages/appflowy_editor/example/macos/Podfile.lock +++ b/frontend/app_flowy/packages/appflowy_editor/example/macos/Podfile.lock @@ -1,6 +1,4 @@ PODS: - - file_saver (0.0.1): - - FlutterMacOS - FlutterMacOS (1.0.0) - path_provider_macos (0.0.1): - FlutterMacOS @@ -12,7 +10,6 @@ PODS: - FlutterMacOS DEPENDENCIES: - - file_saver (from `Flutter/ephemeral/.symlinks/plugins/file_saver/macos`) - FlutterMacOS (from `Flutter/ephemeral`) - path_provider_macos (from `Flutter/ephemeral/.symlinks/plugins/path_provider_macos/macos`) - rich_clipboard_macos (from `Flutter/ephemeral/.symlinks/plugins/rich_clipboard_macos/macos`) @@ -20,8 +17,6 @@ DEPENDENCIES: - wakelock_macos (from `Flutter/ephemeral/.symlinks/plugins/wakelock_macos/macos`) EXTERNAL SOURCES: - file_saver: - :path: Flutter/ephemeral/.symlinks/plugins/file_saver/macos FlutterMacOS: :path: Flutter/ephemeral path_provider_macos: @@ -34,7 +29,6 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/wakelock_macos/macos SPEC CHECKSUMS: - file_saver: 44e6fbf666677faf097302460e214e977fdd977b FlutterMacOS: 57701585bf7de1b3fc2bb61f6378d73bbdea8424 path_provider_macos: 3c0c3b4b0d4a76d2bf989a913c2de869c5641a19 rich_clipboard_macos: 43364b66b9dc69d203eb8dd6d758e2d12e02723c diff --git a/frontend/app_flowy/packages/appflowy_editor/example/pubspec.yaml b/frontend/app_flowy/packages/appflowy_editor/example/pubspec.yaml index 9f2fae24bd..5ba51433d6 100644 --- a/frontend/app_flowy/packages/appflowy_editor/example/pubspec.yaml +++ b/frontend/app_flowy/packages/appflowy_editor/example/pubspec.yaml @@ -40,7 +40,6 @@ dependencies: video_player: ^2.4.5 pod_player: 0.0.8 flutter_inappwebview: ^5.4.3+7 - file_saver: ^0.1.1 path_provider: ^2.0.11 dev_dependencies: diff --git a/frontend/app_flowy/packages/appflowy_editor/example/windows/flutter/generated_plugin_registrant.cc b/frontend/app_flowy/packages/appflowy_editor/example/windows/flutter/generated_plugin_registrant.cc index 391ef56268..4f7884874d 100644 --- a/frontend/app_flowy/packages/appflowy_editor/example/windows/flutter/generated_plugin_registrant.cc +++ b/frontend/app_flowy/packages/appflowy_editor/example/windows/flutter/generated_plugin_registrant.cc @@ -6,12 +6,9 @@ #include "generated_plugin_registrant.h" -#include #include void RegisterPlugins(flutter::PluginRegistry* registry) { - FileSaverPluginRegisterWithRegistrar( - registry->GetRegistrarForPlugin("FileSaverPlugin")); UrlLauncherWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("UrlLauncherWindows")); } diff --git a/frontend/app_flowy/packages/appflowy_editor/example/windows/flutter/generated_plugins.cmake b/frontend/app_flowy/packages/appflowy_editor/example/windows/flutter/generated_plugins.cmake index 047111654d..88b22e5c77 100644 --- a/frontend/app_flowy/packages/appflowy_editor/example/windows/flutter/generated_plugins.cmake +++ b/frontend/app_flowy/packages/appflowy_editor/example/windows/flutter/generated_plugins.cmake @@ -3,7 +3,6 @@ # list(APPEND FLUTTER_PLUGIN_LIST - file_saver url_launcher_windows ) From 6e0a191be1b13b91bfca42a40f28772d8cd17bc2 Mon Sep 17 00:00:00 2001 From: appflowy Date: Fri, 26 Aug 2022 11:51:42 +0800 Subject: [PATCH 050/106] chore: optimize rebuild column --- .../plugins/board/application/board_bloc.dart | 15 ++++++--- .../board/presentation/board_page.dart | 31 ++++++++++++++++--- .../example/lib/multi_board_list_example.dart | 4 +-- .../appflowy_board/lib/src/utils/log.dart | 2 +- .../appflowy_board/lib/src/widgets/board.dart | 13 ++++---- .../widgets/board_column/board_column.dart | 9 ++---- .../lib/src/widgets/board_data.dart | 1 - 7 files changed, 48 insertions(+), 27 deletions(-) diff --git a/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart b/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart index 65e185a089..1dfe5cd45f 100644 --- a/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart +++ b/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart @@ -88,12 +88,14 @@ class BoardBloc extends Bloc { didReceiveGridUpdate: (GridPB grid) { emit(state.copyWith(grid: Some(grid))); }, - didReceiveRows: (List rowInfos) { - emit(state.copyWith(rowInfos: rowInfos)); - }, didReceiveError: (FlowyError error) { emit(state.copyWith(noneOrError: some(error))); }, + didReceiveGroups: (List groups) { + emit(state.copyWith( + groupIds: groups.map((group) => group.groupId).toList(), + )); + }, ); }, ); @@ -170,6 +172,7 @@ class BoardBloc extends Bloc { boardController.addColumns(columns); initializeGroups(groups); + add(BoardEvent.didReceiveGroups(groups)); }, onDeletedGroup: (groupIds) { // @@ -223,6 +226,8 @@ class BoardEvent with _$BoardEvent { const factory BoardEvent.didReceiveGridUpdate( GridPB grid, ) = _DidReceiveGridUpdate; + const factory BoardEvent.didReceiveGroups(List groups) = + _DidReceiveGroups; } @freezed @@ -230,16 +235,16 @@ class BoardState with _$BoardState { const factory BoardState({ required String gridId, required Option grid, + required List groupIds, required Option editingRow, - required List rowInfos, required GridLoadingState loadingState, required Option noneOrError, }) = _BoardState; factory BoardState.initial(String gridId) => BoardState( - rowInfos: [], grid: none(), gridId: gridId, + groupIds: [], editingRow: none(), noneOrError: none(), loadingState: const _Loading(), diff --git a/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart b/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart index 45e9b574df..eeddb123d0 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart @@ -32,13 +32,15 @@ class BoardPage extends StatelessWidget { create: (context) => BoardBloc(view: view)..add(const BoardEvent.initial()), child: BlocBuilder( + buildWhen: (previous, current) => + previous.loadingState != current.loadingState, builder: (context, state) { return state.loadingState.map( loading: (_) => const Center(child: CircularProgressIndicator.adaptive()), finish: (result) { return result.successOrFail.fold( - (_) => BoardContent(), + (_) => const BoardContent(), (err) => FlowyErrorPage(err.toString()), ); }, @@ -49,23 +51,36 @@ class BoardPage extends StatelessWidget { } } -class BoardContent extends StatelessWidget { +class BoardContent extends StatefulWidget { + const BoardContent({Key? key}) : super(key: key); + + @override + State createState() => _BoardContentState(); +} + +class _BoardContentState extends State { + late ScrollController scrollController; final config = AFBoardConfig( columnBackgroundColor: HexColor.fromHex('#F7F8FC'), ); - BoardContent({Key? key}) : super(key: key); + @override + void initState() { + scrollController = ScrollController(); + super.initState(); + } @override Widget build(BuildContext context) { return BlocBuilder( + buildWhen: (previous, current) => previous.groupIds != current.groupIds, builder: (context, state) { return Container( color: Colors.white, child: Padding( padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 20), child: AFBoard( - scrollController: ScrollController(), + scrollController: scrollController, dataController: context.read().boardController, headerBuilder: _buildHeader, footBuilder: _buildFooter, @@ -85,6 +100,12 @@ class BoardContent extends StatelessWidget { ); } + @override + void dispose() { + scrollController.dispose(); + super.dispose(); + } + Widget _buildHeader( BuildContext context, AFBoardColumnHeaderData headerData) { return AppFlowyColumnHeader( @@ -159,7 +180,7 @@ class BoardContent extends StatelessWidget { ); return AppFlowyColumnItemCard( - key: ObjectKey(columnItem), + key: ValueKey(columnItem.id), margin: config.cardPadding, decoration: _makeBoxDecoration(context), child: BoardCard( diff --git a/frontend/app_flowy/packages/appflowy_board/example/lib/multi_board_list_example.dart b/frontend/app_flowy/packages/appflowy_board/example/lib/multi_board_list_example.dart index e571a0559b..170738b2e4 100644 --- a/frontend/app_flowy/packages/appflowy_board/example/lib/multi_board_list_example.dart +++ b/frontend/app_flowy/packages/appflowy_board/example/lib/multi_board_list_example.dart @@ -114,7 +114,7 @@ class _MultiBoardListExampleState extends State { return Align( alignment: Alignment.centerLeft, child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 60), + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 30), child: Text(item.s), ), ); @@ -124,7 +124,7 @@ class _MultiBoardListExampleState extends State { return Align( alignment: Alignment.centerLeft, child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 60), + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/utils/log.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/utils/log.dart index 9c23060b26..6f35ae4195 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/utils/log.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/utils/log.dart @@ -4,7 +4,7 @@ import 'package:flutter/material.dart'; const DART_LOG = "Dart_LOG"; class Log { - static const enableLog = false; + static const enableLog = true; static void info(String? message) { if (enableLog) { diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board.dart index dc7ae1c02f..0eeb47e747 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board.dart @@ -75,7 +75,7 @@ class AFBoard extends StatelessWidget { value: dataController, child: Consumer( builder: (context, notifier, child) { - return BoardContent( + return AFBoardContent( config: config, dataController: dataController, scrollController: scrollController, @@ -94,7 +94,7 @@ class AFBoard extends StatelessWidget { } } -class BoardContent extends StatefulWidget { +class AFBoardContent extends StatefulWidget { final ScrollController? scrollController; final OnDragStarted? onDragStarted; final OnReorder onReorder; @@ -118,7 +118,7 @@ class BoardContent extends StatefulWidget { final BoardPhantomController phantomController; - const BoardContent({ + const AFBoardContent({ required this.config, required this.onReorder, required this.delegate, @@ -137,12 +137,12 @@ class BoardContent extends StatefulWidget { super(key: key); @override - State createState() => _BoardContentState(); + State createState() => _AFBoardContentState(); } -class _BoardContentState extends State { +class _AFBoardContentState extends State { final GlobalKey _columnContainerOverlayKey = - GlobalKey(debugLabel: '$BoardContent overlay key'); + GlobalKey(debugLabel: '$AFBoardContent overlay key'); late BoardOverlayEntry _overlayEntry; @override @@ -215,6 +215,7 @@ class _BoardContentState extends State { child: Consumer( builder: (context, value, child) { final boardColumn = AFBoardColumnWidget( + key: ValueKey(columnData.id), margin: _marginFromIndex(columnIndex), itemMargin: widget.config.columnItemPadding, headerBuilder: _buildHeader, diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column.dart index ce053b5c79..43719b097c 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column.dart @@ -88,9 +88,7 @@ class AFBoardColumnWidget extends StatefulWidget { final Color backgroundColor; - final GlobalKey columnGlobalKey = GlobalKey(); - - AFBoardColumnWidget({ + const AFBoardColumnWidget({ Key? key, this.headerBuilder, this.footBuilder, @@ -140,7 +138,7 @@ class _AFBoardColumnWidgetState extends State { ); Widget reorderFlex = ReorderFlex( - key: widget.columnGlobalKey, + key: widget.key, scrollController: widget.scrollController, config: widget.config, onDragStarted: (index) { @@ -163,9 +161,6 @@ class _AFBoardColumnWidgetState extends State { children: children, ); - // reorderFlex = - // KeyedSubtree(key: widget.columnGlobalKey, child: reorderFlex); - return Container( margin: widget.margin, clipBehavior: Clip.hardEdge, diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_data.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_data.dart index 2cc853d6a5..75282d183c 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_data.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_data.dart @@ -196,7 +196,6 @@ class AFBoardDataController extends ChangeNotifier final index = columnDataController.items.indexWhere((item) => item.isPhantom); - assert(index != -1); if (index != -1) { if (index != newIndex) { Log.trace( From b52f618b1a9beb768745b526b255cc466ba0d64b Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Fri, 26 Aug 2022 14:06:11 +0800 Subject: [PATCH 051/106] test: add more test cases for image --- .../appflowy_editor/example/lib/main.dart | 5 +- .../src/render/image/image_node_builder.dart | 1 + .../src/render/image/image_node_widget.dart | 50 ++++++++++++- ...xt_handler.dart => backspace_handler.dart} | 16 +++- .../default_key_event_handlers.dart | 2 +- .../lib/src/service/toolbar_service.dart | 2 +- .../test/infra/test_editor.dart | 2 +- .../render/image/image_node_widget_test.dart | 11 +++ ..._test.dart => backspace_handler_test.dart} | 75 ++++++++++++++++++- 9 files changed, 152 insertions(+), 12 deletions(-) rename frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/{delete_text_handler.dart => backspace_handler.dart} (92%) rename frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/{delete_text_handler_test.dart => backspace_handler_test.dart} (83%) diff --git a/frontend/app_flowy/packages/appflowy_editor/example/lib/main.dart b/frontend/app_flowy/packages/appflowy_editor/example/lib/main.dart index e72739e246..0184138fb5 100644 --- a/frontend/app_flowy/packages/appflowy_editor/example/lib/main.dart +++ b/frontend/app_flowy/packages/appflowy_editor/example/lib/main.dart @@ -45,6 +45,7 @@ class _MyHomePageState extends State { @override Widget build(BuildContext context) { return Scaffold( + extendBodyBehindAppBar: true, body: Container( alignment: Alignment.topCenter, child: _buildEditor(context), @@ -92,8 +93,8 @@ class _MyHomePageState extends State { ..handler = (message) { debugPrint(message); }; - return Container( - padding: const EdgeInsets.all(20), + return SizedBox( + width: MediaQuery.of(context).size.width, child: AppFlowyEditor( editorState: _editorState, ), diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_builder.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_builder.dart index 796e96c250..ad3cf19d53 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_builder.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_builder.dart @@ -17,6 +17,7 @@ class ImageNodeBuilder extends NodeWidgetBuilder { } return ImageNodeWidget( key: context.node.key, + node: context.node, src: src, width: width, alignment: _textToAlignment(align), diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_widget.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_widget.dart index 2cc0916b66..ca52a14db1 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_widget.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_widget.dart @@ -1,10 +1,17 @@ +import 'dart:math'; + +import 'package:appflowy_editor/src/document/node.dart'; +import 'package:appflowy_editor/src/document/position.dart'; +import 'package:appflowy_editor/src/document/selection.dart'; import 'package:appflowy_editor/src/infra/flowy_svg.dart'; import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart'; +import 'package:appflowy_editor/src/render/selection/selectable.dart'; import 'package:flutter/material.dart'; class ImageNodeWidget extends StatefulWidget { const ImageNodeWidget({ Key? key, + required this.node, required this.src, this.width, required this.alignment, @@ -14,6 +21,7 @@ class ImageNodeWidget extends StatefulWidget { required this.onResize, }) : super(key: key); + final Node node; final String src; final double? width; final Alignment alignment; @@ -26,7 +34,7 @@ class ImageNodeWidget extends StatefulWidget { State createState() => _ImageNodeWidgetState(); } -class _ImageNodeWidgetState extends State { +class _ImageNodeWidgetState extends State with Selectable { double? _imageWidth; double _initial = 0; double _distance = 0; @@ -42,7 +50,8 @@ class _ImageNodeWidgetState extends State { _imageWidth = widget.width; _imageStreamListener = ImageStreamListener( (image, _) { - _imageWidth = image.image.width.toDouble(); + _imageWidth = + min(defaultMaxTextNodeWidth, image.image.width.toDouble()); }, ); } @@ -64,6 +73,43 @@ class _ImageNodeWidgetState extends State { ); } + @override + Position start() { + return Position(path: widget.node.path, offset: 0); + } + + @override + Position end() { + return Position(path: widget.node.path, offset: 1); + } + + @override + Position getPositionInOffset(Offset start) { + return end(); + } + + @override + Rect? getCursorRectInPosition(Position position) { + return null; + } + + @override + List getRectsInSelection(Selection selection) { + final renderBox = context.findRenderObject() as RenderBox; + return [Offset.zero & renderBox.size]; + } + + @override + Selection getSelectionInRange(Offset start, Offset end) { + return Selection(start: this.start(), end: this.end()); + } + + @override + Offset localToGlobal(Offset offset) { + final renderBox = context.findRenderObject() as RenderBox; + return renderBox.localToGlobal(offset); + } + Widget _buildNetworkImage(BuildContext context) { return Align( alignment: widget.alignment, diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/delete_text_handler.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/backspace_handler.dart similarity index 92% rename from frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/delete_text_handler.dart rename to frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/backspace_handler.dart index d2a3d51e64..ebe1eb3bb8 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/delete_text_handler.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/backspace_handler.dart @@ -11,10 +11,16 @@ KeyEventResult _handleBackspace(EditorState editorState, RawKeyEvent event) { var nodes = editorState.service.selectionService.currentSelectedNodes; nodes = selection.isBackward ? nodes : nodes.reversed.toList(growable: false); selection = selection.isBackward ? selection : selection.reversed; - // make sure all nodes is [TextNode]. final textNodes = nodes.whereType().toList(); + final nonTextNodes = + nodes.where((node) => node is! TextNode).toList(growable: false); final transactionBuilder = TransactionBuilder(editorState); + + if (nonTextNodes.isNotEmpty) { + transactionBuilder.deleteNodes(nonTextNodes); + } + if (textNodes.length == 1) { final textNode = textNodes.first; final index = textNode.delta.prevRunePosition(selection.start.offset); @@ -68,7 +74,9 @@ KeyEventResult _handleBackspace(EditorState editorState, RawKeyEvent event) { } } } else { - _deleteNodes(transactionBuilder, textNodes, selection); + if (textNodes.isNotEmpty) { + _deleteTextNodes(transactionBuilder, textNodes, selection); + } } if (transactionBuilder.operations.isNotEmpty) { @@ -121,7 +129,7 @@ KeyEventResult _handleDelete(EditorState editorState, RawKeyEvent event) { } } } else { - _deleteNodes(transactionBuilder, textNodes, selection); + _deleteTextNodes(transactionBuilder, textNodes, selection); } transactionBuilder.commit(); @@ -129,7 +137,7 @@ KeyEventResult _handleDelete(EditorState editorState, RawKeyEvent event) { return KeyEventResult.handled; } -void _deleteNodes(TransactionBuilder transactionBuilder, +void _deleteTextNodes(TransactionBuilder transactionBuilder, List textNodes, Selection selection) { final first = textNodes.first; final last = textNodes.last; diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/default_key_event_handlers.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/default_key_event_handlers.dart index 468eda4e98..a8cbdee3ab 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/default_key_event_handlers.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/default_key_event_handlers.dart @@ -1,6 +1,6 @@ import 'package:appflowy_editor/src/service/internal_key_event_handlers/arrow_keys_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/delete_text_handler.dart'; +import 'package:appflowy_editor/src/service/internal_key_event_handlers/backspace_handler.dart'; import 'package:appflowy_editor/src/service/internal_key_event_handlers/enter_without_shift_in_text_node_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/slash_handler.dart'; diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/toolbar_service.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/toolbar_service.dart index e26a186387..8dba7dcb8e 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/toolbar_service.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/toolbar_service.dart @@ -90,7 +90,7 @@ class _FlowyToolbarState extends State .where((item) => item.validator(widget.editorState)) .toList(growable: false) ..sort((a, b) => a.type.compareTo(b.type)); - if (items.isEmpty) { + if (filterItems.isEmpty) { return []; } final List dividedItems = [filterItems.first]; diff --git a/frontend/app_flowy/packages/appflowy_editor/test/infra/test_editor.dart b/frontend/app_flowy/packages/appflowy_editor/test/infra/test_editor.dart index a815d91875..e3a5a7d0c5 100644 --- a/frontend/app_flowy/packages/appflowy_editor/test/infra/test_editor.dart +++ b/frontend/app_flowy/packages/appflowy_editor/test/infra/test_editor.dart @@ -80,7 +80,7 @@ class EditorWidgetTester { } else { _editorState.service.selectionService.updateSelection(selection); } - await tester.pumpAndSettle(); + await tester.pump(const Duration(milliseconds: 200)); expect(_editorState.service.selectionService.currentSelection.value, selection); diff --git a/frontend/app_flowy/packages/appflowy_editor/test/render/image/image_node_widget_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/render/image/image_node_widget_test.dart index d2f774d33f..a566b7ec07 100644 --- a/frontend/app_flowy/packages/appflowy_editor/test/render/image/image_node_widget_test.dart +++ b/frontend/app_flowy/packages/appflowy_editor/test/render/image/image_node_widget_test.dart @@ -1,3 +1,6 @@ +import 'dart:collection'; + +import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/src/render/image/image_node_widget.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; @@ -20,6 +23,14 @@ void main() async { final widget = ImageNodeWidget( src: src, + node: Node( + type: 'image', + children: LinkedList(), + attributes: { + 'image_src': src, + 'align': 'center', + }, + ), alignment: Alignment.center, onCopy: () { onCopyHit = true; diff --git a/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/delete_text_handler_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/backspace_handler_test.dart similarity index 83% rename from frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/delete_text_handler_test.dart rename to frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/backspace_handler_test.dart index 1e7bf4e842..76843c4300 100644 --- a/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/delete_text_handler_test.dart +++ b/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/backspace_handler_test.dart @@ -1,7 +1,9 @@ import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_editor/src/render/image/image_node_widget.dart'; import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:network_image_mock/network_image_mock.dart'; import '../../infra/test_editor.dart'; void main() async { @@ -9,7 +11,7 @@ void main() async { TestWidgetsFlutterBinding.ensureInitialized(); }); - group('delete_text_handler.dart', () { + group('backspace_handler.dart', () { testWidgets('Presses backspace key in empty document', (tester) async { // Before // @@ -167,6 +169,77 @@ void main() async { testWidgets('Presses delete key in styled text (quote)', (tester) async { await _deleteStyledTextByDelete(tester, StyleKey.quote); }); + + // Before + // + // Welcome to Appflowy 😁 + // Welcome to Appflowy 😁 + // [Image] + // Welcome to Appflowy 😁 + // Welcome to Appflowy 😁 + // + // After + // + // Welcome to Appflowy 😁 + // Welcome to Appflowy 😁 + // + testWidgets('Deletes the image surrounded by text', (tester) async { + mockNetworkImagesFor(() async { + const text = 'Welcome to Appflowy 😁'; + const src = 'https://s1.ax1x.com/2022/08/26/v2sSbR.jpg'; + final editor = tester.editor + ..insertTextNode(text) + ..insertTextNode(text) + ..insertImageNode(src) + ..insertTextNode(text) + ..insertTextNode(text); + await editor.startTesting(); + + expect(editor.documentLength, 5); + expect(find.byType(ImageNodeWidget), findsOneWidget); + + await editor.updateSelection( + Selection( + start: Position(path: [1], offset: 0), + end: Position(path: [3], offset: text.length), + ), + ); + + await editor.pressLogicKey(LogicalKeyboardKey.backspace); + expect(editor.documentLength, 3); + expect(find.byType(ImageNodeWidget), findsNothing); + expect( + editor.documentSelection, + Selection.single(path: [1], startOffset: 0), + ); + }); + }); + + testWidgets('Deletes the first image', (tester) async { + mockNetworkImagesFor(() async { + const text = 'Welcome to Appflowy 😁'; + const src = 'https://s1.ax1x.com/2022/08/26/v2sSbR.jpg'; + final editor = tester.editor + ..insertImageNode(src) + ..insertTextNode(text) + ..insertTextNode(text); + await editor.startTesting(); + + expect(editor.documentLength, 3); + expect(find.byType(ImageNodeWidget), findsOneWidget); + + await editor.updateSelection( + Selection( + start: Position(path: [0], offset: 0), + end: Position(path: [0], offset: 1), + ), + ); + + await editor.pressLogicKey(LogicalKeyboardKey.backspace); + expect(editor.documentLength, 2); + expect(find.byType(ImageNodeWidget), findsNothing); + }); + }); } Future _deleteStyledTextByBackspace( From 01328442a02b39ffe8cfc28cdae23b676b3d4c10 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Fri, 26 Aug 2022 16:00:20 +0800 Subject: [PATCH 052/106] fix: could not delete the image when the selection is multiple --- .../src/render/image/image_node_widget.dart | 6 +- .../backspace_handler.dart | 3 + .../backspace_handler_test.dart | 94 ++++++++++++++----- 3 files changed, 81 insertions(+), 22 deletions(-) diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_widget.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_widget.dart index ca52a14db1..316202b1c7 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_widget.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_widget.dart @@ -101,7 +101,11 @@ class _ImageNodeWidgetState extends State with Selectable { @override Selection getSelectionInRange(Offset start, Offset end) { - return Selection(start: this.start(), end: this.end()); + if (start <= end) { + return Selection(start: this.start(), end: this.end()); + } else { + return Selection(start: this.end(), end: this.start()); + } } @override diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/backspace_handler.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/backspace_handler.dart index ebe1eb3bb8..0eeaf654de 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/backspace_handler.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/backspace_handler.dart @@ -80,6 +80,9 @@ KeyEventResult _handleBackspace(EditorState editorState, RawKeyEvent event) { } if (transactionBuilder.operations.isNotEmpty) { + if (nonTextNodes.isNotEmpty) { + transactionBuilder.afterSelection = Selection.collapsed(selection.start); + } transactionBuilder.commit(); } diff --git a/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/backspace_handler_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/backspace_handler_test.dart index 76843c4300..1976ec3250 100644 --- a/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/backspace_handler_test.dart +++ b/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/backspace_handler_test.dart @@ -215,30 +215,82 @@ void main() async { }); }); - testWidgets('Deletes the first image', (tester) async { - mockNetworkImagesFor(() async { - const text = 'Welcome to Appflowy 😁'; - const src = 'https://s1.ax1x.com/2022/08/26/v2sSbR.jpg'; - final editor = tester.editor - ..insertImageNode(src) - ..insertTextNode(text) - ..insertTextNode(text); - await editor.startTesting(); + testWidgets('Deletes the first image, and selection is backward', + (tester) async { + await _deleteFirstImage(tester, true); + }); - expect(editor.documentLength, 3); - expect(find.byType(ImageNodeWidget), findsOneWidget); + testWidgets('Deletes the first image, and selection is not backward', + (tester) async { + await _deleteFirstImage(tester, false); + }); - await editor.updateSelection( - Selection( - start: Position(path: [0], offset: 0), - end: Position(path: [0], offset: 1), - ), - ); + testWidgets('Deletes the last image and selection is backward', + (tester) async { + await _deleteLastImage(tester, true); + }); - await editor.pressLogicKey(LogicalKeyboardKey.backspace); - expect(editor.documentLength, 2); - expect(find.byType(ImageNodeWidget), findsNothing); - }); + testWidgets('Deletes the last image and selection is not backward', + (tester) async { + await _deleteLastImage(tester, false); + }); +} + +Future _deleteFirstImage(WidgetTester tester, bool isBackward) async { + mockNetworkImagesFor(() async { + const text = 'Welcome to Appflowy 😁'; + const src = 'https://s1.ax1x.com/2022/08/26/v2sSbR.jpg'; + final editor = tester.editor + ..insertImageNode(src) + ..insertTextNode(text) + ..insertTextNode(text); + await editor.startTesting(); + + expect(editor.documentLength, 3); + expect(find.byType(ImageNodeWidget), findsOneWidget); + + final start = Position(path: [0], offset: 0); + final end = Position(path: [1], offset: 1); + await editor.updateSelection( + Selection( + start: isBackward ? start : end, + end: isBackward ? end : start, + ), + ); + + await editor.pressLogicKey(LogicalKeyboardKey.backspace); + expect(editor.documentLength, 2); + expect(find.byType(ImageNodeWidget), findsNothing); + expect(editor.documentSelection, Selection.collapsed(start)); + }); +} + +Future _deleteLastImage(WidgetTester tester, bool isBackward) async { + mockNetworkImagesFor(() async { + const text = 'Welcome to Appflowy 😁'; + const src = 'https://s1.ax1x.com/2022/08/26/v2sSbR.jpg'; + final editor = tester.editor + ..insertTextNode(text) + ..insertTextNode(text) + ..insertImageNode(src); + await editor.startTesting(); + + expect(editor.documentLength, 3); + expect(find.byType(ImageNodeWidget), findsOneWidget); + + final start = Position(path: [1], offset: 0); + final end = Position(path: [2], offset: 1); + await editor.updateSelection( + Selection( + start: isBackward ? start : end, + end: isBackward ? end : start, + ), + ); + + await editor.pressLogicKey(LogicalKeyboardKey.backspace); + expect(editor.documentLength, 2); + expect(find.byType(ImageNodeWidget), findsNothing); + expect(editor.documentSelection, Selection.collapsed(start)); }); } From aba0f946dd24ec90c9c671130f6298f56bc582fb Mon Sep 17 00:00:00 2001 From: appflowy Date: Fri, 26 Aug 2022 16:07:51 +0800 Subject: [PATCH 053/106] chore: do not build the cell if its fieldId equal to the corresponding group field id --- .../plugins/board/application/board_bloc.dart | 33 +++++---- .../board/application/card/card_bloc.dart | 73 +++++++++++-------- .../board/application/group_controller.dart | 12 +-- .../board/presentation/board_page.dart | 7 +- .../card/board_select_option_cell.dart | 5 +- .../plugins/board/presentation/card/card.dart | 20 ++++- .../flowy-grid/src/services/grid_editor.rs | 49 ++++++------- .../src/services/grid_view_editor.rs | 11 ++- .../src/services/grid_view_manager.rs | 22 +++--- 9 files changed, 133 insertions(+), 99 deletions(-) diff --git a/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart b/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart index 1dfe5cd45f..d132fe1ea0 100644 --- a/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart +++ b/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart @@ -165,7 +165,7 @@ class BoardBloc extends Bloc { return AFBoardColumnData( id: group.groupId, name: group.desc, - items: _buildRows(group.rows), + items: _buildRows(group), customData: group, ); }).toList(); @@ -196,9 +196,9 @@ class BoardBloc extends Bloc { ); } - List _buildRows(List rows) { - final items = rows.map((row) { - return BoardColumnItem(row: row); + List _buildRows(GroupPB group) { + final items = group.rows.map((row) { + return BoardColumnItem(row: row, fieldId: group.fieldId); }).toList(); return [...items]; @@ -284,7 +284,9 @@ class GridFieldEquatable extends Equatable { class BoardColumnItem extends AFColumnItem { final RowPB row; - BoardColumnItem({required this.row}); + final String fieldId; + + BoardColumnItem({required this.row, required this.fieldId}); @override String get id => row.id; @@ -301,22 +303,27 @@ class GroupControllerDelegateImpl extends GroupControllerDelegate { GroupControllerDelegateImpl(this.controller); @override - void insertRow(String groupId, RowPB row, int? index) { - final item = BoardColumnItem(row: row); + void insertRow(GroupPB group, RowPB row, int? index) { + final item = BoardColumnItem(row: row, fieldId: group.fieldId); if (index != null) { - controller.insertColumnItem(groupId, index, item); + controller.insertColumnItem(group.groupId, index, item); } else { - controller.addColumnItem(groupId, item); + controller.addColumnItem(group.groupId, item); } } @override - void removeRow(String groupId, String rowId) { - controller.removeColumnItem(groupId, rowId); + void removeRow(GroupPB group, String rowId) { + controller.removeColumnItem(group.groupId, rowId); } @override - void updateRow(String groupId, RowPB row) { - controller.updateColumnItem(groupId, BoardColumnItem(row: row)); + void updateRow(GroupPB group, RowPB row) { + controller.updateColumnItem( + group.groupId, + BoardColumnItem( + row: row, + fieldId: group.fieldId, + )); } } diff --git a/frontend/app_flowy/lib/plugins/board/application/card/card_bloc.dart b/frontend/app_flowy/lib/plugins/board/application/card/card_bloc.dart index ab6aeacfcc..7f1ae766e6 100644 --- a/frontend/app_flowy/lib/plugins/board/application/card/card_bloc.dart +++ b/frontend/app_flowy/lib/plugins/board/application/card/card_bloc.dart @@ -4,7 +4,6 @@ import 'package:app_flowy/plugins/grid/application/row/row_cache.dart'; import 'package:app_flowy/plugins/grid/application/row/row_service.dart'; import 'package:equatable/equatable.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/block_entities.pb.dart'; -import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'dart:async'; @@ -14,10 +13,12 @@ import 'card_data_controller.dart'; part 'card_bloc.freezed.dart'; class BoardCardBloc extends Bloc { + final String fieldId; final RowFFIService _rowService; final CardDataController _dataController; BoardCardBloc({ + required this.fieldId, required String gridId, required CardDataController dataController, }) : _rowService = RowFFIService( @@ -25,22 +26,22 @@ class BoardCardBloc extends Bloc { blockId: dataController.rowPB.blockId, ), _dataController = dataController, - super(BoardCardState.initial( - dataController.rowPB, dataController.loadData())) { + super( + BoardCardState.initial( + dataController.rowPB, + _makeCells(fieldId, dataController.loadData()), + ), + ) { on( (event, emit) async { - await event.map( - initial: (_InitialRow value) async { + await event.when( + initial: () async { await _startListening(); }, - didReceiveCells: (_DidReceiveCells value) async { - final cells = value.gridCellMap.values - .map((e) => GridCellEquatable(e.field)) - .toList(); + didReceiveCells: (cells, reason) async { emit(state.copyWith( - gridCellMap: value.gridCellMap, - cells: UnmodifiableListView(cells), - changeReason: value.reason, + cells: cells, + changeReason: reason, )); }, ); @@ -58,7 +59,7 @@ class BoardCardBloc extends Bloc { return RowInfo( gridId: _rowService.gridId, fields: UnmodifiableListView( - state.cells.map((cell) => cell._field).toList(), + state.cells.map((cell) => cell.identifier.field).toList(), ), rowPB: state.rowPB, ); @@ -66,8 +67,9 @@ class BoardCardBloc extends Bloc { Future _startListening() async { _dataController.addListener( - onRowChanged: (cells, reason) { + onRowChanged: (cellMap, reason) { if (!isClosed) { + final cells = _makeCells(fieldId, cellMap); add(BoardCardEvent.didReceiveCells(cells, reason)); } }, @@ -75,42 +77,49 @@ class BoardCardBloc extends Bloc { } } +UnmodifiableListView _makeCells( + String fieldId, GridCellMap originalCellMap) { + List cells = []; + for (final entry in originalCellMap.entries) { + if (entry.value.fieldId != fieldId) { + cells.add(BoardCellEquatable(entry.value)); + } + } + return UnmodifiableListView(cells); +} + @freezed class BoardCardEvent with _$BoardCardEvent { const factory BoardCardEvent.initial() = _InitialRow; const factory BoardCardEvent.didReceiveCells( - GridCellMap gridCellMap, RowsChangedReason reason) = _DidReceiveCells; + UnmodifiableListView cells, + RowsChangedReason reason, + ) = _DidReceiveCells; } @freezed class BoardCardState with _$BoardCardState { const factory BoardCardState({ required RowPB rowPB, - required GridCellMap gridCellMap, - required UnmodifiableListView cells, + required UnmodifiableListView cells, RowsChangedReason? changeReason, }) = _BoardCardState; - factory BoardCardState.initial(RowPB rowPB, GridCellMap cellDataMap) => - BoardCardState( - rowPB: rowPB, - gridCellMap: cellDataMap, - cells: UnmodifiableListView( - cellDataMap.values.map((e) => GridCellEquatable(e.field)).toList(), - ), - ); + factory BoardCardState.initial( + RowPB rowPB, UnmodifiableListView cells) => + BoardCardState(rowPB: rowPB, cells: cells); } -class GridCellEquatable extends Equatable { - final FieldPB _field; +class BoardCellEquatable extends Equatable { + final GridCellIdentifier identifier; - const GridCellEquatable(FieldPB field) : _field = field; + const BoardCellEquatable(this.identifier); @override List get props => [ - _field.id, - _field.fieldType, - _field.visibility, - _field.width, + identifier.field.id, + identifier.field.fieldType, + identifier.field.visibility, + identifier.field.width, ]; } diff --git a/frontend/app_flowy/lib/plugins/board/application/group_controller.dart b/frontend/app_flowy/lib/plugins/board/application/group_controller.dart index b0a89baaa3..0ca71ff5f6 100644 --- a/frontend/app_flowy/lib/plugins/board/application/group_controller.dart +++ b/frontend/app_flowy/lib/plugins/board/application/group_controller.dart @@ -7,9 +7,9 @@ import 'group_listener.dart'; typedef OnGroupError = void Function(FlowyError); abstract class GroupControllerDelegate { - void removeRow(String groupId, String rowId); - void insertRow(String groupId, RowPB row, int? index); - void updateRow(String groupId, RowPB row); + void removeRow(GroupPB group, String rowId); + void insertRow(GroupPB group, RowPB row, int? index); + void updateRow(GroupPB group, RowPB row); } class GroupController { @@ -37,7 +37,7 @@ class GroupController { (GroupChangesetPB changeset) { for (final deletedRow in changeset.deletedRows) { group.rows.removeWhere((rowPB) => rowPB.id == deletedRow); - delegate.removeRow(group.groupId, deletedRow); + delegate.removeRow(group, deletedRow); } for (final insertedRow in changeset.insertedRows) { @@ -51,7 +51,7 @@ class GroupController { } delegate.insertRow( - group.groupId, + group, insertedRow.row, index, ); @@ -66,7 +66,7 @@ class GroupController { group.rows[index] = updatedRow; } - delegate.updateRow(group.groupId, updatedRow); + delegate.updateRow(group, updatedRow); } }, (err) => Log.error(err), diff --git a/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart b/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart index eeddb123d0..bd1bf54296 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart @@ -73,7 +73,8 @@ class _BoardContentState extends State { @override Widget build(BuildContext context) { return BlocBuilder( - buildWhen: (previous, current) => previous.groupIds != current.groupIds, + buildWhen: (previous, current) => + previous.groupIds.length != current.groupIds.length, builder: (context, state) { return Container( color: Colors.white, @@ -159,7 +160,8 @@ class _BoardContentState extends State { AFBoardColumnData column, AFColumnItem columnItem, ) { - final rowPB = (columnItem as BoardColumnItem).row; + final boardColumnItem = columnItem as BoardColumnItem; + final rowPB = boardColumnItem.row; final rowCache = context.read().getRowCache(rowPB.blockId); /// Return placeholder widget if the rowCache is null. @@ -186,6 +188,7 @@ class _BoardContentState extends State { child: BoardCard( gridId: gridId, groupId: column.id, + fieldId: boardColumnItem.fieldId, isEditing: isEditing, cellBuilder: cellBuilder, dataController: cardController, diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/board_select_option_cell.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/board_select_option_cell.dart index f75de47651..b2383bd937 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/board_select_option_cell.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/board_select_option_cell.dart @@ -35,8 +35,9 @@ class _BoardSelectOptionCellState extends State { return BlocProvider.value( value: _cellBloc, child: BlocBuilder( - buildWhen: (previous, current) => - previous.selectedOptions != current.selectedOptions, + buildWhen: (previous, current) { + return previous.selectedOptions != current.selectedOptions; + }, builder: (context, state) { if (state.selectedOptions .where((element) => element.id == widget.groupId) diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart index 65c7d3dade..dfb8149c9e 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart @@ -15,6 +15,7 @@ typedef OnEndEditing = void Function(String rowId); class BoardCard extends StatefulWidget { final String gridId; final String groupId; + final String fieldId; final bool isEditing; final CardDataController dataController; final BoardCellBuilder cellBuilder; @@ -24,6 +25,7 @@ class BoardCard extends StatefulWidget { const BoardCard({ required this.gridId, required this.groupId, + required this.fieldId, required this.isEditing, required this.dataController, required this.cellBuilder, @@ -43,6 +45,7 @@ class _BoardCardState extends State { void initState() { _cardBloc = BoardCardBloc( gridId: widget.gridId, + fieldId: widget.fieldId, dataController: widget.dataController, )..add(const BoardCardEvent.initial()); super.initState(); @@ -53,6 +56,9 @@ class _BoardCardState extends State { return BlocProvider.value( value: _cardBloc, child: BlocBuilder( + buildWhen: (previous, current) { + return previous.cells.length != current.cells.length; + }, builder: (context, state) { return BoardCardContainer( accessoryBuilder: (context) { @@ -62,7 +68,10 @@ class _BoardCardState extends State { widget.openCard(context); }, child: Column( - children: _makeCells(context, state.gridCellMap), + children: _makeCells( + context, + state.cells.map((cell) => cell.identifier).toList(), + ), ), ); }, @@ -70,9 +79,12 @@ class _BoardCardState extends State { ); } - List _makeCells(BuildContext context, GridCellMap cellMap) { - return cellMap.values.map( - (cellId) { + List _makeCells( + BuildContext context, + List cells, + ) { + return cells.map( + (GridCellIdentifier cellId) { final child = widget.cellBuilder.buildCell(widget.groupId, cellId); return Padding( padding: const EdgeInsets.only(left: 4, right: 4, top: 6), diff --git a/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs b/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs index cdc6cf6e6b..9ae7918955 100644 --- a/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs +++ b/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs @@ -18,7 +18,7 @@ use flowy_sync::client_grid::{GridRevisionChangeset, GridRevisionPad, JsonDeseri use flowy_sync::entities::revision::Revision; use flowy_sync::errors::CollaborateResult; use flowy_sync::util::make_text_delta_from_revisions; -use lib_infra::future::FutureResult; +use lib_infra::future::{wrap_future, FutureResult}; use std::collections::HashMap; use std::sync::Arc; @@ -591,34 +591,33 @@ impl GridRevisionEditor { match self.block_manager.get_row_rev(&from_row_id).await? { None => tracing::warn!("Move row failed, can not find the row:{}", from_row_id), Some(row_rev) => { - if let Some(row_changeset) = self - .view_manager - .move_group_row(row_rev, to_group_id, to_row_id.clone()) - .await - { - tracing::trace!("Move group row cause row data changed: {:?}", row_changeset); + let block_manager = self.block_manager.clone(); + self.view_manager + .move_group_row(row_rev, to_group_id, to_row_id.clone(), |row_changeset| { + wrap_future(async move { + tracing::trace!("Move group row cause row data changed: {:?}", row_changeset); + let cell_changesets = row_changeset + .cell_by_field_id + .into_iter() + .map(|(field_id, cell_rev)| CellChangesetPB { + grid_id: view_id.clone(), + row_id: row_changeset.row_id.clone(), + field_id, + content: cell_rev.data, + }) + .collect::>(); - let cell_changesets = row_changeset - .cell_by_field_id - .into_iter() - .map(|(field_id, cell_rev)| CellChangesetPB { - grid_id: view_id.clone(), - row_id: row_changeset.row_id.clone(), - field_id, - content: cell_rev.data, + for cell_changeset in cell_changesets { + match block_manager.update_cell(cell_changeset).await { + Ok(_) => {} + Err(e) => tracing::error!("Apply cell changeset error:{:?}", e), + } + } }) - .collect::>(); - - for cell_changeset in cell_changesets { - match self.block_manager.update_cell(cell_changeset).await { - Ok(_) => {} - Err(e) => tracing::error!("Apply cell changeset error:{:?}", e), - } - } - } + }) + .await?; } } - Ok(()) } diff --git a/frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs b/frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs index 6a04e18815..8b89aa72ae 100644 --- a/frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs +++ b/frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs @@ -140,8 +140,8 @@ impl GridViewRevisionEditor { row_changeset: &mut RowChangeset, to_group_id: &str, to_row_id: Option, - ) { - if let Some(changesets) = self + ) -> Vec { + match self .group_service .write() .await @@ -150,9 +150,8 @@ impl GridViewRevisionEditor { }) .await { - for changeset in changesets { - self.notify_did_update_group(changeset).await; - } + None => vec![], + Some(changesets) => changesets, } } /// Only call once after grid view editor initialized @@ -266,7 +265,7 @@ impl GridViewRevisionEditor { Ok(()) } - async fn notify_did_update_group(&self, changeset: GroupChangesetPB) { + pub async fn notify_did_update_group(&self, changeset: GroupChangesetPB) { send_dart_notification(&changeset.group_id, GridNotification::DidUpdateGroup) .payload(changeset) .send(); diff --git a/frontend/rust-lib/flowy-grid/src/services/grid_view_manager.rs b/frontend/rust-lib/flowy-grid/src/services/grid_view_manager.rs index 657058b31f..8c9893e4ef 100644 --- a/frontend/rust-lib/flowy-grid/src/services/grid_view_manager.rs +++ b/frontend/rust-lib/flowy-grid/src/services/grid_view_manager.rs @@ -134,19 +134,23 @@ impl GridViewManager { row_rev: Arc, to_group_id: String, to_row_id: Option, - ) -> Option { + with_row_changeset: impl FnOnce(RowChangeset) -> AFFuture<()>, + ) -> FlowyResult<()> { let mut row_changeset = RowChangeset::new(row_rev.id.clone()); - for view_editor in self.view_editors.iter() { - view_editor - .move_group_row(&row_rev, &mut row_changeset, &to_group_id, to_row_id.clone()) - .await; + let view_editor = self.get_default_view_editor().await?; + let group_changesets = view_editor + .move_group_row(&row_rev, &mut row_changeset, &to_group_id, to_row_id.clone()) + .await; + + if row_changeset.is_empty() == false { + with_row_changeset(row_changeset).await; } - if row_changeset.is_empty() { - None - } else { - Some(row_changeset) + for group_changeset in group_changesets { + view_editor.notify_did_update_group(group_changeset).await; } + + Ok(()) } pub(crate) async fn did_update_field(&self, field_id: &str) -> FlowyResult<()> { From d6162159aa2d621dc5b575f2bb69eaf385924e8d Mon Sep 17 00:00:00 2001 From: appflowy Date: Fri, 26 Aug 2022 21:42:59 +0800 Subject: [PATCH 054/106] chore: scroll to bottom when create new card --- .../plugins/board/application/board_bloc.dart | 28 ++++++--- .../appflowy_board/lib/src/widgets/board.dart | 44 ++++++++++---- .../widgets/board_column/board_column.dart | 15 +++-- .../src/widgets/reorder_flex/drag_target.dart | 12 ++-- .../reorder_flex/drag_target_interceptor.dart | 10 +--- .../widgets/reorder_flex/reorder_flex.dart | 57 +++++++++++++++++-- 6 files changed, 122 insertions(+), 44 deletions(-) diff --git a/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart b/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart index d132fe1ea0..48d16e854d 100644 --- a/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart +++ b/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart @@ -198,7 +198,10 @@ class BoardBloc extends Bloc { List _buildRows(GroupPB group) { final items = group.rows.map((row) { - return BoardColumnItem(row: row, fieldId: group.fieldId); + return BoardColumnItem( + row: row, + fieldId: group.fieldId, + ); }).toList(); return [...items]; @@ -286,17 +289,18 @@ class BoardColumnItem extends AFColumnItem { final String fieldId; - BoardColumnItem({required this.row, required this.fieldId}); + final bool requestFocus; + + BoardColumnItem({ + required this.row, + required this.fieldId, + this.requestFocus = false, + }); @override String get id => row.id; } -class CreateCardItem extends AFColumnItem { - @override - String get id => '$CreateCardItem'; -} - class GroupControllerDelegateImpl extends GroupControllerDelegate { final AFBoardDataController controller; @@ -304,10 +308,18 @@ class GroupControllerDelegateImpl extends GroupControllerDelegate { @override void insertRow(GroupPB group, RowPB row, int? index) { - final item = BoardColumnItem(row: row, fieldId: group.fieldId); if (index != null) { + final item = BoardColumnItem( + row: row, + fieldId: group.fieldId, + ); controller.insertColumnItem(group.groupId, index, item); } else { + final item = BoardColumnItem( + row: row, + fieldId: group.fieldId, + requestFocus: true, + ); controller.addColumnItem(group.groupId, item); } } diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board.dart index 0eeb47e747..ccb15d3830 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board.dart @@ -1,3 +1,5 @@ +import 'dart:collection'; + import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'board_column/board_column.dart'; @@ -141,18 +143,22 @@ class AFBoardContent extends StatefulWidget { } class _AFBoardContentState extends State { + late _BoardColumnState columnState; + final GlobalKey _columnContainerOverlayKey = GlobalKey(debugLabel: '$AFBoardContent overlay key'); late BoardOverlayEntry _overlayEntry; @override void initState() { + columnState = _BoardColumnState(); _overlayEntry = BoardOverlayEntry( builder: (BuildContext context) { final interceptor = OverlappingDragTargetInterceptor( reorderFlexId: widget.dataController.identifier, acceptedReorderFlexId: widget.dataController.columnIds, delegate: widget.delegate, + columnKeys: UnmodifiableMapView(columnState.columnKeys), ); final reorderFlex = ReorderFlex( @@ -165,7 +171,7 @@ class _AFBoardContentState extends State { dataSource: widget.dataController, direction: Axis.horizontal, interceptor: interceptor, - children: _buildColumns(interceptor.columnKeys), + children: _buildColumns(), ); return Stack( @@ -197,7 +203,7 @@ class _AFBoardContentState extends State { ); } - List _buildColumns(List columnKeys) { + List _buildColumns() { final List children = widget.dataController.columnDatas.asMap().entries.map( (item) { @@ -222,21 +228,14 @@ class _AFBoardContentState extends State { footBuilder: widget.footBuilder, cardBuilder: widget.cardBuilder, dataSource: dataSource, - scrollController: ScrollController(), + scrollController: columnState.scrollController(columnData.id), phantomController: widget.phantomController, onReorder: widget.dataController.moveColumnItem, cornerRadius: widget.config.cornerRadius, backgroundColor: widget.config.columnBackgroundColor, ); - // columnKeys - // .removeWhere((element) => element.columnId == columnData.id); - // columnKeys.add( - // ColumnKey( - // columnId: columnData.id, - // key: boardColumn.columnGlobalKey, - // ), - // ); + columnState.cacheColumn(columnData.id, boardColumn.globalKey); return ConstrainedBox( constraints: widget.columnConstraints, @@ -297,3 +296,26 @@ class _BoardColumnDataSourceImpl extends AFBoardColumnDataDataSource { @override List get acceptedColumnIds => dataController.columnIds; } + +class _BoardColumnState { + final Map columnKeys = {}; + + void cacheColumn(String columnId, GlobalKey key) { + columnKeys[columnId] = key; + } + + ScrollController scrollController(String columnId) { + final flexGlobalKey = columnKeys[columnId]; + var scrollController = ScrollController(); + if (flexGlobalKey != null) { + // assert(flexGlobalKey.currentWidget is ReorderFlex); + + // if (flexGlobalKey.currentWidget is ReorderFlex) { + // final reorderFlex = flexGlobalKey.currentWidget as ReorderFlex; + // final offset = reorderFlex.scrollController!.offset; + // scrollController = ScrollController(initialScrollOffset: offset); + // } + } + return scrollController; + } +} diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column.dart index 43719b097c..d2aab18850 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column.dart @@ -88,7 +88,9 @@ class AFBoardColumnWidget extends StatefulWidget { final Color backgroundColor; - const AFBoardColumnWidget({ + final GlobalKey globalKey = GlobalKey(); + + AFBoardColumnWidget({ Key? key, this.headerBuilder, this.footBuilder, @@ -114,10 +116,13 @@ class _AFBoardColumnWidgetState extends State { final GlobalKey _columnOverlayKey = GlobalKey(debugLabel: '$AFBoardColumnWidget overlay key'); + late GlobalObjectKey _indexGlobalKey; + late BoardOverlayEntry _overlayEntry; @override void initState() { + _indexGlobalKey = GlobalObjectKey(widget.key!); _overlayEntry = BoardOverlayEntry( builder: (BuildContext context) { final children = widget.dataSource.columnData.items @@ -138,7 +143,6 @@ class _AFBoardColumnWidgetState extends State { ); Widget reorderFlex = ReorderFlex( - key: widget.key, scrollController: widget.scrollController, config: widget.config, onDragStarted: (index) { @@ -161,6 +165,8 @@ class _AFBoardColumnWidgetState extends State { children: children, ); + reorderFlex = KeyedSubtree(key: _indexGlobalKey, child: reorderFlex); + return Container( margin: widget.margin, clipBehavior: Clip.hardEdge, @@ -172,10 +178,7 @@ class _AFBoardColumnWidgetState extends State { children: [ if (header != null) header, Expanded( - child: Padding( - padding: widget.itemMargin, - child: reorderFlex, - ), + child: Padding(padding: widget.itemMargin, child: reorderFlex), ), if (footer != null) footer, ], diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_target.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_target.dart index a091e9711a..80c1b7b744 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_target.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_target.dart @@ -39,7 +39,7 @@ class ReorderDragTarget extends StatefulWidget { final Widget child; final T dragTargetData; - final GlobalObjectKey _indexGlobalKey; + final GlobalObjectKey indexGlobalKey; /// Called when dragTarget is being dragging. final DragTargetOnStarted onDragStarted; @@ -69,9 +69,10 @@ class ReorderDragTarget extends StatefulWidget { final bool useMoveAnimation; - ReorderDragTarget({ + const ReorderDragTarget({ Key? key, required this.child, + required this.indexGlobalKey, required this.dragTargetData, required this.onDragStarted, required this.onDragEnded, @@ -82,8 +83,7 @@ class ReorderDragTarget extends StatefulWidget { this.onAccept, this.onLeave, this.draggableTargetBuilder, - }) : _indexGlobalKey = GlobalObjectKey(child.key!), - super(key: key); + }) : super(key: key); @override State> createState() => _ReorderDragTargetState(); @@ -112,7 +112,7 @@ class _ReorderDragTargetState }, ); - dragTarget = KeyedSubtree(key: widget._indexGlobalKey, child: dragTarget); + dragTarget = KeyedSubtree(key: widget.indexGlobalKey, child: dragTarget); return dragTarget; } @@ -150,7 +150,7 @@ class _ReorderDragTargetState child: widget.child, ), onDragStarted: () { - _draggingFeedbackSize = widget._indexGlobalKey.currentContext?.size; + _draggingFeedbackSize = widget.indexGlobalKey.currentContext?.size; widget.onDragStarted( widget.child, widget.dragTargetData.draggingIndex, diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_target_interceptor.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_target_interceptor.dart index 36366cd1e0..d1eb4b5b8b 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_target_interceptor.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_target_interceptor.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:collection'; import 'package:flutter/material.dart'; @@ -55,13 +56,14 @@ class OverlappingDragTargetInterceptor extends DragTargetInterceptor { final String reorderFlexId; final List acceptedReorderFlexId; final OverlapDragTargetDelegate delegate; - final List columnKeys = []; + final UnmodifiableMapView columnKeys; Timer? _delayOperation; OverlappingDragTargetInterceptor({ required this.delegate, required this.reorderFlexId, required this.acceptedReorderFlexId, + required this.columnKeys, }); @override @@ -105,12 +107,6 @@ class OverlappingDragTargetInterceptor extends DragTargetInterceptor { } } -class ColumnKey { - String columnId; - GlobalKey key; - ColumnKey({required this.columnId, required this.key}); -} - abstract class CrossReorderFlexDragTargetDelegate { /// * [reorderFlexId] is the id that the [ReorderFlex] passed in. bool acceptNewDragTargetData( diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/reorder_flex.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/reorder_flex.dart index 26b68c2304..3040611e74 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/reorder_flex.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/reorder_flex.dart @@ -74,7 +74,7 @@ class ReorderFlex extends StatefulWidget { final DragTargetInterceptor? interceptor; - const ReorderFlex({ + ReorderFlex({ Key? key, this.scrollController, required this.dataSource, @@ -85,7 +85,9 @@ class ReorderFlex extends StatefulWidget { this.onDragEnded, this.interceptor, this.direction = Axis.vertical, - }) : super(key: key); + }) : assert(children.every((Widget w) => w.key != null), + 'All child must have a key.'), + super(key: key); @override State createState() => ReorderFlexState(); @@ -112,10 +114,13 @@ class ReorderFlexState extends State late ReorderFlexNotifier _notifier; + late Map _childKeys; + @override void initState() { _notifier = ReorderFlexNotifier(); dragState = DraggingState(widget.reorderFlexId); + _childKeys = {}; _animation = DragTargetAnimation( reorderAnimationDuration: widget.config.reorderAnimationDuration, @@ -159,7 +164,11 @@ class ReorderFlexState extends State for (int i = 0; i < widget.children.length; i += 1) { Widget child = widget.children[i]; - children.add(_wrap(child, i)); + final item = widget.dataSource.items[i]; + + final indexGlobalKey = GlobalObjectKey(child.key!); + _childKeys[item.id] = indexGlobalKey; + children.add(_wrap(child, i, indexGlobalKey)); // if (widget.config.useMovePlaceholder) { // children.add(DragTargeMovePlaceholder( @@ -168,6 +177,9 @@ class ReorderFlexState extends State // )); // } } + Future.delayed(Duration(seconds: 3), () { + scrollToBottom(); + }); final child = _wrapContainer(children); return _wrapScrollView(child: child); @@ -203,10 +215,10 @@ class ReorderFlexState extends State /// [child]: the child will be wrapped with dartTarget /// [childIndex]: the index of the child in a list - Widget _wrap(Widget child, int childIndex) { + Widget _wrap(Widget child, int childIndex, GlobalObjectKey indexGlobalKey) { return Builder(builder: (context) { final ReorderDragTarget dragTarget = - _buildDragTarget(context, child, childIndex); + _buildDragTarget(context, child, childIndex, indexGlobalKey); int shiftedIndex = childIndex; if (dragState.isOverlapWithPhantom()) { @@ -312,10 +324,15 @@ class ReorderFlexState extends State } ReorderDragTarget _buildDragTarget( - BuildContext builderContext, Widget child, int dragTargetIndex) { + BuildContext builderContext, + Widget child, + int dragTargetIndex, + GlobalObjectKey indexGlobalKey, + ) { final ReoderFlexItem reorderFlexItem = widget.dataSource.items[dragTargetIndex]; return ReorderDragTarget( + indexGlobalKey: indexGlobalKey, dragTargetData: FlexDragTargetData( draggingIndex: dragTargetIndex, reorderFlexId: widget.reorderFlexId, @@ -515,6 +532,34 @@ class ReorderFlexState extends State } } + void scrollToBottom() { + if (_scrolling) return; + + if (widget.dataSource.items.isNotEmpty) { + final item = widget.dataSource.items.last; + final indexKey = _childKeys[item.id]; + if (indexKey == null) return; + + final indexContext = indexKey.currentContext; + if (indexContext == null) return; + if (_scrollController.hasClients == false) return; + + final renderObject = indexContext.findRenderObject(); + if (renderObject != null) { + _scrolling = true; + _scrollController.position + .ensureVisible( + renderObject, + alignment: 0.5, + duration: const Duration(milliseconds: 120), + ) + .then((value) { + setState(() => _scrolling = false); + }); + } + } + } + // Scrolls to a target context if that context is not on the screen. void _scrollTo(BuildContext context) { if (_scrolling) return; From 82c0006868dce79a29afd87465f30bb12815222d Mon Sep 17 00:00:00 2001 From: appflowy Date: Sat, 27 Aug 2022 09:52:08 +0800 Subject: [PATCH 055/106] chore: scroll to bottom after post frame --- .../plugins/board/application/board_bloc.dart | 59 +++++++++----- .../board/presentation/board_page.dart | 70 +++++++++++------ .../appflowy_board/lib/src/widgets/board.dart | 78 ++++++++++++++----- .../widgets/board_column/board_column.dart | 6 +- .../widgets/reorder_flex/reorder_flex.dart | 28 ++++--- 5 files changed, 164 insertions(+), 77 deletions(-) diff --git a/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart b/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart index 48d16e854d..c9709698de 100644 --- a/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart +++ b/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart @@ -72,16 +72,19 @@ class BoardBloc extends Bloc { createRow: (groupId) async { final result = await _gridDataController.createBoardCard(groupId); result.fold( - (rowPB) { - emit(state.copyWith(editingRow: some(rowPB))); - }, + (_) {}, (err) => Log.error(err), ); }, + didCreateRow: (String groupId, RowPB row) { + emit(state.copyWith( + editingRow: Some(BoardEditingRow(columnId: groupId, row: row)), + )); + }, endEditRow: (rowId) { assert(state.editingRow.isSome()); - state.editingRow.fold(() => null, (row) { - assert(row.id == rowId); + state.editingRow.fold(() => null, (editingRow) { + assert(editingRow.row.id == rowId); emit(state.copyWith(editingRow: none())); }); }, @@ -137,7 +140,12 @@ class BoardBloc extends Bloc { void initializeGroups(List groups) { for (final group in groups) { - final delegate = GroupControllerDelegateImpl(boardController); + final delegate = GroupControllerDelegateImpl( + controller: boardController, + didAddColumnItem: (groupId, row) { + add(BoardEvent.didCreateRow(groupId, row)); + }, + ); final controller = GroupController( gridId: state.gridId, group: group, @@ -222,8 +230,10 @@ class BoardBloc extends Bloc { @freezed class BoardEvent with _$BoardEvent { - const factory BoardEvent.initial() = InitialGrid; + const factory BoardEvent.initial() = InitialBrid; const factory BoardEvent.createRow(String groupId) = _CreateRow; + const factory BoardEvent.didCreateRow(String groupId, RowPB row) = + _DidCreateRow; const factory BoardEvent.endEditRow(String rowId) = _EndEditRow; const factory BoardEvent.didReceiveError(FlowyError error) = _DidReceiveError; const factory BoardEvent.didReceiveGridUpdate( @@ -239,7 +249,7 @@ class BoardState with _$BoardState { required String gridId, required Option grid, required List groupIds, - required Option editingRow, + required Option editingRow, required GridLoadingState loadingState, required Option noneOrError, }) = _BoardState; @@ -303,16 +313,17 @@ class BoardColumnItem extends AFColumnItem { class GroupControllerDelegateImpl extends GroupControllerDelegate { final AFBoardDataController controller; + final void Function(String, RowPB) didAddColumnItem; - GroupControllerDelegateImpl(this.controller); + GroupControllerDelegateImpl({ + required this.controller, + required this.didAddColumnItem, + }); @override void insertRow(GroupPB group, RowPB row, int? index) { if (index != null) { - final item = BoardColumnItem( - row: row, - fieldId: group.fieldId, - ); + final item = BoardColumnItem(row: row, fieldId: group.fieldId); controller.insertColumnItem(group.groupId, index, item); } else { final item = BoardColumnItem( @@ -321,6 +332,7 @@ class GroupControllerDelegateImpl extends GroupControllerDelegate { requestFocus: true, ); controller.addColumnItem(group.groupId, item); + didAddColumnItem(group.groupId, row); } } @@ -332,10 +344,21 @@ class GroupControllerDelegateImpl extends GroupControllerDelegate { @override void updateRow(GroupPB group, RowPB row) { controller.updateColumnItem( - group.groupId, - BoardColumnItem( - row: row, - fieldId: group.fieldId, - )); + group.groupId, + BoardColumnItem( + row: row, + fieldId: group.fieldId, + ), + ); } } + +class BoardEditingRow { + String columnId; + RowPB row; + + BoardEditingRow({ + required this.columnId, + required this.row, + }); +} diff --git a/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart b/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart index bd1bf54296..d0884e4efc 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart @@ -60,6 +60,8 @@ class BoardContent extends StatefulWidget { class _BoardContentState extends State { late ScrollController scrollController; + late AFBoardScrollManager scrollManager; + final config = AFBoardConfig( columnBackgroundColor: HexColor.fromHex('#F7F8FC'), ); @@ -67,37 +69,55 @@ class _BoardContentState extends State { @override void initState() { scrollController = ScrollController(); + scrollManager = AFBoardScrollManager(); super.initState(); } @override Widget build(BuildContext context) { - return BlocBuilder( - buildWhen: (previous, current) => - previous.groupIds.length != current.groupIds.length, - builder: (context, state) { - return Container( - color: Colors.white, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 20), - child: AFBoard( - scrollController: scrollController, - dataController: context.read().boardController, - headerBuilder: _buildHeader, - footBuilder: _buildFooter, - cardBuilder: (_, column, columnItem) => _buildCard( - context, - column, - columnItem, - ), - columnConstraints: const BoxConstraints.tightFor(width: 240), - config: AFBoardConfig( - columnBackgroundColor: HexColor.fromHex('#F7F8FC'), - ), - ), - ), + return BlocListener( + listener: (context, state) { + state.editingRow.fold( + () => null, + (editingRow) { + WidgetsBinding.instance.addPostFrameCallback((_) { + scrollManager.scrollToBottom(editingRow.columnId, () { + context + .read() + .add(BoardEvent.endEditRow(editingRow.row.id)); + }); + }); + }, ); }, + child: BlocBuilder( + buildWhen: (previous, current) => + previous.groupIds.length != current.groupIds.length, + builder: (context, state) { + return Container( + color: Colors.white, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 20), + child: AFBoard( + scrollManager: scrollManager, + scrollController: scrollController, + dataController: context.read().boardController, + headerBuilder: _buildHeader, + footBuilder: _buildFooter, + cardBuilder: (_, column, columnItem) => _buildCard( + context, + column, + columnItem, + ), + columnConstraints: const BoxConstraints.tightFor(width: 240), + config: AFBoardConfig( + columnBackgroundColor: HexColor.fromHex('#F7F8FC'), + ), + ), + ), + ); + }, + ), ); } @@ -178,7 +198,7 @@ class _BoardContentState extends State { final cellBuilder = BoardCellBuilder(cardController); final isEditing = context.read().state.editingRow.fold( () => false, - (editingRow) => editingRow.id == rowPB.id, + (editingRow) => editingRow.row.id == rowPB.id, ); return AppFlowyColumnItemCard( diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board.dart index ccb15d3830..007a9b82a8 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board.dart @@ -10,6 +10,16 @@ import 'reorder_flex/reorder_flex.dart'; import 'reorder_phantom/phantom_controller.dart'; import '../rendering/board_overlay.dart'; +class AFBoardScrollManager { + BoardColumnState? _columnState; + + // AFBoardScrollManager(); + + void scrollToBottom(String columnId, VoidCallback? completed) { + _columnState?.reorderFlexStateAtColumn(columnId)?.scrollToBottom(completed); + } +} + class AFBoardConfig { final double cornerRadius; final EdgeInsets columnPadding; @@ -58,6 +68,10 @@ class AFBoard extends StatelessWidget { final AFBoardConfig config; + final AFBoardScrollManager? scrollManager; + + final BoardColumnState _columnState = BoardColumnState(); + AFBoard({ required this.dataController, required this.cardBuilder, @@ -65,6 +79,7 @@ class AFBoard extends StatelessWidget { this.footBuilder, this.headerBuilder, this.scrollController, + this.scrollManager, this.columnConstraints = const BoxConstraints(maxWidth: 200), this.config = const AFBoardConfig(), Key? key, @@ -77,10 +92,16 @@ class AFBoard extends StatelessWidget { value: dataController, child: Consumer( builder: (context, notifier, child) { + if (scrollManager != null) { + scrollManager!._columnState = _columnState; + } + return AFBoardContent( config: config, dataController: dataController, scrollController: scrollController, + scrollManager: scrollManager, + columnState: _columnState, background: background, delegate: phantomController, columnConstraints: columnConstraints, @@ -106,6 +127,8 @@ class AFBoardContent extends StatefulWidget { final AFBoardConfig config; final ReorderFlexConfig reorderFlexConfig; final BoxConstraints columnConstraints; + final AFBoardScrollManager? scrollManager; + final BoardColumnState columnState; /// final AFBoardColumnCardBuilder cardBuilder; @@ -125,6 +148,8 @@ class AFBoardContent extends StatefulWidget { required this.onReorder, required this.delegate, required this.dataController, + required this.scrollManager, + required this.columnState, this.onDragStarted, this.onDragEnded, this.scrollController, @@ -143,22 +168,19 @@ class AFBoardContent extends StatefulWidget { } class _AFBoardContentState extends State { - late _BoardColumnState columnState; - - final GlobalKey _columnContainerOverlayKey = + final GlobalKey _boardContentKey = GlobalKey(debugLabel: '$AFBoardContent overlay key'); late BoardOverlayEntry _overlayEntry; @override void initState() { - columnState = _BoardColumnState(); _overlayEntry = BoardOverlayEntry( builder: (BuildContext context) { final interceptor = OverlappingDragTargetInterceptor( reorderFlexId: widget.dataController.identifier, acceptedReorderFlexId: widget.dataController.columnIds, delegate: widget.delegate, - columnKeys: UnmodifiableMapView(columnState.columnKeys), + columnKeys: UnmodifiableMapView(widget.columnState.columnKeys), ); final reorderFlex = ReorderFlex( @@ -198,7 +220,7 @@ class _AFBoardContentState extends State { @override Widget build(BuildContext context) { return BoardOverlay( - key: _columnContainerOverlayKey, + key: _boardContentKey, initialEntries: [_overlayEntry], ); } @@ -220,6 +242,8 @@ class _AFBoardContentState extends State { value: widget.dataController.getColumnController(columnData.id), child: Consumer( builder: (context, value, child) { + final scrollController = + widget.columnState.scrollController(columnData.id); final boardColumn = AFBoardColumnWidget( key: ValueKey(columnData.id), margin: _marginFromIndex(columnIndex), @@ -228,14 +252,17 @@ class _AFBoardContentState extends State { footBuilder: widget.footBuilder, cardBuilder: widget.cardBuilder, dataSource: dataSource, - scrollController: columnState.scrollController(columnData.id), + scrollController: scrollController, phantomController: widget.phantomController, onReorder: widget.dataController.moveColumnItem, cornerRadius: widget.config.cornerRadius, backgroundColor: widget.config.columnBackgroundColor, ); - columnState.cacheColumn(columnData.id, boardColumn.globalKey); + widget.columnState.addColumn( + columnData.id, + boardColumn.globalKey, + ); return ConstrainedBox( constraints: widget.columnConstraints, @@ -297,25 +324,36 @@ class _BoardColumnDataSourceImpl extends AFBoardColumnDataDataSource { List get acceptedColumnIds => dataController.columnIds; } -class _BoardColumnState { +class BoardColumnState { + /// Quick access to the [AFBoardColumnWidget] final Map columnKeys = {}; - void cacheColumn(String columnId, GlobalKey key) { + /// Records the position of the [AFBoardColumnWidget] + final Map columnScrollPositions = {}; + + void addColumn(String columnId, GlobalKey key) { columnKeys[columnId] = key; } - ScrollController scrollController(String columnId) { + ReorderFlexState? reorderFlexStateAtColumn(String columnId) { final flexGlobalKey = columnKeys[columnId]; - var scrollController = ScrollController(); - if (flexGlobalKey != null) { - // assert(flexGlobalKey.currentWidget is ReorderFlex); + if (flexGlobalKey == null) return null; + if (flexGlobalKey.currentState is! ReorderFlexState) return null; + final state = flexGlobalKey.currentState as ReorderFlexState; + return state; + } + + ReorderFlex? reorderFlexAtColumn(String columnId) { + final flexGlobalKey = columnKeys[columnId]; + if (flexGlobalKey == null) return null; + if (flexGlobalKey.currentWidget is! ReorderFlex) return null; + final widget = flexGlobalKey.currentWidget as ReorderFlex; + return widget; + } + + ScrollController scrollController(String columnId) { + ScrollController scrollController = ScrollController(); - // if (flexGlobalKey.currentWidget is ReorderFlex) { - // final reorderFlex = flexGlobalKey.currentWidget as ReorderFlex; - // final offset = reorderFlex.scrollController!.offset; - // scrollController = ScrollController(initialScrollOffset: offset); - // } - } return scrollController; } } diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column.dart index d2aab18850..c4a80bd80b 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column.dart @@ -116,13 +116,10 @@ class _AFBoardColumnWidgetState extends State { final GlobalKey _columnOverlayKey = GlobalKey(debugLabel: '$AFBoardColumnWidget overlay key'); - late GlobalObjectKey _indexGlobalKey; - late BoardOverlayEntry _overlayEntry; @override void initState() { - _indexGlobalKey = GlobalObjectKey(widget.key!); _overlayEntry = BoardOverlayEntry( builder: (BuildContext context) { final children = widget.dataSource.columnData.items @@ -143,6 +140,7 @@ class _AFBoardColumnWidgetState extends State { ); Widget reorderFlex = ReorderFlex( + key: widget.globalKey, scrollController: widget.scrollController, config: widget.config, onDragStarted: (index) { @@ -165,8 +163,6 @@ class _AFBoardColumnWidgetState extends State { children: children, ); - reorderFlex = KeyedSubtree(key: _indexGlobalKey, child: reorderFlex); - return Container( margin: widget.margin, clipBehavior: Clip.hardEdge, diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/reorder_flex.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/reorder_flex.dart index 3040611e74..d90b7ef3d9 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/reorder_flex.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/reorder_flex.dart @@ -177,9 +177,6 @@ class ReorderFlexState extends State // )); // } } - Future.delayed(Duration(seconds: 3), () { - scrollToBottom(); - }); final child = _wrapContainer(children); return _wrapScrollView(child: child); @@ -532,17 +529,25 @@ class ReorderFlexState extends State } } - void scrollToBottom() { - if (_scrolling) return; + void scrollToBottom(VoidCallback? completed) { + if (_scrolling) { + completed?.call(); + return; + } if (widget.dataSource.items.isNotEmpty) { final item = widget.dataSource.items.last; final indexKey = _childKeys[item.id]; - if (indexKey == null) return; + if (indexKey == null) { + completed?.call(); + return; + } final indexContext = indexKey.currentContext; - if (indexContext == null) return; - if (_scrollController.hasClients == false) return; + if (indexContext == null || _scrollController.hasClients == false) { + completed?.call(); + return; + } final renderObject = indexContext.findRenderObject(); if (renderObject != null) { @@ -554,8 +559,13 @@ class ReorderFlexState extends State duration: const Duration(milliseconds: 120), ) .then((value) { - setState(() => _scrolling = false); + setState(() { + _scrolling = false; + completed?.call(); + }); }); + } else { + completed?.call(); } } } From 4ead583f6db88afb42894263d284910d9ffc61e7 Mon Sep 17 00:00:00 2001 From: appflowy Date: Sat, 27 Aug 2022 21:22:26 +0800 Subject: [PATCH 056/106] chore: save scroll pos --- .../board/application/group_controller.dart | 32 +++++++++++++++---- .../appflowy_board/lib/src/widgets/board.dart | 20 +++--------- .../widgets/board_column/board_column.dart | 9 +++--- 3 files changed, 34 insertions(+), 27 deletions(-) diff --git a/frontend/app_flowy/lib/plugins/board/application/group_controller.dart b/frontend/app_flowy/lib/plugins/board/application/group_controller.dart index 0ca71ff5f6..942fa872c4 100644 --- a/frontend/app_flowy/lib/plugins/board/application/group_controller.dart +++ b/frontend/app_flowy/lib/plugins/board/application/group_controller.dart @@ -1,7 +1,7 @@ import 'package:flowy_sdk/log.dart'; import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/protobuf.dart'; - +import 'package:protobuf/protobuf.dart'; import 'group_listener.dart'; typedef OnGroupError = void Function(FlowyError); @@ -42,7 +42,6 @@ class GroupController { for (final insertedRow in changeset.insertedRows) { final index = insertedRow.hasIndex() ? insertedRow.index : null; - if (insertedRow.hasIndex() && group.rows.length > insertedRow.index) { group.rows.insert(insertedRow.index, insertedRow.row); @@ -50,11 +49,7 @@ class GroupController { group.rows.add(insertedRow.row); } - delegate.insertRow( - group, - insertedRow.row, - index, - ); + delegate.insertRow(group, insertedRow.row, index); } for (final updatedRow in changeset.updatedRows) { @@ -74,6 +69,29 @@ class GroupController { }); } + // GroupChangesetPB _transformChangeset(GroupChangesetPB changeset) { + // final insertedRows = changeset.insertedRows + // .where( + // (delete) => !changeset.deletedRows.contains(delete.row.id), + // ) + // .toList(); + + // final deletedRows = changeset.deletedRows + // .where((deletedRowId) => + // changeset.insertedRows + // .indexWhere((insert) => insert.row.id == deletedRowId) == + // -1) + // .toList(); + + // return changeset.rebuild((rebuildChangeset) { + // rebuildChangeset.insertedRows.clear(); + // rebuildChangeset.insertedRows.addAll(insertedRows); + + // rebuildChangeset.deletedRows.clear(); + // rebuildChangeset.deletedRows.addAll(deletedRows); + // }); + // } + Future dispose() async { _listener.stop(); } diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board.dart index 007a9b82a8..bf8cfa5313 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board.dart @@ -184,7 +184,7 @@ class _AFBoardContentState extends State { ); final reorderFlex = ReorderFlex( - key: widget.key, + key: const PageStorageKey('AFBoardContent'), config: widget.reorderFlexConfig, scrollController: widget.scrollController, onDragStarted: widget.onDragStarted, @@ -242,27 +242,23 @@ class _AFBoardContentState extends State { value: widget.dataController.getColumnController(columnData.id), child: Consumer( builder: (context, value, child) { - final scrollController = - widget.columnState.scrollController(columnData.id); final boardColumn = AFBoardColumnWidget( - key: ValueKey(columnData.id), + key: const PageStorageKey('AFBoardColumnWidget'), margin: _marginFromIndex(columnIndex), itemMargin: widget.config.columnItemPadding, headerBuilder: _buildHeader, footBuilder: widget.footBuilder, cardBuilder: widget.cardBuilder, dataSource: dataSource, - scrollController: scrollController, + scrollController: ScrollController(), phantomController: widget.phantomController, onReorder: widget.dataController.moveColumnItem, cornerRadius: widget.config.cornerRadius, backgroundColor: widget.config.columnBackgroundColor, ); - widget.columnState.addColumn( - columnData.id, - boardColumn.globalKey, - ); + widget.columnState + .addColumn(columnData.id, boardColumn.globalKey); return ConstrainedBox( constraints: widget.columnConstraints, @@ -350,10 +346,4 @@ class BoardColumnState { final widget = flexGlobalKey.currentWidget as ReorderFlex; return widget; } - - ScrollController scrollController(String columnId) { - ScrollController scrollController = ScrollController(); - - return scrollController; - } } diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column.dart index c4a80bd80b..150e3f8d6f 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column.dart @@ -65,7 +65,6 @@ class AFBoardColumnWidget extends StatefulWidget { final AFBoardColumnDataDataSource dataSource; final ScrollController? scrollController; final ReorderFlexConfig config; - final OnColumnDragStarted? onDragStarted; final OnColumnReorder onReorder; final OnColumnDragEnded? onDragEnded; @@ -88,7 +87,7 @@ class AFBoardColumnWidget extends StatefulWidget { final Color backgroundColor; - final GlobalKey globalKey = GlobalKey(); + final GlobalKey globalKey; AFBoardColumnWidget({ Key? key, @@ -98,14 +97,15 @@ class AFBoardColumnWidget extends StatefulWidget { required this.onReorder, required this.dataSource, required this.phantomController, - this.onDragStarted, this.scrollController, + this.onDragStarted, this.onDragEnded, this.margin = EdgeInsets.zero, this.itemMargin = EdgeInsets.zero, this.cornerRadius = 0.0, this.backgroundColor = Colors.transparent, - }) : config = const ReorderFlexConfig(), + }) : globalKey = GlobalKey(), + config = const ReorderFlexConfig(), super(key: key); @override @@ -115,7 +115,6 @@ class AFBoardColumnWidget extends StatefulWidget { class _AFBoardColumnWidgetState extends State { final GlobalKey _columnOverlayKey = GlobalKey(debugLabel: '$AFBoardColumnWidget overlay key'); - late BoardOverlayEntry _overlayEntry; @override From 60cf97969c37c6df7b632acb2ed3a240cc6489ba Mon Sep 17 00:00:00 2001 From: appflowy Date: Mon, 29 Aug 2022 09:41:40 +0800 Subject: [PATCH 057/106] fix: optimize insert card --- .../appflowy_board/lib/src/utils/log.dart | 2 +- .../appflowy_board/lib/src/widgets/board.dart | 91 ++++++++++++++----- .../widgets/board_column/board_column.dart | 9 ++ .../src/widgets/reorder_flex/drag_state.dart | 58 ++++++++++++ .../src/widgets/reorder_flex/drag_target.dart | 11 +++ .../reorder_flex/drag_target_interceptor.dart | 33 ++++--- .../widgets/reorder_flex/reorder_flex.dart | 70 ++++++++++---- .../reorder_phantom/phantom_controller.dart | 2 +- 8 files changed, 221 insertions(+), 55 deletions(-) diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/utils/log.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/utils/log.dart index 6f35ae4195..9c23060b26 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/utils/log.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/utils/log.dart @@ -4,7 +4,7 @@ import 'package:flutter/material.dart'; const DART_LOG = "Dart_LOG"; class Log { - static const enableLog = true; + static const enableLog = false; static void info(String? message) { if (enableLog) { diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board.dart index bf8cfa5313..6c2f55f2af 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board.dart @@ -1,22 +1,24 @@ -import 'dart:collection'; - +import 'package:appflowy_board/src/utils/log.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'board_column/board_column.dart'; import 'board_column/board_column_data.dart'; import 'board_data.dart'; +import 'reorder_flex/drag_state.dart'; import 'reorder_flex/drag_target_interceptor.dart'; import 'reorder_flex/reorder_flex.dart'; import 'reorder_phantom/phantom_controller.dart'; import '../rendering/board_overlay.dart'; class AFBoardScrollManager { - BoardColumnState? _columnState; + BoardColumnsState? _columnState; // AFBoardScrollManager(); void scrollToBottom(String columnId, VoidCallback? completed) { - _columnState?.reorderFlexStateAtColumn(columnId)?.scrollToBottom(completed); + _columnState + ?.getReorderFlexState(columnId: columnId) + ?.scrollToBottom(completed); } } @@ -70,7 +72,7 @@ class AFBoard extends StatelessWidget { final AFBoardScrollManager? scrollManager; - final BoardColumnState _columnState = BoardColumnState(); + final BoardColumnsState _columnState = BoardColumnsState(); AFBoard({ required this.dataController, @@ -101,7 +103,7 @@ class AFBoard extends StatelessWidget { dataController: dataController, scrollController: scrollController, scrollManager: scrollManager, - columnState: _columnState, + columnsState: _columnState, background: background, delegate: phantomController, columnConstraints: columnConstraints, @@ -128,7 +130,7 @@ class AFBoardContent extends StatefulWidget { final ReorderFlexConfig reorderFlexConfig; final BoxConstraints columnConstraints; final AFBoardScrollManager? scrollManager; - final BoardColumnState columnState; + final BoardColumnsState columnsState; /// final AFBoardColumnCardBuilder cardBuilder; @@ -149,7 +151,7 @@ class AFBoardContent extends StatefulWidget { required this.delegate, required this.dataController, required this.scrollManager, - required this.columnState, + required this.columnsState, this.onDragStarted, this.onDragEnded, this.scrollController, @@ -180,11 +182,10 @@ class _AFBoardContentState extends State { reorderFlexId: widget.dataController.identifier, acceptedReorderFlexId: widget.dataController.columnIds, delegate: widget.delegate, - columnKeys: UnmodifiableMapView(widget.columnState.columnKeys), + columnsState: widget.columnsState, ); final reorderFlex = ReorderFlex( - key: const PageStorageKey('AFBoardContent'), config: widget.reorderFlexConfig, scrollController: widget.scrollController, onDragStarted: widget.onDragStarted, @@ -243,7 +244,8 @@ class _AFBoardContentState extends State { child: Consumer( builder: (context, value, child) { final boardColumn = AFBoardColumnWidget( - key: const PageStorageKey('AFBoardColumnWidget'), + key: PageStorageKey(columnData.id), + // key: GlobalObjectKey(columnData.id), margin: _marginFromIndex(columnIndex), itemMargin: widget.config.columnItemPadding, headerBuilder: _buildHeader, @@ -255,10 +257,11 @@ class _AFBoardContentState extends State { onReorder: widget.dataController.moveColumnItem, cornerRadius: widget.config.cornerRadius, backgroundColor: widget.config.columnBackgroundColor, + dragStateStorage: widget.columnsState, + dragTargetIndexKeyStorage: widget.columnsState, ); - widget.columnState - .addColumn(columnData.id, boardColumn.globalKey); + widget.columnsState.addColumn(columnData.id, boardColumn); return ConstrainedBox( constraints: widget.columnConstraints, @@ -320,18 +323,23 @@ class _BoardColumnDataSourceImpl extends AFBoardColumnDataDataSource { List get acceptedColumnIds => dataController.columnIds; } -class BoardColumnState { +class BoardColumnContext { + GlobalKey? columnKey; + DraggingState? draggingState; +} + +class BoardColumnsState extends DraggingStateStorage + with ReorderDragTargerIndexKeyStorage { /// Quick access to the [AFBoardColumnWidget] final Map columnKeys = {}; + final Map columnDragStates = {}; + final Map> columnDragDragTargets = {}; - /// Records the position of the [AFBoardColumnWidget] - final Map columnScrollPositions = {}; - - void addColumn(String columnId, GlobalKey key) { - columnKeys[columnId] = key; + void addColumn(String columnId, AFBoardColumnWidget columnWidget) { + columnKeys[columnId] = columnWidget.globalKey; } - ReorderFlexState? reorderFlexStateAtColumn(String columnId) { + ReorderFlexState? getReorderFlexState({required String columnId}) { final flexGlobalKey = columnKeys[columnId]; if (flexGlobalKey == null) return null; if (flexGlobalKey.currentState is! ReorderFlexState) return null; @@ -339,11 +347,52 @@ class BoardColumnState { return state; } - ReorderFlex? reorderFlexAtColumn(String columnId) { + ReorderFlex? getReorderFlex({required String columnId}) { final flexGlobalKey = columnKeys[columnId]; if (flexGlobalKey == null) return null; if (flexGlobalKey.currentWidget is! ReorderFlex) return null; final widget = flexGlobalKey.currentWidget as ReorderFlex; return widget; } + + @override + DraggingState? read(String reorderFlexId) { + return columnDragStates[reorderFlexId]; + } + + @override + void write(String reorderFlexId, DraggingState state) { + Log.trace('$reorderFlexId Write dragging state: $state'); + columnDragStates[reorderFlexId] = state; + } + + @override + void remove(String reorderFlexId) { + columnDragStates.remove(reorderFlexId); + } + + @override + void addKey( + String reorderFlexId, + String key, + GlobalObjectKey> value, + ) { + Map? column = columnDragDragTargets[reorderFlexId]; + if (column == null) { + column = {}; + columnDragDragTargets[reorderFlexId] = column; + } + column[key] = value; + } + + @override + GlobalObjectKey>? readKey( + String reorderFlexId, String key) { + Map? column = columnDragDragTargets[reorderFlexId]; + if (column != null) { + return column[key]; + } else { + return null; + } + } } diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column.dart index 150e3f8d6f..ac4158a3e3 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column.dart @@ -1,5 +1,6 @@ import 'dart:collection'; +import 'package:appflowy_board/src/widgets/reorder_flex/drag_state.dart'; import 'package:flutter/material.dart'; import '../../rendering/board_overlay.dart'; import '../../utils/log.dart'; @@ -87,6 +88,10 @@ class AFBoardColumnWidget extends StatefulWidget { final Color backgroundColor; + final DraggingStateStorage? dragStateStorage; + + final ReorderDragTargerIndexKeyStorage? dragTargetIndexKeyStorage; + final GlobalKey globalKey; AFBoardColumnWidget({ @@ -97,6 +102,8 @@ class AFBoardColumnWidget extends StatefulWidget { required this.onReorder, required this.dataSource, required this.phantomController, + this.dragStateStorage, + this.dragTargetIndexKeyStorage, this.scrollController, this.onDragStarted, this.onDragEnded, @@ -140,6 +147,8 @@ class _AFBoardColumnWidgetState extends State { Widget reorderFlex = ReorderFlex( key: widget.globalKey, + dragStateStorage: widget.dragStateStorage, + dragTargetIndexKeyStorage: widget.dragTargetIndexKeyStorage, scrollController: widget.scrollController, config: widget.config, onDragStarted: (index) { diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_state.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_state.dart index 592277afbc..ef2c89418c 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_state.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_state.dart @@ -24,6 +24,10 @@ class FlexDragTargetData extends DragTargetData { final String dragTargetId; + Offset dragTargetOffset = Offset.zero; + + final GlobalObjectKey dragTargetIndexKey; + final String reorderFlexId; final ReoderFlexItem reorderFlexItem; @@ -33,6 +37,7 @@ class FlexDragTargetData extends DragTargetData { required this.draggingIndex, required this.reorderFlexId, required this.reorderFlexItem, + required this.dragTargetIndexKey, required DraggingState state, }) : _state = state; @@ -40,6 +45,50 @@ class FlexDragTargetData extends DragTargetData { String toString() { return 'ReorderFlexId: $reorderFlexId, dragTargetId: $dragTargetId'; } + + bool isOverlapWithWidgets(List widgetKeys) { + final renderBox = dragTargetIndexKey.currentContext?.findRenderObject(); + + if (renderBox == null) return false; + if (renderBox is! RenderBox) return false; + final size = feedbackSize ?? Size.zero; + final Rect rect = dragTargetOffset & size; + + for (final widgetKey in widgetKeys) { + final renderObject = widgetKey.currentContext?.findRenderObject(); + if (renderObject != null && renderObject is RenderBox) { + Rect widgetRect = + renderObject.localToGlobal(Offset.zero) & renderObject.size; + // return rect.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; + } +} + +abstract class DraggingStateStorage { + void write(String reorderFlexId, DraggingState state); + void remove(String reorderFlexId); + DraggingState? read(String reorderFlexId); } class DraggingState { @@ -128,6 +177,7 @@ class DraggingState { /// Set the currentIndex to nextIndex void moveDragTargetToNext() { + Log.debug('$reorderFlexId updateCurrentIndex: $nextIndex'); currentIndex = nextIndex; } @@ -136,6 +186,14 @@ class DraggingState { nextIndex = index; } + void setStartDragggingIndex(int index) { + Log.debug('$reorderFlexId setDragIndex: $index'); + dragStartIndex = index; + phantomIndex = index; + currentIndex = index; + nextIndex = index; + } + bool isNotDragging() { return dragStartIndex == -1; } diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_target.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_target.dart index 80c1b7b744..92b71cd1f3 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_target.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_target.dart @@ -26,6 +26,11 @@ typedef DragTargetWillAccepted = bool Function( /// typedef DragTargetOnStarted = void Function(Widget, int, Size?); +typedef DragTargetOnMove = void Function( + T dragTargetData, + Offset offset, +); + /// typedef DragTargetOnEnded = void Function( T dragTargetData); @@ -46,6 +51,8 @@ class ReorderDragTarget extends StatefulWidget { final DragTargetOnEnded onDragEnded; + final DragTargetOnMove onDragMoved; + /// Called to determine whether this widget is interested in receiving a given /// piece of data being dragged over this drag target. /// @@ -75,6 +82,7 @@ class ReorderDragTarget extends StatefulWidget { required this.indexGlobalKey, required this.dragTargetData, required this.onDragStarted, + required this.onDragMoved, required this.onDragEnded, required this.onWillAccept, required this.insertAnimationController, @@ -104,6 +112,9 @@ class _ReorderDragTargetState return widget.onWillAccept(dragTargetData); }, onAccept: widget.onAccept, + onMove: (detail) { + widget.onDragMoved(detail.data, detail.offset); + }, onLeave: (dragTargetData) { assert(dragTargetData != null); if (dragTargetData != null) { diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_target_interceptor.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_target_interceptor.dart index d1eb4b5b8b..a2d2cf9c6c 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_target_interceptor.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_target_interceptor.dart @@ -1,8 +1,7 @@ import 'dart:async'; -import 'dart:collection'; +import 'package:appflowy_board/src/widgets/board.dart'; import 'package:flutter/material.dart'; - import '../../utils/log.dart'; import 'drag_state.dart'; import 'drag_target.dart'; @@ -42,7 +41,7 @@ abstract class OverlapDragTargetDelegate { int dragTargetIndex, ); - int canMoveTo(String dragTargetId); + int getInsertedIndex(String dragTargetId); } /// [OverlappingDragTargetInterceptor] is used to receive the overlapping @@ -56,14 +55,14 @@ class OverlappingDragTargetInterceptor extends DragTargetInterceptor { final String reorderFlexId; final List acceptedReorderFlexId; final OverlapDragTargetDelegate delegate; - final UnmodifiableMapView columnKeys; + final BoardColumnsState columnsState; Timer? _delayOperation; OverlappingDragTargetInterceptor({ required this.delegate, required this.reorderFlexId, required this.acceptedReorderFlexId, - required this.columnKeys, + required this.columnsState, }); @override @@ -81,24 +80,30 @@ class OverlappingDragTargetInterceptor extends DragTargetInterceptor { if (dragTargetId == dragTargetData.reorderFlexId) { delegate.cancel(); } else { + // Ignore the event if the dragTarget overlaps with the other column's dragTargets. + final columnKeys = columnsState.columnDragDragTargets[dragTargetId]; + if (columnKeys != null) { + final keys = columnKeys.values.toList(); + if (dragTargetData.isOverlapWithWidgets(keys)) { + _delayOperation?.cancel(); + return true; + } + } + /// The priority of the column interactions is high than the cross column. /// Workaround: delay 100 milliseconds to lower the cross column event priority. + /// _delayOperation?.cancel(); _delayOperation = Timer(const Duration(milliseconds: 100), () { - final index = delegate.canMoveTo(dragTargetId); + final index = delegate.getInsertedIndex(dragTargetId); if (index != -1) { Log.trace( '[$OverlappingDragTargetInterceptor] move to $dragTargetId at $index'); delegate.moveTo(dragTargetId, dragTargetData, index); - // final columnIndex = columnKeys - // .indexWhere((element) => element.columnId == dragTargetId); - // if (columnIndex != -1) { - // final state = columnKeys[columnIndex].key.currentState; - // if (state is ReorderFlexState) { - // state.handleOnWillAccept(context, index); - // } - // } + columnsState + .getReorderFlexState(columnId: dragTargetId) + ?.resetDragTargetIndex(index); } }); } diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/reorder_flex.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/reorder_flex.dart index d90b7ef3d9..0850b5f030 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/reorder_flex.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/reorder_flex.dart @@ -31,6 +31,11 @@ abstract class ReoderFlexItem { String get id; } +abstract class ReorderDragTargerIndexKeyStorage { + void addKey(String reorderFlexId, String key, GlobalObjectKey value); + GlobalObjectKey? readKey(String reorderFlexId, String key); +} + class ReorderFlexConfig { /// The opacity of the dragging widget final double draggingWidgetOpacity = 0.3; @@ -52,7 +57,6 @@ class ReorderFlexConfig { class ReorderFlex extends StatefulWidget { final ReorderFlexConfig config; - final List children; /// [direction] How to place the children, default is Axis.vertical @@ -74,6 +78,10 @@ class ReorderFlex extends StatefulWidget { final DragTargetInterceptor? interceptor; + final DraggingStateStorage? dragStateStorage; + + final ReorderDragTargerIndexKeyStorage? dragTargetIndexKeyStorage; + ReorderFlex({ Key? key, this.scrollController, @@ -81,6 +89,8 @@ class ReorderFlex extends StatefulWidget { required this.children, required this.config, required this.onReorder, + this.dragStateStorage, + this.dragTargetIndexKeyStorage, this.onDragStarted, this.onDragEnded, this.interceptor, @@ -114,13 +124,15 @@ class ReorderFlexState extends State late ReorderFlexNotifier _notifier; - late Map _childKeys; - @override void initState() { _notifier = ReorderFlexNotifier(); - dragState = DraggingState(widget.reorderFlexId); - _childKeys = {}; + final flexId = widget.reorderFlexId; + dragState = widget.dragStateStorage?.read(flexId) ?? + DraggingState(widget.reorderFlexId); + Log.trace('[DragTarget] init dragState: $dragState'); + + widget.dragStateStorage?.remove(flexId); _animation = DragTargetAnimation( reorderAnimationDuration: widget.config.reorderAnimationDuration, @@ -164,11 +176,17 @@ class ReorderFlexState extends State for (int i = 0; i < widget.children.length; i += 1) { Widget child = widget.children[i]; - final item = widget.dataSource.items[i]; + final ReoderFlexItem item = widget.dataSource.items[i]; - final indexGlobalKey = GlobalObjectKey(child.key!); - _childKeys[item.id] = indexGlobalKey; - children.add(_wrap(child, i, indexGlobalKey)); + final indexKey = GlobalObjectKey(child.key!); + // Save the index key for quick access + widget.dragTargetIndexKeyStorage?.addKey( + widget.reorderFlexId, + item.id, + indexKey, + ); + + children.add(_wrap(child, i, indexKey)); // if (widget.config.useMovePlaceholder) { // children.add(DragTargeMovePlaceholder( @@ -212,10 +230,10 @@ class ReorderFlexState extends State /// [child]: the child will be wrapped with dartTarget /// [childIndex]: the index of the child in a list - Widget _wrap(Widget child, int childIndex, GlobalObjectKey indexGlobalKey) { + Widget _wrap(Widget child, int childIndex, GlobalObjectKey indexKey) { return Builder(builder: (context) { final ReorderDragTarget dragTarget = - _buildDragTarget(context, child, childIndex, indexGlobalKey); + _buildDragTarget(context, child, childIndex, indexKey); int shiftedIndex = childIndex; if (dragState.isOverlapWithPhantom()) { @@ -324,24 +342,28 @@ class ReorderFlexState extends State BuildContext builderContext, Widget child, int dragTargetIndex, - GlobalObjectKey indexGlobalKey, + GlobalObjectKey indexKey, ) { - final ReoderFlexItem reorderFlexItem = - widget.dataSource.items[dragTargetIndex]; + final reorderFlexItem = widget.dataSource.items[dragTargetIndex]; return ReorderDragTarget( - indexGlobalKey: indexGlobalKey, + indexGlobalKey: indexKey, dragTargetData: FlexDragTargetData( draggingIndex: dragTargetIndex, reorderFlexId: widget.reorderFlexId, reorderFlexItem: reorderFlexItem, state: dragState, dragTargetId: reorderFlexItem.id, + dragTargetIndexKey: indexKey, ), onDragStarted: (draggingWidget, draggingIndex, size) { Log.debug( "[DragTarget] Column:[${widget.dataSource.identifier}] start dragging item at $draggingIndex"); _startDragging(draggingWidget, draggingIndex, size); widget.onDragStarted?.call(draggingIndex); + widget.dragStateStorage?.remove(widget.reorderFlexId); + }, + onDragMoved: (dragTargetData, offset) { + dragTargetData.dragTargetOffset = offset; }, onDragEnded: (dragTargetData) { if (!mounted) return; @@ -445,14 +467,20 @@ class ReorderFlexState extends State }); } + void resetDragTargetIndex(int dragTargetIndex) { + dragState.setStartDragggingIndex(dragTargetIndex); + widget.dragStateStorage?.write( + widget.reorderFlexId, + dragState, + ); + } + bool handleOnWillAccept(BuildContext context, int dragTargetIndex) { final dragIndex = dragState.dragStartIndex; /// The [willAccept] will be true if the dargTarget is the widget that gets /// dragged and it is dragged on top of the other dragTargets. /// - Log.trace( - '[$ReorderDragTarget] ${widget.dataSource.identifier} on will accept, dragIndex:$dragIndex, dragTargetIndex:$dragTargetIndex, count: ${widget.dataSource.items.length}'); bool willAccept = dragState.dragStartIndex == dragIndex && dragIndex != dragTargetIndex; @@ -466,6 +494,9 @@ class ReorderFlexState extends State _requestAnimationToNextIndex(isAcceptingNewTarget: true); }); + Log.trace( + '[$ReorderDragTarget] ${widget.reorderFlexId} dragging state: $dragState}'); + _scrollTo(context); /// If the target is not the original starting point, then we will accept the drop. @@ -537,7 +568,10 @@ class ReorderFlexState extends State if (widget.dataSource.items.isNotEmpty) { final item = widget.dataSource.items.last; - final indexKey = _childKeys[item.id]; + final indexKey = widget.dragTargetIndexKeyStorage?.readKey( + widget.reorderFlexId, + item.id, + ); if (indexKey == null) { completed?.call(); return; diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_phantom/phantom_controller.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_phantom/phantom_controller.dart index 4dd4f05a74..14b525d67d 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_phantom/phantom_controller.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_phantom/phantom_controller.dart @@ -203,7 +203,7 @@ class BoardPhantomController extends OverlapDragTargetDelegate } @override - int canMoveTo(String dragTargetId) { + int getInsertedIndex(String dragTargetId) { if (columnsState.isDragging(dragTargetId)) { return -1; } From 61fb13eba6cf9c2482bbc0cdec1692ec7d0821a5 Mon Sep 17 00:00:00 2001 From: appflowy Date: Mon, 29 Aug 2022 10:06:29 +0800 Subject: [PATCH 058/106] ci: fix warnings --- .../app_flowy/lib/plugins/board/application/board_bloc.dart | 2 +- .../lib/plugins/board/application/group_controller.dart | 1 - .../packages/appflowy_board/lib/src/widgets/board.dart | 2 +- .../lib/src/widgets/board_column/board_column.dart | 2 +- .../lib/src/widgets/reorder_flex/drag_state.dart | 2 +- .../lib/src/widgets/reorder_flex/reorder_flex.dart | 6 +++--- 6 files changed, 7 insertions(+), 8 deletions(-) diff --git a/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart b/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart index c9709698de..dcac0a47bb 100644 --- a/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart +++ b/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart @@ -230,7 +230,7 @@ class BoardBloc extends Bloc { @freezed class BoardEvent with _$BoardEvent { - const factory BoardEvent.initial() = InitialBrid; + const factory BoardEvent.initial() = _InitialBoard; const factory BoardEvent.createRow(String groupId) = _CreateRow; const factory BoardEvent.didCreateRow(String groupId, RowPB row) = _DidCreateRow; diff --git a/frontend/app_flowy/lib/plugins/board/application/group_controller.dart b/frontend/app_flowy/lib/plugins/board/application/group_controller.dart index 942fa872c4..812dbc76d3 100644 --- a/frontend/app_flowy/lib/plugins/board/application/group_controller.dart +++ b/frontend/app_flowy/lib/plugins/board/application/group_controller.dart @@ -1,7 +1,6 @@ import 'package:flowy_sdk/log.dart'; import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/protobuf.dart'; -import 'package:protobuf/protobuf.dart'; import 'group_listener.dart'; typedef OnGroupError = void Function(FlowyError); diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board.dart index 6c2f55f2af..67d8111aec 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board.dart @@ -329,7 +329,7 @@ class BoardColumnContext { } class BoardColumnsState extends DraggingStateStorage - with ReorderDragTargerIndexKeyStorage { + with ReorderDragTargetIndexKeyStorage { /// Quick access to the [AFBoardColumnWidget] final Map columnKeys = {}; final Map columnDragStates = {}; diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column.dart index ac4158a3e3..a89ef269cb 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column.dart @@ -90,7 +90,7 @@ class AFBoardColumnWidget extends StatefulWidget { final DraggingStateStorage? dragStateStorage; - final ReorderDragTargerIndexKeyStorage? dragTargetIndexKeyStorage; + final ReorderDragTargetIndexKeyStorage? dragTargetIndexKeyStorage; final GlobalKey globalKey; diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_state.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_state.dart index ef2c89418c..f795efa6b3 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_state.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_state.dart @@ -186,7 +186,7 @@ class DraggingState { nextIndex = index; } - void setStartDragggingIndex(int index) { + void setStartDraggingIndex(int index) { Log.debug('$reorderFlexId setDragIndex: $index'); dragStartIndex = index; phantomIndex = index; diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/reorder_flex.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/reorder_flex.dart index 0850b5f030..b0f511f641 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/reorder_flex.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/reorder_flex.dart @@ -31,7 +31,7 @@ abstract class ReoderFlexItem { String get id; } -abstract class ReorderDragTargerIndexKeyStorage { +abstract class ReorderDragTargetIndexKeyStorage { void addKey(String reorderFlexId, String key, GlobalObjectKey value); GlobalObjectKey? readKey(String reorderFlexId, String key); } @@ -80,7 +80,7 @@ class ReorderFlex extends StatefulWidget { final DraggingStateStorage? dragStateStorage; - final ReorderDragTargerIndexKeyStorage? dragTargetIndexKeyStorage; + final ReorderDragTargetIndexKeyStorage? dragTargetIndexKeyStorage; ReorderFlex({ Key? key, @@ -468,7 +468,7 @@ class ReorderFlexState extends State } void resetDragTargetIndex(int dragTargetIndex) { - dragState.setStartDragggingIndex(dragTargetIndex); + dragState.setStartDraggingIndex(dragTargetIndex); widget.dragStateStorage?.write( widget.reorderFlexId, dragState, From 3686351592b1b8a707544e3739d213b2d6433452 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Mon, 29 Aug 2022 10:08:44 +0800 Subject: [PATCH 059/106] fix: #918 could not update the link sometimes --- .../lib/src/render/rich_text/flowy_rich_text.dart | 13 ++++++++++++- .../test/render/rich_text/checkbox_text_test.dart | 4 ++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/flowy_rich_text.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/flowy_rich_text.dart index 39f484c23f..884a8bbe12 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/flowy_rich_text.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/flowy_rich_text.dart @@ -42,7 +42,7 @@ class FlowyRichText extends StatefulWidget { } class _FlowyRichTextState extends State with Selectable { - final _textKey = GlobalKey(); + var _textKey = GlobalKey(); final _placeholderTextKey = GlobalKey(); final _lineHeight = 1.5; @@ -53,6 +53,17 @@ class _FlowyRichTextState extends State with Selectable { RenderParagraph get _placeholderRenderParagraph => _placeholderTextKey.currentContext?.findRenderObject() as RenderParagraph; + @override + void didUpdateWidget(covariant FlowyRichText oldWidget) { + super.didUpdateWidget(oldWidget); + + // https://github.com/flutter/flutter/issues/110342 + if (_textKey.currentWidget is RichText) { + // Force refresh the RichText widget. + _textKey = GlobalKey(); + } + } + @override Widget build(BuildContext context) { return _buildRichText(context); diff --git a/frontend/app_flowy/packages/appflowy_editor/test/render/rich_text/checkbox_text_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/render/rich_text/checkbox_text_test.dart index f039c227d9..afd89ddee9 100644 --- a/frontend/app_flowy/packages/appflowy_editor/test/render/rich_text/checkbox_text_test.dart +++ b/frontend/app_flowy/packages/appflowy_editor/test/render/rich_text/checkbox_text_test.dart @@ -10,8 +10,8 @@ void main() async { TestWidgetsFlutterBinding.ensureInitialized(); }); - group('delete_text_handler.dart', () { - testWidgets('Presses backspace key in empty document', (tester) async { + group('checkbox_text_handler.dart', () { + testWidgets('Click checkbox icon', (tester) async { // Before // // [BIUS]Welcome to Appflowy 😁[BIUS] From cde48926e2f9aab0fead3bcab95b546ee3f11244 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Mon, 29 Aug 2022 10:25:56 +0800 Subject: [PATCH 060/106] feat: add copy link to link menu --- .../appflowy_editor/assets/images/copy.svg | 4 + .../extensions/url_launcher_extension.dart | 14 ++++ .../src/operation/transaction_builder.dart | 14 ++-- .../lib/src/render/link_menu/link_menu.dart | 19 +++-- .../src/render/rich_text/flowy_rich_text.dart | 80 +++++++++---------- .../lib/src/render/toolbar/toolbar_item.dart | 20 +++-- .../lib/src/service/input_service.dart | 4 - .../test/render/link_menu/link_menu_test.dart | 1 + 8 files changed, 95 insertions(+), 61 deletions(-) create mode 100644 frontend/app_flowy/packages/appflowy_editor/assets/images/copy.svg create mode 100644 frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/url_launcher_extension.dart diff --git a/frontend/app_flowy/packages/appflowy_editor/assets/images/copy.svg b/frontend/app_flowy/packages/appflowy_editor/assets/images/copy.svg new file mode 100644 index 0000000000..101cf34205 --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/assets/images/copy.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/url_launcher_extension.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/url_launcher_extension.dart new file mode 100644 index 0000000000..1c0ea30c82 --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/url_launcher_extension.dart @@ -0,0 +1,14 @@ +import 'package:url_launcher/url_launcher_string.dart'; + +Future safeLaunchUrl(String? href) async { + if (href == null) { + return Future.value(false); + } + final uri = Uri.parse(href); + // url_launcher cannot open a link without scheme. + final newHref = (uri.scheme.isNotEmpty ? href : 'http://$href').trim(); + if (await canLaunchUrlString(newHref)) { + await launchUrlString(newHref); + } + return Future.value(true); +} diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/operation/transaction_builder.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/operation/transaction_builder.dart index 12c13bf2e5..1390b23918 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/operation/transaction_builder.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/operation/transaction_builder.dart @@ -115,17 +115,18 @@ class TransactionBuilder { /// Inserts content at a specified index. /// Optionally, you may specify formatting attributes that are applied to the inserted string. /// By default, the formatting attributes before the insert position will be used. - insertText(TextNode node, int index, String content, - {Attributes? attributes, Attributes? removedAttributes}) { + insertText( + TextNode node, + int index, + String content, { + Attributes? attributes, + }) { var newAttributes = attributes; if (index != 0 && attributes == null) { newAttributes = node.delta.slice(max(index - 1, 0), index).first.attributes; if (newAttributes != null) { newAttributes = Attributes.from(newAttributes); - if (removedAttributes != null) { - newAttributes.addAll(removedAttributes); - } } } textEdit( @@ -138,7 +139,8 @@ class TransactionBuilder { ), ); afterSelection = Selection.collapsed( - Position(path: node.path, offset: index + content.length)); + Position(path: node.path, offset: index + content.length), + ); } /// Assigns formatting attributes to a range of text. diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/link_menu/link_menu.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/link_menu/link_menu.dart index a33adf3b8c..07e1b947eb 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/link_menu/link_menu.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/link_menu/link_menu.dart @@ -6,12 +6,14 @@ class LinkMenu extends StatefulWidget { Key? key, this.linkText, required this.onSubmitted, + required this.onOpenLink, required this.onCopyLink, required this.onRemoveLink, }) : super(key: key); final String? linkText; final void Function(String text) onSubmitted; + final VoidCallback onOpenLink; final VoidCallback onCopyLink; final VoidCallback onRemoveLink; @@ -26,15 +28,12 @@ class _LinkMenuState extends State { @override void initState() { super.initState(); - _textEditingController.text = widget.linkText ?? ''; - _focusNode.requestFocus(); } @override void dispose() { - _focusNode.dispose(); - + _textEditingController.dispose(); super.dispose(); } @@ -67,6 +66,12 @@ class _LinkMenuState extends State { if (widget.linkText != null) ...[ _buildIconButton( iconName: 'link', + text: 'Open link', + onPressed: widget.onOpenLink, + ), + _buildIconButton( + iconName: 'copy', + color: Colors.black, text: 'Copy link', onPressed: widget.onCopyLink, ), @@ -126,11 +131,15 @@ class _LinkMenuState extends State { Widget _buildIconButton({ required String iconName, + Color? color, required String text, required VoidCallback onPressed, }) { return TextButton.icon( - icon: FlowySvg(name: iconName), + icon: FlowySvg( + name: iconName, + color: color, + ), style: TextButton.styleFrom( minimumSize: const Size.fromHeight(40), padding: EdgeInsets.zero, diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/flowy_rich_text.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/flowy_rich_text.dart index 884a8bbe12..517f7dd4b8 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/flowy_rich_text.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/flowy_rich_text.dart @@ -1,6 +1,8 @@ import 'dart:async'; import 'dart:ui'; +import 'package:appflowy_editor/src/extensions/url_launcher_extension.dart'; +import 'package:appflowy_editor/src/render/toolbar/toolbar_item.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; @@ -13,7 +15,6 @@ import 'package:appflowy_editor/src/document/text_delta.dart'; import 'package:appflowy_editor/src/editor_state.dart'; import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart'; import 'package:appflowy_editor/src/render/selection/selectable.dart'; -import 'package:url_launcher/url_launcher_string.dart'; typedef FlowyTextSpanDecorator = TextSpan Function(TextSpan textSpan); @@ -204,53 +205,23 @@ class _FlowyRichTextState extends State with Selectable { var offset = 0; return TextSpan( children: widget.textNode.delta.whereType().map((insert) { - GestureRecognizer? gestureDetector; + GestureRecognizer? gestureRecognizer; if (insert.attributes?[StyleKey.href] != null) { - final startOffset = offset; - Timer? timer; - var tapCount = 0; - gestureDetector = TapGestureRecognizer() - ..onTap = () async { - // implement a simple double tap logic - tapCount += 1; - timer?.cancel(); - - if (tapCount == 2) { - tapCount = 0; - final href = insert.attributes![StyleKey.href]; - final uri = Uri.parse(href); - // url_launcher cannot open a link without scheme. - final newHref = - (uri.scheme.isNotEmpty ? href : 'http://$href').trim(); - if (await canLaunchUrlString(newHref)) { - await launchUrlString(newHref); - } - return; - } - - timer = Timer(const Duration(milliseconds: 200), () { - tapCount = 0; - // update selection - final selection = Selection.single( - path: widget.textNode.path, - startOffset: startOffset, - endOffset: startOffset + insert.length, - ); - widget.editorState.service.selectionService - .updateSelection(selection); - WidgetsBinding.instance.addPostFrameCallback((timeStamp) { - widget.editorState.service.toolbarService - ?.triggerHandler('appflowy.toolbar.link'); - }); - }); - }; + gestureRecognizer = _buildTapHrefGestureRecognizer( + insert.attributes![StyleKey.href], + Selection.single( + path: widget.textNode.path, + startOffset: offset, + endOffset: offset + insert.length, + ), + ); } offset += insert.length; final textSpan = RichTextStyle( attributes: insert.attributes ?? {}, text: insert.content, height: _lineHeight, - gestureRecognizer: gestureDetector, + gestureRecognizer: gestureRecognizer, ).toTextSpan(); return textSpan; }).toList(growable: false), @@ -266,4 +237,31 @@ class _FlowyRichTextState extends State with Selectable { height: _lineHeight, ).toTextSpan() ]); + + GestureRecognizer _buildTapHrefGestureRecognizer( + String href, Selection selection) { + Timer? timer; + var tapCount = 0; + final tapGestureRecognizer = TapGestureRecognizer() + ..onTap = () async { + // implement a simple double tap logic + tapCount += 1; + timer?.cancel(); + + if (tapCount == 2) { + tapCount = 0; + safeLaunchUrl(href); + return; + } + + timer = Timer(const Duration(milliseconds: 200), () { + tapCount = 0; + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + showLinkMenu(context, widget.editorState, + customSelection: selection); + }); + }); + }; + return tapGestureRecognizer; + } } diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item.dart index 107ae23b6f..979f86cdd1 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item.dart @@ -1,4 +1,5 @@ import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_editor/src/extensions/url_launcher_extension.dart'; import 'package:appflowy_editor/src/infra/flowy_svg.dart'; import 'package:appflowy_editor/src/render/link_menu/link_menu.dart'; import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart'; @@ -132,7 +133,7 @@ List defaultToolbarItems = [ tooltipsMessage: 'Link', icon: const FlowySvg(name: 'toolbar/link'), validator: _onlyShowInSingleTextSelection, - handler: (editorState, context) => _showLinkMenu(editorState, context), + handler: (editorState, context) => showLinkMenu(context, editorState), ), ToolbarItem( id: 'appflowy.toolbar.highlight', @@ -157,7 +158,11 @@ ToolbarShowValidator _showInTextSelection = (editorState) { OverlayEntry? _linkMenuOverlay; EditorState? _editorState; -void _showLinkMenu(EditorState editorState, BuildContext context) { +void showLinkMenu( + BuildContext context, + EditorState editorState, { + Selection? customSelection, +}) { final rects = editorState.service.selectionService.selectionRects; var maxBottom = 0.0; late Rect matchRect; @@ -173,8 +178,11 @@ void _showLinkMenu(EditorState editorState, BuildContext context) { // Since the link menu will only show in single text selection, // We get the text node directly instead of judging details again. - final selection = - editorState.service.selectionService.currentSelection.value!; + final selection = customSelection ?? + editorState.service.selectionService.currentSelection.value; + if (selection == null) { + return; + } final index = selection.isBackward ? selection.start.offset : selection.end.offset; final length = (selection.start.offset - selection.end.offset).abs(); @@ -191,6 +199,9 @@ void _showLinkMenu(EditorState editorState, BuildContext context) { child: Material( child: LinkMenu( linkText: linkText, + onOpenLink: () async { + await safeLaunchUrl(linkText); + }, onSubmitted: (text) { TransactionBuilder(editorState) ..formatText(node, index, length, {StyleKey.href: text}) @@ -214,7 +225,6 @@ void _showLinkMenu(EditorState editorState, BuildContext context) { Overlay.of(context)?.insert(_linkMenuOverlay!); editorState.service.scrollService?.disable(); - editorState.service.keyboardService?.disable(); editorState.service.selectionService.currentSelection .addListener(_dismissLinkMenu); } diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/input_service.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/input_service.dart index 96f0777544..a92fae1b95 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/input_service.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/input_service.dart @@ -1,5 +1,4 @@ import 'package:appflowy_editor/src/infra/log.dart'; -import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -150,9 +149,6 @@ class _AppFlowyInputState extends State textNode, delta.insertionOffset, delta.textInserted, - removedAttributes: { - StyleKey.href: null, - }, ) ..commit(); } else { diff --git a/frontend/app_flowy/packages/appflowy_editor/test/render/link_menu/link_menu_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/render/link_menu/link_menu_test.dart index 7b4541033b..5b102b9ec1 100644 --- a/frontend/app_flowy/packages/appflowy_editor/test/render/link_menu/link_menu_test.dart +++ b/frontend/app_flowy/packages/appflowy_editor/test/render/link_menu/link_menu_test.dart @@ -12,6 +12,7 @@ void main() async { const link = 'appflowy.io'; var submittedText = ''; final linkMenu = LinkMenu( + onOpenLink: () {}, onCopyLink: () {}, onRemoveLink: () {}, onSubmitted: (text) { From c07af9007cf18e651d28d6622c5ad20c3cc9da69 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Mon, 29 Aug 2022 11:40:34 +0800 Subject: [PATCH 061/106] fix: Should not add a new line below after pressing enter at the front of the first line of text. --- .../lib/src/document/state_tree.dart | 15 +++++++++---- ...thout_shift_in_text_node_handler_test.dart | 21 +++++++++++++++++++ 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/document/state_tree.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/document/state_tree.dart index a17b2fbf98..a4a9869df5 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/document/state_tree.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/document/state_tree.dart @@ -62,10 +62,17 @@ class StateTree { } return false; } - for (var i = 0; i < nodes.length; i++) { - final node = nodes[i]; - insertedNode!.insertAfter(node); - insertedNode = node; + if (path.last <= 0) { + for (var i = 0; i < nodes.length; i++) { + final node = nodes[i]; + insertedNode.insertBefore(node); + } + } else { + for (var i = 0; i < nodes.length; i++) { + final node = nodes[i]; + insertedNode!.insertAfter(node); + insertedNode = node; + } } return true; } diff --git a/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler_test.dart index ee21dfa455..5bfe1ada67 100644 --- a/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler_test.dart +++ b/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler_test.dart @@ -116,6 +116,27 @@ void main() async { (tester) async { _testMultipleSelection(tester, false); }); + + testWidgets('Presses enter key in the first line', (tester) async { + // Before + // + // Welcome to Appflowy 😁 + // + // After + // + // [Empty Line] + // Welcome to Appflowy 😁 + // + const text = 'Welcome to Appflowy 😁'; + final editor = tester.editor..insertTextNode(text); + await editor.startTesting(); + await editor.updateSelection( + Selection.single(path: [0], startOffset: 0), + ); + await editor.pressLogicKey(LogicalKeyboardKey.enter); + expect(editor.documentLength, 2); + expect((editor.nodeAtPath([1]) as TextNode).toRawString(), text); + }); }); } From dd9cac9c1d4a681d6fdb859d0bc199fe7c12c189 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Mon, 29 Aug 2022 12:02:47 +0800 Subject: [PATCH 062/106] feat: highlight selection when tap on the link menu --- .../lib/src/render/editor/editor_entry.dart | 43 ++++++++++--------- .../lib/src/render/link_menu/link_menu.dart | 8 ++++ .../src/render/rich_text/flowy_rich_text.dart | 7 ++- .../lib/src/render/toolbar/toolbar_item.dart | 29 ++++++++++--- .../test/render/link_menu/link_menu_test.dart | 1 + 5 files changed, 59 insertions(+), 29 deletions(-) diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/editor/editor_entry.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/editor/editor_entry.dart index e71dc7c79b..d14d44613f 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/editor/editor_entry.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/editor/editor_entry.dart @@ -32,26 +32,29 @@ class EditorNodeWidget extends StatelessWidget { @override Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: node.children - .map( - (child) => - editorState.service.renderPluginService.buildPluginWidget( - child is TextNode - ? NodeWidgetContext( - context: context, - node: child, - editorState: editorState, - ) - : NodeWidgetContext( - context: context, - node: child, - editorState: editorState, - ), - ), - ) - .toList(), + return Container( + color: Colors.red.withOpacity(0.1), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: node.children + .map( + (child) => + editorState.service.renderPluginService.buildPluginWidget( + child is TextNode + ? NodeWidgetContext( + context: context, + node: child, + editorState: editorState, + ) + : NodeWidgetContext( + context: context, + node: child, + editorState: editorState, + ), + ), + ) + .toList(), + ), ); } } diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/link_menu/link_menu.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/link_menu/link_menu.dart index 07e1b947eb..13396a33c4 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/link_menu/link_menu.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/link_menu/link_menu.dart @@ -9,6 +9,7 @@ class LinkMenu extends StatefulWidget { required this.onOpenLink, required this.onCopyLink, required this.onRemoveLink, + required this.onFocusChange, }) : super(key: key); final String? linkText; @@ -16,6 +17,7 @@ class LinkMenu extends StatefulWidget { final VoidCallback onOpenLink; final VoidCallback onCopyLink; final VoidCallback onRemoveLink; + final void Function(bool value) onFocusChange; @override State createState() => _LinkMenuState(); @@ -29,11 +31,13 @@ class _LinkMenuState extends State { void initState() { super.initState(); _textEditingController.text = widget.linkText ?? ''; + _focusNode.addListener(_onFocusChange); } @override void dispose() { _textEditingController.dispose(); + _focusNode.removeListener(_onFocusChange); super.dispose(); } @@ -157,4 +161,8 @@ class _LinkMenuState extends State { onPressed: onPressed, ); } + + void _onFocusChange() { + widget.onFocusChange(_focusNode.hasFocus); + } } diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/flowy_rich_text.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/flowy_rich_text.dart index 517f7dd4b8..473f29eaa7 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/flowy_rich_text.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/flowy_rich_text.dart @@ -257,8 +257,11 @@ class _FlowyRichTextState extends State with Selectable { timer = Timer(const Duration(milliseconds: 200), () { tapCount = 0; WidgetsBinding.instance.addPostFrameCallback((timeStamp) { - showLinkMenu(context, widget.editorState, - customSelection: selection); + showLinkMenu( + context, + widget.editorState, + customSelection: selection, + ); }); }); }; diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item.dart index 979f86cdd1..34b38c0444 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item.dart @@ -158,6 +158,7 @@ ToolbarShowValidator _showInTextSelection = (editorState) { OverlayEntry? _linkMenuOverlay; EditorState? _editorState; +bool _changeSelectionInner = false; void showLinkMenu( BuildContext context, EditorState editorState, { @@ -180,17 +181,17 @@ void showLinkMenu( // We get the text node directly instead of judging details again. final selection = customSelection ?? editorState.service.selectionService.currentSelection.value; - if (selection == null) { + final node = editorState.service.selectionService.currentSelectedNodes; + if (selection == null || node.isEmpty || node.first is! TextNode) { return; } final index = selection.isBackward ? selection.start.offset : selection.end.offset; final length = (selection.start.offset - selection.end.offset).abs(); - final node = editorState.service.selectionService.currentSelectedNodes.first - as TextNode; + final textNode = node.first as TextNode; String? linkText; - if (node.allSatisfyLinkInSelection(selection)) { - linkText = node.getAttributeInSelection(selection, StyleKey.href); + if (textNode.allSatisfyLinkInSelection(selection)) { + linkText = textNode.getAttributeInSelection(selection, StyleKey.href); } _linkMenuOverlay = OverlayEntry(builder: (context) { return Positioned( @@ -204,7 +205,7 @@ void showLinkMenu( }, onSubmitted: (text) { TransactionBuilder(editorState) - ..formatText(node, index, length, {StyleKey.href: text}) + ..formatText(textNode, index, length, {StyleKey.href: text}) ..commit(); _dismissLinkMenu(); }, @@ -214,10 +215,17 @@ void showLinkMenu( }, onRemoveLink: () { TransactionBuilder(editorState) - ..formatText(node, index, length, {StyleKey.href: null}) + ..formatText(textNode, index, length, {StyleKey.href: null}) ..commit(); _dismissLinkMenu(); }, + onFocusChange: (value) { + if (value && customSelection != null) { + _changeSelectionInner = true; + editorState.service.selectionService + .updateSelection(customSelection); + } + }, ), ), ); @@ -230,6 +238,13 @@ void showLinkMenu( } void _dismissLinkMenu() { + if (_editorState?.service.selectionService.currentSelection.value == null) { + return; + } + if (_changeSelectionInner) { + _changeSelectionInner = false; + return; + } _linkMenuOverlay?.remove(); _linkMenuOverlay = null; diff --git a/frontend/app_flowy/packages/appflowy_editor/test/render/link_menu/link_menu_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/render/link_menu/link_menu_test.dart index 5b102b9ec1..cef16a1cec 100644 --- a/frontend/app_flowy/packages/appflowy_editor/test/render/link_menu/link_menu_test.dart +++ b/frontend/app_flowy/packages/appflowy_editor/test/render/link_menu/link_menu_test.dart @@ -15,6 +15,7 @@ void main() async { onOpenLink: () {}, onCopyLink: () {}, onRemoveLink: () {}, + onFocusChange: (value) {}, onSubmitted: (text) { submittedText = text; }, From 42866e105702a91007f77afd6de5140c788013ad Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Mon, 29 Aug 2022 12:10:57 +0800 Subject: [PATCH 063/106] fix: The cursor will not disappear after clicking in an area outside the editor. --- .../lib/src/service/keyboard_service.dart | 4 ++++ .../lib/src/service/selection_service.dart | 14 ++++++++++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/keyboard_service.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/keyboard_service.dart index 1867574993..dee3f42725 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/keyboard_service.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/keyboard_service.dart @@ -129,6 +129,10 @@ class _AppFlowyKeyboardState extends State void _onFocusChange(bool value) { Log.keyboard.debug('on keyboard event focus change $value'); + isFocus = value; + if (!value) { + widget.editorState.service.selectionService.clearCursor(); + } } KeyEventResult _onKey(FocusNode node, RawKeyEvent event) { diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/selection_service.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/selection_service.dart index c5e351059c..6f6897596f 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/selection_service.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/selection_service.dart @@ -57,6 +57,9 @@ abstract class AppFlowySelectionService { /// Clears the selection area, cursor area and the popup list area. void clearSelection(); + /// Clears the cursor area. + void clearCursor(); + /// Returns the [Node]s in [Selection]. List getNodesInSelection(Selection selection); @@ -205,16 +208,23 @@ class _AppFlowySelectionState extends State currentSelectedNodes = []; currentSelection.value = null; + clearCursor(); // clear selection areas _selectionAreas ..forEach((overlay) => overlay.remove()) ..clear(); // clear cursor areas + + // hide toolbar + editorState.service.toolbarService?.hide(); + } + + @override + void clearCursor() { + // clear cursor areas _cursorAreas ..forEach((overlay) => overlay.remove()) ..clear(); - // hide toolbar - editorState.service.toolbarService?.hide(); } @override From e567158cee02da9199cda2e6194dddc27a7c0d4a Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Mon, 29 Aug 2022 13:41:20 +0800 Subject: [PATCH 064/106] fix: The click area of the linked text is too large. --- .../appflowy_editor/example/lib/main.dart | 9 ++-- .../lib/src/render/editor/editor_entry.dart | 43 +++++++++---------- .../render/rich_text/bulleted_list_text.dart | 8 ++-- .../src/render/rich_text/checkbox_text.dart | 2 +- .../src/render/rich_text/flowy_rich_text.dart | 4 +- .../src/render/rich_text/heading_text.dart | 4 +- .../render/rich_text/number_list_text.dart | 2 +- .../lib/src/render/rich_text/quoted_text.dart | 2 +- .../lib/src/render/rich_text/rich_text.dart | 4 +- .../lib/src/render/toolbar/toolbar_item.dart | 6 +++ 10 files changed, 46 insertions(+), 38 deletions(-) diff --git a/frontend/app_flowy/packages/appflowy_editor/example/lib/main.dart b/frontend/app_flowy/packages/appflowy_editor/example/lib/main.dart index 0184138fb5..8f6bf64c30 100644 --- a/frontend/app_flowy/packages/appflowy_editor/example/lib/main.dart +++ b/frontend/app_flowy/packages/appflowy_editor/example/lib/main.dart @@ -46,9 +46,12 @@ class _MyHomePageState extends State { Widget build(BuildContext context) { return Scaffold( extendBodyBehindAppBar: true, - body: Container( - alignment: Alignment.topCenter, - child: _buildEditor(context), + body: Center( + child: Container( + width: 780, + alignment: Alignment.topCenter, + child: _buildEditor(context), + ), ), floatingActionButton: _buildExpandableFab(), ); diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/editor/editor_entry.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/editor/editor_entry.dart index d14d44613f..4167ca1b38 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/editor/editor_entry.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/editor/editor_entry.dart @@ -32,29 +32,26 @@ class EditorNodeWidget extends StatelessWidget { @override Widget build(BuildContext context) { - return Container( - color: Colors.red.withOpacity(0.1), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: node.children - .map( - (child) => - editorState.service.renderPluginService.buildPluginWidget( - child is TextNode - ? NodeWidgetContext( - context: context, - node: child, - editorState: editorState, - ) - : NodeWidgetContext( - context: context, - node: child, - editorState: editorState, - ), - ), - ) - .toList(), - ), + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: node.children + .map( + (child) => + editorState.service.renderPluginService.buildPluginWidget( + child is TextNode + ? NodeWidgetContext( + context: context, + node: child, + editorState: editorState, + ) + : NodeWidgetContext( + context: context, + node: child, + editorState: editorState, + ), + ), + ) + .toList(), ); } } diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/bulleted_list_text.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/bulleted_list_text.dart index 7d69ff459f..5408f862d8 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/bulleted_list_text.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/bulleted_list_text.dart @@ -56,8 +56,8 @@ class _BulletedListTextNodeWidgetState extends State @override Widget build(BuildContext context) { - return SizedBox( - width: defaultMaxTextNodeWidth, + return Container( + constraints: BoxConstraints(maxWidth: defaultMaxTextNodeWidth), child: Padding( padding: EdgeInsets.only(bottom: defaultLinePadding), child: Row( @@ -70,14 +70,14 @@ class _BulletedListTextNodeWidgetState extends State padding: EdgeInsets.only(right: _iconRightPadding), name: 'point', ), - Expanded( + Flexible( child: FlowyRichText( key: _richTextKey, placeholderText: 'List', textNode: widget.textNode, editorState: widget.editorState, ), - ), + ) ], ), ), diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/checkbox_text.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/checkbox_text.dart index 0255a84049..bfda4e3f73 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/checkbox_text.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/checkbox_text.dart @@ -86,7 +86,7 @@ class _CheckboxNodeWidgetState extends State ..commit(); }, ), - Expanded( + Flexible( child: FlowyRichText( key: _richTextKey, placeholderText: 'To-do', diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/flowy_rich_text.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/flowy_rich_text.dart index 473f29eaa7..3489c2bb52 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/flowy_rich_text.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/flowy_rich_text.dart @@ -194,7 +194,9 @@ class _FlowyRichTextState extends State with Selectable { return RichText( key: _textKey, textHeightBehavior: const TextHeightBehavior( - applyHeightToFirstAscent: false, applyHeightToLastDescent: false), + applyHeightToFirstAscent: false, + applyHeightToLastDescent: false, + ), text: widget.textSpanDecorator != null ? widget.textSpanDecorator!(textSpan) : textSpan, diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/heading_text.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/heading_text.dart index 050b330f8b..7b94783f03 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/heading_text.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/heading_text.dart @@ -63,8 +63,8 @@ class _HeadingTextNodeWidgetState extends State top: _topPadding, bottom: defaultLinePadding, ), - child: SizedBox( - width: defaultMaxTextNodeWidth, + child: Container( + constraints: BoxConstraints(maxWidth: defaultMaxTextNodeWidth), child: FlowyRichText( key: _richTextKey, placeholderText: 'Heading', diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/number_list_text.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/number_list_text.dart index c1062e1c3c..a4d72bb011 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/number_list_text.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/number_list_text.dart @@ -70,7 +70,7 @@ class _NumberListTextNodeWidgetState extends State padding: EdgeInsets.only(right: _iconRightPadding), number: widget.textNode.attributes.number, ), - Expanded( + Flexible( child: FlowyRichText( key: _richTextKey, placeholderText: 'List', diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/quoted_text.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/quoted_text.dart index 78c6653904..04ae379799 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/quoted_text.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/quoted_text.dart @@ -69,7 +69,7 @@ class _QuotedTextNodeWidgetState extends State padding: EdgeInsets.only(right: _iconRightPadding), name: 'quote', ), - Expanded( + Flexible( child: FlowyRichText( key: _richTextKey, placeholderText: 'Quote', diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/rich_text.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/rich_text.dart index d8dcfb91f6..5fe65db4b7 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/rich_text.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/rich_text.dart @@ -52,8 +52,8 @@ class _RichTextNodeWidgetState extends State @override Widget build(BuildContext context) { - return SizedBox( - width: defaultMaxTextNodeWidth, + return Container( + constraints: BoxConstraints(maxWidth: defaultMaxTextNodeWidth), child: Padding( padding: EdgeInsets.only(bottom: defaultLinePadding), child: FlowyRichText( diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item.dart index 34b38c0444..9a1b2f1c02 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item.dart @@ -238,6 +238,12 @@ void showLinkMenu( } void _dismissLinkMenu() { + // workaround: SelectionService has been released after hot reload. + final isSelectionDisposed = + _editorState?.service.selectionServiceKey.currentState == null; + if (isSelectionDisposed) { + return; + } if (_editorState?.service.selectionService.currentSelection.value == null) { return; } From 32bce890f520544a9065bcedcd7e8b950e01c4a5 Mon Sep 17 00:00:00 2001 From: chiragkr04 Date: Mon, 29 Aug 2022 11:54:59 +0530 Subject: [PATCH 065/106] fix: added tooltip for grid row leading buttons --- frontend/app_flowy/assets/translations/en.json | 8 +++----- .../plugins/grid/presentation/widgets/row/grid_row.dart | 8 +++++++- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/frontend/app_flowy/assets/translations/en.json b/frontend/app_flowy/assets/translations/en.json index c68cfe4263..39ba056cb9 100644 --- a/frontend/app_flowy/assets/translations/en.json +++ b/frontend/app_flowy/assets/translations/en.json @@ -95,7 +95,9 @@ "tooltip": { "lightMode": "Switch to Light mode", "darkMode": "Switch to Dark mode", - "openAsPage": "Open as a Page" + "openAsPage": "Open as a Page", + "addNewRow": "Add a new row", + "openMenu": "Click to open menu" }, "notifications": { "export": { @@ -215,9 +217,5 @@ "timeHintTextInTwelveHour": "12:00 AM", "timeHintTextInTwentyFourHour": "12:00" } - }, - "sideBar": { - "openSidebar": "Open sidebar", - "closeSidebar": "Close sidebar" } } diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/grid_row.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/grid_row.dart index a4bf813fe5..d560f03fc0 100755 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/grid_row.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/grid_row.dart @@ -14,6 +14,8 @@ import '../cell/cell_accessory.dart'; import '../cell/cell_container.dart'; import '../cell/prelude.dart'; import 'row_action_sheet.dart'; +import "package:app_flowy/generated/locale_keys.g.dart"; +import 'package:easy_localization/easy_localization.dart'; class GridRowWidget extends StatefulWidget { final RowInfo rowInfo; @@ -122,10 +124,13 @@ class _InsertRowButton extends StatelessWidget { Widget build(BuildContext context) { final theme = context.watch(); return FlowyIconButton( + tooltipText: LocaleKeys.tooltip_addNewRow.tr(), hoverColor: theme.hover, width: 20, height: 30, - onPressed: () => context.read().add(const RowEvent.createRow()), + onPressed: () => context.read().add( + const RowEvent.createRow(), + ), iconPadding: const EdgeInsets.all(3), icon: svgWidget("home/add"), ); @@ -139,6 +144,7 @@ class _DeleteRowButton extends StatelessWidget { Widget build(BuildContext context) { final theme = context.watch(); return FlowyIconButton( + tooltipText: LocaleKeys.tooltip_openMenu.tr(), hoverColor: theme.hover, width: 20, height: 30, From e42bfe0b1eedc8530d1fbb931c40990f13efda75 Mon Sep 17 00:00:00 2001 From: appflowy Date: Mon, 29 Aug 2022 14:37:38 +0800 Subject: [PATCH 066/106] chore: ignore dragtarget event when performing insert animation --- .../lib/src/widgets/reorder_flex/reorder_flex.dart | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/reorder_flex.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/reorder_flex.dart index b0f511f641..63bd9638b4 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/reorder_flex.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/reorder_flex.dart @@ -82,6 +82,8 @@ class ReorderFlex extends StatefulWidget { final ReorderDragTargetIndexKeyStorage? dragTargetIndexKeyStorage; + final bool reorderable; + ReorderFlex({ Key? key, this.scrollController, @@ -89,6 +91,7 @@ class ReorderFlex extends StatefulWidget { required this.children, required this.config, required this.onReorder, + this.reorderable = true, this.dragStateStorage, this.dragTargetIndexKeyStorage, this.onDragStarted, @@ -385,7 +388,7 @@ class ReorderFlexState extends State }, onWillAccept: (FlexDragTargetData dragTargetData) { // Do not receive any events if the Insert item is animating. - if (_animation.deleteController.isAnimating) { + if (_animation.insertController.isAnimating) { return false; } @@ -421,6 +424,7 @@ class ReorderFlexState extends State deleteAnimationController: _animation.deleteController, draggableTargetBuilder: widget.interceptor?.draggableTargetBuilder, useMoveAnimation: widget.config.useMoveAnimation, + draggable: widget.reorderable, child: child, ); } From 0eac23be6af84aa17ac295752394782b5cfbc697 Mon Sep 17 00:00:00 2001 From: chiragkr04 Date: Mon, 29 Aug 2022 12:23:06 +0530 Subject: [PATCH 067/106] fix: .tr() missing in side bar tooltip text --- frontend/app_flowy/assets/translations/en.json | 4 ++++ .../app_flowy/lib/workspace/presentation/home/menu/menu.dart | 5 +++-- .../lib/workspace/presentation/home/navigation.dart | 3 ++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/frontend/app_flowy/assets/translations/en.json b/frontend/app_flowy/assets/translations/en.json index 39ba056cb9..eca01d1bed 100644 --- a/frontend/app_flowy/assets/translations/en.json +++ b/frontend/app_flowy/assets/translations/en.json @@ -99,6 +99,10 @@ "addNewRow": "Add a new row", "openMenu": "Click to open menu" }, + "sideBar": { + "closeSidebar": "Close side bar", + "openSidebar": "Open side bar" + }, "notifications": { "export": { "markdown": "Exported Note To Markdown", diff --git a/frontend/app_flowy/lib/workspace/presentation/home/menu/menu.dart b/frontend/app_flowy/lib/workspace/presentation/home/menu/menu.dart index d978cc5ff6..f9704a695f 100644 --- a/frontend/app_flowy/lib/workspace/presentation/home/menu/menu.dart +++ b/frontend/app_flowy/lib/workspace/presentation/home/menu/menu.dart @@ -26,6 +26,7 @@ import 'package:app_flowy/core/frameless_window.dart'; // import 'package:app_flowy/workspace/presentation/home/home_sizes.dart'; import 'package:flowy_infra/image.dart'; import 'package:flowy_infra_ui/style_widget/icon_button.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'app/menu_app.dart'; import 'app/create_button.dart'; @@ -220,8 +221,8 @@ class MenuTopBar extends StatelessWidget { const Spacer(), Tooltip( richMessage: TextSpan(children: [ - const TextSpan( - text: LocaleKeys.sideBar_closeSidebar + "\n"), + TextSpan( + text: LocaleKeys.sideBar_closeSidebar.tr() + "\n"), TextSpan( text: Platform.isMacOS ? "⌘+\\" : "Ctrl+\\", style: const TextStyle(color: Colors.white60), diff --git a/frontend/app_flowy/lib/workspace/presentation/home/navigation.dart b/frontend/app_flowy/lib/workspace/presentation/home/navigation.dart index 9513a20ccc..019a567932 100644 --- a/frontend/app_flowy/lib/workspace/presentation/home/navigation.dart +++ b/frontend/app_flowy/lib/workspace/presentation/home/navigation.dart @@ -11,6 +11,7 @@ import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:styled_widget/styled_widget.dart'; +import 'package:easy_localization/easy_localization.dart'; typedef NaviAction = void Function(); @@ -100,7 +101,7 @@ class FlowyNavigation extends StatelessWidget { turns: const AlwaysStoppedAnimation(180 / 360), child: Tooltip( richMessage: TextSpan(children: [ - const TextSpan(text: LocaleKeys.sideBar_openSidebar + "\n"), + TextSpan(text: LocaleKeys.sideBar_openSidebar.tr() + "\n"), TextSpan( text: Platform.isMacOS ? "⌘+\\" : "Ctrl+\\", style: const TextStyle(color: Colors.white60), From 4e8308b8342565dcb343752ba87f5c221f06798c Mon Sep 17 00:00:00 2001 From: appflowy Date: Mon, 29 Aug 2022 15:42:08 +0800 Subject: [PATCH 068/106] chore: disable moving column --- .../card/board_text_cell_bloc.dart | 8 ++ .../card/board_select_option_cell.dart | 41 +++++++++-- .../presentation/card/board_text_cell.dart | 41 +++++++---- .../plugins/board/presentation/card/card.dart | 24 ++++-- .../flutter/generated_plugin_registrant.cc | 4 + .../linux/flutter/generated_plugins.cmake | 1 + .../Flutter/GeneratedPluginRegistrant.swift | 2 + .../example/lib/multi_board_list_example.dart | 73 ++++++++++++------- .../appflowy_board/lib/src/utils/log.dart | 4 +- .../appflowy_board/lib/src/widgets/board.dart | 13 +++- .../widgets/board_column/board_column.dart | 4 +- .../src/widgets/reorder_flex/drag_target.dart | 5 ++ .../reorder_flex/drag_target_interceptor.dart | 3 +- .../reorder_phantom/phantom_controller.dart | 35 +++++---- .../reorder_phantom/phantom_state.dart | 2 +- 15 files changed, 183 insertions(+), 77 deletions(-) diff --git a/frontend/app_flowy/lib/plugins/board/application/card/board_text_cell_bloc.dart b/frontend/app_flowy/lib/plugins/board/application/card/board_text_cell_bloc.dart index e11d7b5ac6..3df8b029f8 100644 --- a/frontend/app_flowy/lib/plugins/board/application/card/board_text_cell_bloc.dart +++ b/frontend/app_flowy/lib/plugins/board/application/card/board_text_cell_bloc.dart @@ -1,4 +1,5 @@ import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'dart:async'; @@ -20,6 +21,12 @@ class BoardTextCellBloc extends Bloc { didReceiveCellUpdate: (content) { emit(state.copyWith(content: content)); }, + updateText: (text) { + if (text != state.content) { + cellController.saveCellData(text); + emit(state.copyWith(content: text)); + } + }, ); }, ); @@ -49,6 +56,7 @@ class BoardTextCellBloc extends Bloc { @freezed class BoardTextCellEvent with _$BoardTextCellEvent { const factory BoardTextCellEvent.initial() = _InitialCell; + const factory BoardTextCellEvent.updateText(String text) = _UpdateContent; const factory BoardTextCellEvent.didReceiveCellUpdate(String cellContent) = _DidReceiveCellUpdate; } diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/board_select_option_cell.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/board_select_option_cell.dart index b2383bd937..e0607db306 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/board_select_option_cell.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/board_select_option_cell.dart @@ -1,6 +1,7 @@ import 'package:app_flowy/plugins/board/application/card/board_select_option_cell_bloc.dart'; import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart'; import 'package:app_flowy/plugins/grid/presentation/widgets/cell/select_option_cell/extension.dart'; +import 'package:app_flowy/plugins/grid/presentation/widgets/cell/select_option_cell/select_option_editor.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -40,8 +41,9 @@ class _BoardSelectOptionCellState extends State { }, builder: (context, state) { if (state.selectedOptions - .where((element) => element.id == widget.groupId) - .isNotEmpty) { + .where((element) => element.id == widget.groupId) + .isNotEmpty || + state.selectedOptions.isEmpty) { return const SizedBox(); } else { final children = state.selectedOptions @@ -52,10 +54,17 @@ class _BoardSelectOptionCellState extends State { ), ) .toList(); - return Align( - alignment: Alignment.centerLeft, - child: AbsorbPointer( - child: Wrap(children: children, spacing: 4, runSpacing: 2), + + return IntrinsicHeight( + child: Stack( + alignment: AlignmentDirectional.center, + fit: StackFit.expand, + children: [ + Wrap(children: children, spacing: 4, runSpacing: 2), + _SelectOptionDialog( + controller: widget.cellControllerBuilder.build(), + ), + ], ), ); } @@ -70,3 +79,23 @@ class _BoardSelectOptionCellState extends State { super.dispose(); } } + +class _SelectOptionDialog extends StatelessWidget { + final GridSelectOptionCellController _controller; + const _SelectOptionDialog({ + Key? key, + required IGridCellController controller, + }) : _controller = controller as GridSelectOptionCellController, + super(key: key); + + @override + Widget build(BuildContext context) { + return InkWell(onTap: () { + SelectOptionCellEditor.show( + context, + _controller, + () {}, + ); + }); + } +} diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/board_text_cell.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/board_text_cell.dart index deea60e793..ed683005f9 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/board_text_cell.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/board_text_cell.dart @@ -1,5 +1,6 @@ import 'package:app_flowy/plugins/board/application/card/board_text_cell_bloc.dart'; import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart'; +import 'package:app_flowy/plugins/grid/presentation/widgets/cell/cell_builder.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -19,14 +20,16 @@ class BoardTextCell extends StatefulWidget { class _BoardTextCellState extends State { late BoardTextCellBloc _cellBloc; + late TextEditingController _controller; + SingleListenerFocusNode focusNode = SingleListenerFocusNode(); @override void initState() { final cellController = widget.cellControllerBuilder.build() as GridCellController; - _cellBloc = BoardTextCellBloc(cellController: cellController) ..add(const BoardTextCellEvent.initial()); + _controller = TextEditingController(text: _cellBloc.state.content); super.initState(); } @@ -34,28 +37,38 @@ class _BoardTextCellState extends State { Widget build(BuildContext context) { return BlocProvider.value( value: _cellBloc, - child: BlocBuilder( - buildWhen: (previous, current) => previous.content != current.content, - builder: (context, state) { - if (state.content.isEmpty) { - return const SizedBox(); - } else { - return Align( - alignment: Alignment.centerLeft, - child: ConstrainedBox( - constraints: const BoxConstraints(maxHeight: 120), - child: FlowyText.medium(state.content, fontSize: 14), - ), - ); + child: BlocListener( + listener: (context, state) { + if (_controller.text != state.content) { + _controller.text = state.content; } }, + child: TextField( + controller: _controller, + focusNode: focusNode, + onChanged: (value) => focusChanged(), + onEditingComplete: () => focusNode.unfocus(), + maxLines: 1, + style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500), + decoration: const InputDecoration( + contentPadding: EdgeInsets.symmetric(vertical: 6), + border: InputBorder.none, + isDense: true, + ), + ), ), ); } + Future focusChanged() async { + _cellBloc.add(BoardTextCellEvent.updateText(_controller.text)); + } + @override Future dispose() async { _cellBloc.close(); + _controller.dispose(); + focusNode.dispose(); super.dispose(); } } diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart index dfb8149c9e..9f919f2dd1 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart @@ -68,6 +68,7 @@ class _BoardCardState extends State { widget.openCard(context); }, child: Column( + mainAxisSize: MainAxisSize.min, children: _makeCells( context, state.cells.map((cell) => cell.identifier).toList(), @@ -83,15 +84,24 @@ class _BoardCardState extends State { BuildContext context, List cells, ) { - return cells.map( - (GridCellIdentifier cellId) { + final List children = []; + cells.asMap().forEach( + (int index, GridCellIdentifier cellId) { final child = widget.cellBuilder.buildCell(widget.groupId, cellId); - return Padding( - padding: const EdgeInsets.only(left: 4, right: 4, top: 6), - child: child, - ); + if (index != 0) { + children.add(Padding( + padding: const EdgeInsets.only(left: 4, right: 4, top: 8), + child: child, + )); + } else { + children.add(Padding( + padding: const EdgeInsets.only(left: 4, right: 4), + child: child, + )); + } }, - ).toList(); + ); + return children; } @override diff --git a/frontend/app_flowy/linux/flutter/generated_plugin_registrant.cc b/frontend/app_flowy/linux/flutter/generated_plugin_registrant.cc index ae6ec7ed89..f05fb593f4 100644 --- a/frontend/app_flowy/linux/flutter/generated_plugin_registrant.cc +++ b/frontend/app_flowy/linux/flutter/generated_plugin_registrant.cc @@ -7,6 +7,7 @@ #include "generated_plugin_registrant.h" #include +#include #include #include #include @@ -15,6 +16,9 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) flowy_infra_ui_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "FlowyInfraUIPlugin"); flowy_infra_u_i_plugin_register_with_registrar(flowy_infra_ui_registrar); + g_autoptr(FlPluginRegistrar) hotkey_manager_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "HotkeyManagerPlugin"); + hotkey_manager_plugin_register_with_registrar(hotkey_manager_registrar); g_autoptr(FlPluginRegistrar) rich_clipboard_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "RichClipboardPlugin"); rich_clipboard_plugin_register_with_registrar(rich_clipboard_linux_registrar); diff --git a/frontend/app_flowy/linux/flutter/generated_plugins.cmake b/frontend/app_flowy/linux/flutter/generated_plugins.cmake index 3e0d068b6a..ce38abcac0 100644 --- a/frontend/app_flowy/linux/flutter/generated_plugins.cmake +++ b/frontend/app_flowy/linux/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST flowy_infra_ui + hotkey_manager rich_clipboard_linux url_launcher_linux window_size diff --git a/frontend/app_flowy/macos/Flutter/GeneratedPluginRegistrant.swift b/frontend/app_flowy/macos/Flutter/GeneratedPluginRegistrant.swift index 60d2b8c792..2f24aad58b 100644 --- a/frontend/app_flowy/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/frontend/app_flowy/macos/Flutter/GeneratedPluginRegistrant.swift @@ -9,6 +9,7 @@ import connectivity_plus_macos import device_info_plus_macos import flowy_infra_ui import flowy_sdk +import hotkey_manager import package_info_plus_macos import path_provider_macos import rich_clipboard_macos @@ -21,6 +22,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) FlowyInfraUIPlugin.register(with: registry.registrar(forPlugin: "FlowyInfraUIPlugin")) FlowySdkPlugin.register(with: registry.registrar(forPlugin: "FlowySdkPlugin")) + HotkeyManagerPlugin.register(with: registry.registrar(forPlugin: "HotkeyManagerPlugin")) FLTPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FLTPackageInfoPlusPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) RichClipboardPlugin.register(with: registry.registrar(forPlugin: "RichClipboardPlugin")) diff --git a/frontend/app_flowy/packages/appflowy_board/example/lib/multi_board_list_example.dart b/frontend/app_flowy/packages/appflowy_board/example/lib/multi_board_list_example.dart index 170738b2e4..7b9a84b6d2 100644 --- a/frontend/app_flowy/packages/appflowy_board/example/lib/multi_board_list_example.dart +++ b/frontend/app_flowy/packages/appflowy_board/example/lib/multi_board_list_example.dart @@ -11,13 +11,13 @@ class MultiBoardListExample extends StatefulWidget { class _MultiBoardListExampleState extends State { final AFBoardDataController boardDataController = AFBoardDataController( onMoveColumn: (fromColumnId, fromIndex, toColumnId, toIndex) { - debugPrint('Move column from $fromIndex to $toIndex'); + // debugPrint('Move column from $fromIndex to $toIndex'); }, onMoveColumnItem: (columnId, fromIndex, toIndex) { - debugPrint('Move $columnId:$fromIndex to $columnId:$toIndex'); + // debugPrint('Move $columnId:$fromIndex to $columnId:$toIndex'); }, onMoveColumnItemToColumn: (fromColumnId, fromIndex, toColumnId, toIndex) { - debugPrint('Move $fromColumnId:$fromIndex to $toColumnId:$toIndex'); + // debugPrint('Move $fromColumnId:$fromIndex to $toColumnId:$toIndex'); }, ); @@ -96,7 +96,7 @@ class _MultiBoardListExampleState extends State { }, cardBuilder: (context, column, columnItem) { return AppFlowyColumnItemCard( - key: ObjectKey(columnItem), + key: ValueKey(columnItem.id), child: _buildCard(columnItem), ); }, @@ -121,33 +121,56 @@ class _MultiBoardListExampleState extends State { } if (item is RichTextItem) { - return Align( - alignment: Alignment.centerLeft, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 20), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - item.title, - style: const TextStyle(fontSize: 14), - textAlign: TextAlign.left, - ), - const SizedBox(height: 10), - Text( - item.subtitle, - style: const TextStyle(fontSize: 12, color: Colors.grey), - ) - ], - ), - ), - ); + return RichTextCard(item: item); } throw UnimplementedError(); } } +class RichTextCard extends StatefulWidget { + final RichTextItem item; + const RichTextCard({ + required this.item, + Key? key, + }) : super(key: key); + + @override + State createState() => _RichTextCardState(); +} + +class _RichTextCardState extends State { + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Align( + alignment: Alignment.centerLeft, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.item.title, + style: const TextStyle(fontSize: 14), + textAlign: TextAlign.left, + ), + const SizedBox(height: 10), + Text( + widget.item.subtitle, + style: const TextStyle(fontSize: 12, color: Colors.grey), + ) + ], + ), + ), + ); + } +} + class TextItem extends AFColumnItem { final String s; diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/utils/log.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/utils/log.dart index 9c23060b26..bd2f39e926 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/utils/log.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/utils/log.dart @@ -4,7 +4,7 @@ import 'package:flutter/material.dart'; const DART_LOG = "Dart_LOG"; class Log { - static const enableLog = false; + static const enableLog = true; static void info(String? message) { if (enableLog) { @@ -26,7 +26,7 @@ class Log { static void trace(String? message) { if (enableLog) { - debugPrint('❗️[Trace] - ${DateTime.now().second}=> $message'); + // debugPrint('❗️[Trace] - ${DateTime.now().second}=> $message'); } } } diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board.dart index 67d8111aec..3360325e8e 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board.dart @@ -64,7 +64,7 @@ class AFBoard extends StatelessWidget { final BoxConstraints columnConstraints; /// - final BoardPhantomController phantomController; + late final BoardPhantomController phantomController; final ScrollController? scrollController; @@ -85,8 +85,12 @@ class AFBoard extends StatelessWidget { this.columnConstraints = const BoxConstraints(maxWidth: 200), this.config = const AFBoardConfig(), Key? key, - }) : phantomController = BoardPhantomController(delegate: dataController), - super(key: key); + }) : super(key: key) { + phantomController = BoardPhantomController( + delegate: dataController, + columnsState: _columnState, + ); + } @override Widget build(BuildContext context) { @@ -194,6 +198,7 @@ class _AFBoardContentState extends State { dataSource: widget.dataController, direction: Axis.horizontal, interceptor: interceptor, + reorderable: false, children: _buildColumns(), ); @@ -244,7 +249,7 @@ class _AFBoardContentState extends State { child: Consumer( builder: (context, value, child) { final boardColumn = AFBoardColumnWidget( - key: PageStorageKey(columnData.id), + // key: PageStorageKey(columnData.id), // key: GlobalObjectKey(columnData.id), margin: _marginFromIndex(columnIndex), itemMargin: widget.config.columnItemPadding, diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column.dart index a89ef269cb..ee1b501e8f 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column.dart @@ -92,7 +92,7 @@ class AFBoardColumnWidget extends StatefulWidget { final ReorderDragTargetIndexKeyStorage? dragTargetIndexKeyStorage; - final GlobalKey globalKey; + final GlobalObjectKey globalKey; AFBoardColumnWidget({ Key? key, @@ -111,7 +111,7 @@ class AFBoardColumnWidget extends StatefulWidget { this.itemMargin = EdgeInsets.zero, this.cornerRadius = 0.0, this.backgroundColor = Colors.transparent, - }) : globalKey = GlobalKey(), + }) : globalKey = GlobalObjectKey(dataSource.columnData.id), config = const ReorderFlexConfig(), super(key: key); diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_target.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_target.dart index 92b71cd1f3..9da9e393ad 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_target.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_target.dart @@ -75,6 +75,7 @@ class ReorderDragTarget extends StatefulWidget { final AnimationController deleteAnimationController; final bool useMoveAnimation; + final bool draggable; const ReorderDragTarget({ Key? key, @@ -88,6 +89,7 @@ class ReorderDragTarget extends StatefulWidget { required this.insertAnimationController, required this.deleteAnimationController, required this.useMoveAnimation, + required this.draggable, this.onAccept, this.onLeave, this.draggableTargetBuilder, @@ -132,6 +134,9 @@ class _ReorderDragTargetState List acceptedCandidates, List rejectedCandidates, ) { + if (!widget.draggable) { + return widget.child; + } Widget feedbackBuilder = Builder(builder: (BuildContext context) { BoxConstraints contentSizeConstraints = BoxConstraints.loose(_draggingFeedbackSize!); diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_target_interceptor.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_target_interceptor.dart index a2d2cf9c6c..20f66c0921 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_target_interceptor.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_target_interceptor.dart @@ -131,6 +131,7 @@ class CrossReorderFlexDragTargetInterceptor extends DragTargetInterceptor { final String reorderFlexId; final List acceptedReorderFlexIds; final CrossReorderFlexDragTargetDelegate delegate; + @override final ReorderFlexDraggableTargetBuilder? draggableTargetBuilder; @@ -188,7 +189,7 @@ class CrossReorderFlexDragTargetInterceptor extends DragTargetInterceptor { ); Log.debug( - '[$CrossReorderFlexDragTargetInterceptor] dargTargetIndex: $dragTargetIndex, reorderFlexId: $reorderFlexId'); + '[$CrossReorderFlexDragTargetInterceptor] isNewDragTarget: $isNewDragTarget, dargTargetIndex: $dragTargetIndex, reorderFlexId: $reorderFlexId'); if (isNewDragTarget == false) { delegate.updateDragTargetData(reorderFlexId, dragTargetIndex); diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_phantom/phantom_controller.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_phantom/phantom_controller.dart index 14b525d67d..a3e0f5fb6e 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_phantom/phantom_controller.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_phantom/phantom_controller.dart @@ -1,3 +1,4 @@ +import 'package:appflowy_board/appflowy_board.dart'; import 'package:flutter/widgets.dart'; import '../../utils/log.dart'; @@ -39,8 +40,12 @@ class BoardPhantomController extends OverlapDragTargetDelegate with CrossReorderFlexDragTargetDelegate { PhantomRecord? phantomRecord; final BoardPhantomControllerDelegate delegate; - final columnsState = ColumnPhantomStateController(); - BoardPhantomController({required this.delegate}); + final BoardColumnsState columnsState; + final phantomState = ColumnPhantomState(); + BoardPhantomController({ + required this.delegate, + required this.columnsState, + }); bool isFromColumn(String columnId) { if (phantomRecord != null) { @@ -59,19 +64,19 @@ class BoardPhantomController extends OverlapDragTargetDelegate } void columnStartDragging(String columnId) { - columnsState.setColumnIsDragging(columnId, true); + phantomState.setColumnIsDragging(columnId, true); } /// Remove the phantom in the column when the column is end dragging. void columnEndDragging(String columnId) { - columnsState.setColumnIsDragging(columnId, false); + phantomState.setColumnIsDragging(columnId, false); if (phantomRecord == null) return; final fromColumnId = phantomRecord!.fromColumnId; final toColumnId = phantomRecord!.toColumnId; if (fromColumnId == columnId) { - columnsState.notifyDidRemovePhantom(toColumnId); + phantomState.notifyDidRemovePhantom(toColumnId); } if (phantomRecord!.toColumnId == columnId) { @@ -82,8 +87,8 @@ class BoardPhantomController extends OverlapDragTargetDelegate phantomRecord!.toColumnIndex, ); - Log.debug( - "[$BoardPhantomController] did move ${phantomRecord.toString()}"); + // Log.debug( + // "[$BoardPhantomController] did move ${phantomRecord.toString()}"); phantomRecord = null; } } @@ -91,8 +96,8 @@ class BoardPhantomController extends OverlapDragTargetDelegate /// Remove the phantom in the column if it contains phantom void _removePhantom(String columnId) { if (delegate.removePhantom(columnId)) { - columnsState.notifyDidRemovePhantom(columnId); - columnsState.removeColumnListener(columnId); + phantomState.notifyDidRemovePhantom(columnId); + phantomState.removeColumnListener(columnId); } } @@ -105,7 +110,7 @@ class BoardPhantomController extends OverlapDragTargetDelegate index: phantomIndex, dragTargetData: dragTargetData, ); - columnsState.addColumnListener(toColumnId, phantomContext); + phantomState.addColumnListener(toColumnId, phantomContext); delegate.insertPhantom( toColumnId, @@ -113,7 +118,7 @@ class BoardPhantomController extends OverlapDragTargetDelegate PhantomColumnItem(phantomContext), ); - columnsState.notifyDidInsertPhantom(toColumnId, phantomIndex); + phantomState.notifyDidInsertPhantom(toColumnId, phantomIndex); } /// Reset or initial the [PhantomRecord] @@ -150,7 +155,8 @@ class BoardPhantomController extends OverlapDragTargetDelegate if (phantomRecord == null) { _resetPhantomRecord(reorderFlexId, dragTargetData, dragTargetIndex); _insertPhantom(reorderFlexId, dragTargetData, dragTargetIndex); - return false; + + return true; } final isNewDragTarget = phantomRecord!.toColumnId != reorderFlexId; @@ -204,7 +210,7 @@ class BoardPhantomController extends OverlapDragTargetDelegate @override int getInsertedIndex(String dragTargetId) { - if (columnsState.isDragging(dragTargetId)) { + if (phantomState.isDragging(dragTargetId)) { return -1; } @@ -243,8 +249,7 @@ class PhantomRecord { if (fromColumnIndex == index) { return; } - Log.debug( - '[$PhantomRecord] Update Column:[$fromColumnId] remove position to $index'); + fromColumnIndex = index; } diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_phantom/phantom_state.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_phantom/phantom_state.dart index 443d7fb936..c550ee3bca 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_phantom/phantom_state.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_phantom/phantom_state.dart @@ -1,7 +1,7 @@ import 'phantom_controller.dart'; import 'package:flutter/material.dart'; -class ColumnPhantomStateController { +class ColumnPhantomState { final _states = {}; void setColumnIsDragging(String columnId, bool isDragging) { From 3f38e246ea294f6f21f8e8ef917e0fbc118c2c21 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Mon, 29 Aug 2022 15:56:33 +0800 Subject: [PATCH 069/106] feat: support customizing editor edges --- .../appflowy_editor/example/lib/main.dart | 9 +-- .../appflowy_editor/lib/appflowy_editor.dart | 1 + .../appflowy_editor/lib/src/editor_state.dart | 4 ++ .../src/render/image/image_node_widget.dart | 18 +++--- .../render/rich_text/bulleted_list_text.dart | 43 ++++++------- .../src/render/rich_text/checkbox_text.dart | 63 +++++++++---------- .../src/render/rich_text/heading_text.dart | 17 +++-- .../render/rich_text/number_list_text.dart | 39 ++++++------ .../lib/src/render/rich_text/quoted_text.dart | 43 ++++++------- .../lib/src/render/rich_text/rich_text.dart | 15 ++--- .../src/render/rich_text/rich_text_style.dart | 1 - .../lib/src/render/style/editor_style.dart | 20 ++++++ .../lib/src/service/editor_service.dart | 47 ++++++++------ .../render/image/image_node_builder_test.dart | 9 +-- 14 files changed, 172 insertions(+), 157 deletions(-) create mode 100644 frontend/app_flowy/packages/appflowy_editor/lib/src/render/style/editor_style.dart diff --git a/frontend/app_flowy/packages/appflowy_editor/example/lib/main.dart b/frontend/app_flowy/packages/appflowy_editor/example/lib/main.dart index 8f6bf64c30..4bd1cb1972 100644 --- a/frontend/app_flowy/packages/appflowy_editor/example/lib/main.dart +++ b/frontend/app_flowy/packages/appflowy_editor/example/lib/main.dart @@ -46,13 +46,7 @@ class _MyHomePageState extends State { Widget build(BuildContext context) { return Scaffold( extendBodyBehindAppBar: true, - body: Center( - child: Container( - width: 780, - alignment: Alignment.topCenter, - child: _buildEditor(context), - ), - ), + body: _buildEditor(context), floatingActionButton: _buildExpandableFab(), ); } @@ -100,6 +94,7 @@ class _MyHomePageState extends State { width: MediaQuery.of(context).size.width, child: AppFlowyEditor( editorState: _editorState, + editorStyle: const EditorStyle.defaultStyle(), ), ); } else { diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/appflowy_editor.dart b/frontend/app_flowy/packages/appflowy_editor/lib/appflowy_editor.dart index 14826ff713..12b3a29252 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/appflowy_editor.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/appflowy_editor.dart @@ -2,6 +2,7 @@ library appflowy_editor; export 'src/infra/log.dart'; +export 'src/render/style/editor_style.dart'; export 'src/document/node.dart'; export 'src/document/path.dart'; export 'src/document/position.dart'; diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/editor_state.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/editor_state.dart index 396b428baf..2750af07a6 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/editor_state.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/editor_state.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:appflowy_editor/src/infra/log.dart'; import 'package:appflowy_editor/src/render/selection_menu/selection_menu_widget.dart'; +import 'package:appflowy_editor/src/render/style/editor_style.dart'; import 'package:appflowy_editor/src/service/service.dart'; import 'package:flutter/material.dart'; @@ -58,6 +59,9 @@ class EditorState { /// Stores the selection menu items. List selectionMenuItems = []; + /// Stores the editor style. + EditorStyle editorStyle = const EditorStyle.defaultStyle(); + final UndoManager undoManager = UndoManager(); Selection? _cursorSelection; diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_widget.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_widget.dart index 316202b1c7..a65df11541 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_widget.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_widget.dart @@ -1,10 +1,8 @@ -import 'dart:math'; - +import 'package:appflowy_editor/src/extensions/object_extensions.dart'; import 'package:appflowy_editor/src/document/node.dart'; import 'package:appflowy_editor/src/document/position.dart'; import 'package:appflowy_editor/src/document/selection.dart'; import 'package:appflowy_editor/src/infra/flowy_svg.dart'; -import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart'; import 'package:appflowy_editor/src/render/selection/selectable.dart'; import 'package:flutter/material.dart'; @@ -35,6 +33,8 @@ class ImageNodeWidget extends StatefulWidget { } class _ImageNodeWidgetState extends State with Selectable { + final _imageKey = GlobalKey(); + double? _imageWidth; double _initial = 0; double _distance = 0; @@ -50,8 +50,11 @@ class _ImageNodeWidgetState extends State with Selectable { _imageWidth = widget.width; _imageStreamListener = ImageStreamListener( (image, _) { - _imageWidth = - min(defaultMaxTextNodeWidth, image.image.width.toDouble()); + _imageWidth = _imageKey.currentContext + ?.findRenderObject() + ?.unwrapOrNull() + ?.size + .width; }, ); } @@ -65,9 +68,8 @@ class _ImageNodeWidgetState extends State with Selectable { @override Widget build(BuildContext context) { // only support network image. - return Container( - width: defaultMaxTextNodeWidth, + key: _imageKey, padding: const EdgeInsets.only(top: 8, bottom: 8), child: _buildNetworkImage(context), ); @@ -137,7 +139,7 @@ class _ImageNodeWidgetState extends State with Selectable { loadingBuilder: (context, child, loadingProgress) => loadingProgress == null ? child : _buildLoading(context), errorBuilder: (context, error, stackTrace) { - _imageWidth ??= defaultMaxTextNodeWidth; + // _imageWidth ??= defaultMaxTextNodeWidth; return _buildError(context); }, ); diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/bulleted_list_text.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/bulleted_list_text.dart index 5408f862d8..7f0f0363f8 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/bulleted_list_text.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/bulleted_list_text.dart @@ -56,30 +56,27 @@ class _BulletedListTextNodeWidgetState extends State @override Widget build(BuildContext context) { - return Container( - constraints: BoxConstraints(maxWidth: defaultMaxTextNodeWidth), - child: Padding( - padding: EdgeInsets.only(bottom: defaultLinePadding), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - FlowySvg( - key: iconKey, - width: _iconWidth, - height: _iconWidth, - padding: EdgeInsets.only(right: _iconRightPadding), - name: 'point', + return Padding( + padding: EdgeInsets.only(bottom: defaultLinePadding), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + FlowySvg( + key: iconKey, + width: _iconWidth, + height: _iconWidth, + padding: EdgeInsets.only(right: _iconRightPadding), + name: 'point', + ), + Flexible( + child: FlowyRichText( + key: _richTextKey, + placeholderText: 'List', + textNode: widget.textNode, + editorState: widget.editorState, ), - Flexible( - child: FlowyRichText( - key: _richTextKey, - placeholderText: 'List', - textNode: widget.textNode, - editorState: widget.editorState, - ), - ) - ], - ), + ) + ], ), ); } diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/checkbox_text.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/checkbox_text.dart index bfda4e3f73..ed6748a43e 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/checkbox_text.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/checkbox_text.dart @@ -63,41 +63,38 @@ class _CheckboxNodeWidgetState extends State Widget _buildWithSingle(BuildContext context) { final check = widget.textNode.attributes.check; - return SizedBox( - width: defaultMaxTextNodeWidth, - child: Padding( - padding: EdgeInsets.only(bottom: defaultLinePadding), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - GestureDetector( - key: iconKey, - child: FlowySvg( - width: _iconWidth, - height: _iconWidth, - padding: EdgeInsets.only(right: _iconRightPadding), - name: check ? 'check' : 'uncheck', - ), - onTap: () { - TransactionBuilder(widget.editorState) - ..updateNode(widget.textNode, { - StyleKey.checkbox: !check, - }) - ..commit(); - }, + return Padding( + padding: EdgeInsets.only(bottom: defaultLinePadding), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + GestureDetector( + key: iconKey, + child: FlowySvg( + width: _iconWidth, + height: _iconWidth, + padding: EdgeInsets.only(right: _iconRightPadding), + name: check ? 'check' : 'uncheck', ), - Flexible( - child: FlowyRichText( - key: _richTextKey, - placeholderText: 'To-do', - textNode: widget.textNode, - textSpanDecorator: _textSpanDecorator, - placeholderTextSpanDecorator: _textSpanDecorator, - editorState: widget.editorState, - ), + onTap: () { + TransactionBuilder(widget.editorState) + ..updateNode(widget.textNode, { + StyleKey.checkbox: !check, + }) + ..commit(); + }, + ), + Flexible( + child: FlowyRichText( + key: _richTextKey, + placeholderText: 'To-do', + textNode: widget.textNode, + textSpanDecorator: _textSpanDecorator, + placeholderTextSpanDecorator: _textSpanDecorator, + editorState: widget.editorState, ), - ], - ), + ), + ], ), ); } diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/heading_text.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/heading_text.dart index 7b94783f03..fff25dd2d5 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/heading_text.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/heading_text.dart @@ -63,16 +63,13 @@ class _HeadingTextNodeWidgetState extends State top: _topPadding, bottom: defaultLinePadding, ), - child: Container( - constraints: BoxConstraints(maxWidth: defaultMaxTextNodeWidth), - child: FlowyRichText( - key: _richTextKey, - placeholderText: 'Heading', - placeholderTextSpanDecorator: _placeholderTextSpanDecorator, - textSpanDecorator: _textSpanDecorator, - textNode: widget.textNode, - editorState: widget.editorState, - ), + child: FlowyRichText( + key: _richTextKey, + placeholderText: 'Heading', + placeholderTextSpanDecorator: _placeholderTextSpanDecorator, + textSpanDecorator: _textSpanDecorator, + textNode: widget.textNode, + editorState: widget.editorState, ), ); } diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/number_list_text.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/number_list_text.dart index a4d72bb011..36cf91bdce 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/number_list_text.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/number_list_text.dart @@ -58,28 +58,25 @@ class _NumberListTextNodeWidgetState extends State Widget build(BuildContext context) { return Padding( padding: EdgeInsets.only(bottom: defaultLinePadding), - child: SizedBox( - width: defaultMaxTextNodeWidth, - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - FlowySvg( - key: iconKey, - width: _iconWidth, - height: _iconWidth, - padding: EdgeInsets.only(right: _iconRightPadding), - number: widget.textNode.attributes.number, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + FlowySvg( + key: iconKey, + width: _iconWidth, + height: _iconWidth, + padding: EdgeInsets.only(right: _iconRightPadding), + number: widget.textNode.attributes.number, + ), + Flexible( + child: FlowyRichText( + key: _richTextKey, + placeholderText: 'List', + textNode: widget.textNode, + editorState: widget.editorState, ), - Flexible( - child: FlowyRichText( - key: _richTextKey, - placeholderText: 'List', - textNode: widget.textNode, - editorState: widget.editorState, - ), - ), - ], - ), + ), + ], )); } } diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/quoted_text.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/quoted_text.dart index 04ae379799..9c2366d1cb 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/quoted_text.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/quoted_text.dart @@ -55,30 +55,27 @@ class _QuotedTextNodeWidgetState extends State @override Widget build(BuildContext context) { - return SizedBox( - width: defaultMaxTextNodeWidth, - child: Padding( - padding: EdgeInsets.only(bottom: defaultLinePadding), - child: IntrinsicHeight( - child: Row( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - FlowySvg( - key: iconKey, - width: _iconWidth, - padding: EdgeInsets.only(right: _iconRightPadding), - name: 'quote', + return Padding( + padding: EdgeInsets.only(bottom: defaultLinePadding), + child: IntrinsicHeight( + child: Row( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + FlowySvg( + key: iconKey, + width: _iconWidth, + padding: EdgeInsets.only(right: _iconRightPadding), + name: 'quote', + ), + Flexible( + child: FlowyRichText( + key: _richTextKey, + placeholderText: 'Quote', + textNode: widget.textNode, + editorState: widget.editorState, ), - Flexible( - child: FlowyRichText( - key: _richTextKey, - placeholderText: 'Quote', - textNode: widget.textNode, - editorState: widget.editorState, - ), - ), - ], - ), + ), + ], ), ), ); diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/rich_text.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/rich_text.dart index 5fe65db4b7..b9a3e2f314 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/rich_text.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/rich_text.dart @@ -52,15 +52,12 @@ class _RichTextNodeWidgetState extends State @override Widget build(BuildContext context) { - return Container( - constraints: BoxConstraints(maxWidth: defaultMaxTextNodeWidth), - child: Padding( - padding: EdgeInsets.only(bottom: defaultLinePadding), - child: FlowyRichText( - key: _richTextKey, - textNode: widget.textNode, - editorState: widget.editorState, - ), + return Padding( + padding: EdgeInsets.only(bottom: defaultLinePadding), + child: FlowyRichText( + key: _richTextKey, + textNode: widget.textNode, + editorState: widget.editorState, ), ); } diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/rich_text_style.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/rich_text_style.dart index efcdd3790f..6270127610 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/rich_text_style.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/rich_text_style.dart @@ -61,7 +61,6 @@ class StyleKey { } // TODO: customize -double defaultMaxTextNodeWidth = 780.0; double defaultLinePadding = 8.0; double baseFontSize = 16.0; String defaultHighlightColor = '0x6000BCF0'; diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/style/editor_style.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/style/editor_style.dart new file mode 100644 index 0000000000..e691ea689e --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/style/editor_style.dart @@ -0,0 +1,20 @@ +import 'package:flutter/material.dart'; + +/// Editor style configuration +class EditorStyle { + const EditorStyle({ + required this.padding, + }); + + const EditorStyle.defaultStyle() + : padding = const EdgeInsets.fromLTRB(200.0, 0.0, 200.0, 0.0); + + /// The margin of the document context from the editor. + final EdgeInsets padding; + + EditorStyle copyWith({EdgeInsets? padding}) { + return EditorStyle( + padding: padding ?? this.padding, + ); + } +} diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/editor_service.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/editor_service.dart index 2781471b46..3a8d75560b 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/editor_service.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/editor_service.dart @@ -1,5 +1,6 @@ import 'package:appflowy_editor/src/render/image/image_node_builder.dart'; import 'package:appflowy_editor/src/render/selection_menu/selection_menu_widget.dart'; +import 'package:appflowy_editor/src/render/style/editor_style.dart'; import 'package:appflowy_editor/src/service/internal_key_event_handlers/default_key_event_handlers.dart'; import 'package:flutter/material.dart'; @@ -36,6 +37,7 @@ class AppFlowyEditor extends StatefulWidget { this.customBuilders = const {}, this.keyEventHandlers = const [], this.selectionMenuItems = const [], + this.editorStyle = const EditorStyle.defaultStyle(), }) : super(key: key); final EditorState editorState; @@ -48,6 +50,8 @@ class AppFlowyEditor extends StatefulWidget { final List selectionMenuItems; + final EditorStyle editorStyle; + @override State createState() => _AppFlowyEditorState(); } @@ -60,6 +64,7 @@ class _AppFlowyEditorState extends State { super.initState(); editorState.selectionMenuItems = widget.selectionMenuItems; + editorState.editorStyle = widget.editorStyle; editorState.service.renderPluginService = _createRenderPlugin(); } @@ -68,6 +73,8 @@ class _AppFlowyEditorState extends State { super.didUpdateWidget(oldWidget); if (editorState.service != oldWidget.editorState.service) { + editorState.selectionMenuItems = widget.selectionMenuItems; + editorState.editorStyle = widget.editorStyle; editorState.service.renderPluginService = _createRenderPlugin(); } } @@ -76,27 +83,31 @@ class _AppFlowyEditorState extends State { Widget build(BuildContext context) { return AppFlowyScroll( key: editorState.service.scrollServiceKey, - child: AppFlowySelection( - key: editorState.service.selectionServiceKey, - editorState: editorState, - child: AppFlowyInput( - key: editorState.service.inputServiceKey, + child: Padding( + padding: widget.editorStyle.padding, + child: AppFlowySelection( + key: editorState.service.selectionServiceKey, editorState: editorState, - child: AppFlowyKeyboard( - key: editorState.service.keyboardServiceKey, - handlers: [ - ...defaultKeyEventHandlers, - ...widget.keyEventHandlers, - ], + child: AppFlowyInput( + key: editorState.service.inputServiceKey, editorState: editorState, - child: FlowyToolbar( - key: editorState.service.toolbarServiceKey, + child: AppFlowyKeyboard( + key: editorState.service.keyboardServiceKey, + handlers: [ + ...defaultKeyEventHandlers, + ...widget.keyEventHandlers, + ], editorState: editorState, - child: editorState.service.renderPluginService.buildPluginWidget( - NodeWidgetContext( - context: context, - node: editorState.document.root, - editorState: editorState, + child: FlowyToolbar( + key: editorState.service.toolbarServiceKey, + editorState: editorState, + child: + editorState.service.renderPluginService.buildPluginWidget( + NodeWidgetContext( + context: context, + node: editorState.document.root, + editorState: editorState, + ), ), ), ), diff --git a/frontend/app_flowy/packages/appflowy_editor/test/render/image/image_node_builder_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/render/image/image_node_builder_test.dart index 9121fa1868..a9732d8a20 100644 --- a/frontend/app_flowy/packages/appflowy_editor/test/render/image/image_node_builder_test.dart +++ b/frontend/app_flowy/packages/appflowy_editor/test/render/image/image_node_builder_test.dart @@ -49,9 +49,10 @@ void main() async { final editorRect = tester.getRect(editorFinder); final leftImageRect = tester.getRect(imageFinder.at(0)); - expect(leftImageRect.left, editorRect.left); + expect(leftImageRect.left, editor.editorState.editorStyle.padding.left); final rightImageRect = tester.getRect(imageFinder.at(2)); - expect(rightImageRect.right, editorRect.right); + expect(rightImageRect.right, + editorRect.right - editor.editorState.editorStyle.padding.right); final centerImageRect = tester.getRect(imageFinder.at(1)); expect(centerImageRect.left, (leftImageRect.left + rightImageRect.left) / 2.0); @@ -73,8 +74,8 @@ void main() async { leftImage.onAlign(Alignment.centerRight); await tester.pump(const Duration(milliseconds: 100)); expect( - tester.getRect(imageFinder.at(0)).left, - rightImageRect.left, + tester.getRect(imageFinder.at(0)).right, + rightImageRect.right, ); }); }); From af3bfebb64c7ae6c189a1512524f99ae0c4e39c8 Mon Sep 17 00:00:00 2001 From: appflowy Date: Mon, 29 Aug 2022 16:32:20 +0800 Subject: [PATCH 070/106] chore: become editing when creating a new card --- .../plugins/board/application/board_bloc.dart | 19 +++++++++++---- .../board/application/group_controller.dart | 7 +++++- .../board/presentation/board_page.dart | 9 ++------ .../board/presentation/card/board_cell.dart | 3 +++ .../presentation/card/board_text_cell.dart | 9 +++++++- .../plugins/board/presentation/card/card.dart | 23 +++++++++++-------- .../presentation/card/card_cell_builder.dart | 7 +++++- .../packages/appflowy_board/CHANGELOG.md | 4 ++++ .../widgets/reorder_flex/reorder_flex.dart | 1 - .../reorder_phantom/phantom_controller.dart | 1 - .../packages/appflowy_board/pubspec.yaml | 2 +- .../flowy-grid/src/entities/block_entities.rs | 15 ++++++++++-- .../flowy-grid/src/services/block_manager.rs | 1 + .../src/services/grid_view_editor.rs | 1 + 14 files changed, 73 insertions(+), 29 deletions(-) create mode 100644 frontend/app_flowy/lib/plugins/board/presentation/card/board_cell.dart diff --git a/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart b/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart index dcac0a47bb..53038c8d42 100644 --- a/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart +++ b/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart @@ -142,7 +142,7 @@ class BoardBloc extends Bloc { for (final group in groups) { final delegate = GroupControllerDelegateImpl( controller: boardController, - didAddColumnItem: (groupId, row) { + onNewColumnItem: (groupId, row) { add(BoardEvent.didCreateRow(groupId, row)); }, ); @@ -313,11 +313,11 @@ class BoardColumnItem extends AFColumnItem { class GroupControllerDelegateImpl extends GroupControllerDelegate { final AFBoardDataController controller; - final void Function(String, RowPB) didAddColumnItem; + final void Function(String, RowPB) onNewColumnItem; GroupControllerDelegateImpl({ required this.controller, - required this.didAddColumnItem, + required this.onNewColumnItem, }); @override @@ -329,10 +329,8 @@ class GroupControllerDelegateImpl extends GroupControllerDelegate { final item = BoardColumnItem( row: row, fieldId: group.fieldId, - requestFocus: true, ); controller.addColumnItem(group.groupId, item); - didAddColumnItem(group.groupId, row); } } @@ -351,6 +349,17 @@ class GroupControllerDelegateImpl extends GroupControllerDelegate { ), ); } + + @override + void addNewRow(GroupPB group, RowPB row) { + final item = BoardColumnItem( + row: row, + fieldId: group.fieldId, + requestFocus: true, + ); + controller.addColumnItem(group.groupId, item); + onNewColumnItem(group.groupId, row); + } } class BoardEditingRow { diff --git a/frontend/app_flowy/lib/plugins/board/application/group_controller.dart b/frontend/app_flowy/lib/plugins/board/application/group_controller.dart index 812dbc76d3..9407ae360b 100644 --- a/frontend/app_flowy/lib/plugins/board/application/group_controller.dart +++ b/frontend/app_flowy/lib/plugins/board/application/group_controller.dart @@ -9,6 +9,7 @@ abstract class GroupControllerDelegate { void removeRow(GroupPB group, String rowId); void insertRow(GroupPB group, RowPB row, int? index); void updateRow(GroupPB group, RowPB row); + void addNewRow(GroupPB group, RowPB row); } class GroupController { @@ -48,7 +49,11 @@ class GroupController { group.rows.add(insertedRow.row); } - delegate.insertRow(group, insertedRow.row, index); + if (insertedRow.isNew) { + delegate.addNewRow(group, insertedRow.row); + } else { + delegate.insertRow(group, insertedRow.row, index); + } } for (final updatedRow in changeset.updatedRows) { diff --git a/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart b/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart index d0884e4efc..f6bfd609c3 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart @@ -196,10 +196,8 @@ class _BoardContentState extends State { ); final cellBuilder = BoardCellBuilder(cardController); - final isEditing = context.read().state.editingRow.fold( - () => false, - (editingRow) => editingRow.row.id == rowPB.id, - ); + + final isEditing = context.read().state.editingRow.isSome(); return AppFlowyColumnItemCard( key: ValueKey(columnItem.id), @@ -212,9 +210,6 @@ class _BoardContentState extends State { isEditing: isEditing, cellBuilder: cellBuilder, dataController: cardController, - onEditEditing: (rowId) { - context.read().add(BoardEvent.endEditRow(rowId)); - }, openCard: (context) => _openCard( gridId, fieldCache, diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/board_cell.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/board_cell.dart new file mode 100644 index 0000000000..3c0c0298e9 --- /dev/null +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/board_cell.dart @@ -0,0 +1,3 @@ +abstract class FocusableBoardCell { + set becomeFocus(bool isFocus); +} diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/board_text_cell.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/board_text_cell.dart index ed683005f9..20b1a07085 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/board_text_cell.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/board_text_cell.dart @@ -1,16 +1,19 @@ import 'package:app_flowy/plugins/board/application/card/board_text_cell_bloc.dart'; import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart'; import 'package:app_flowy/plugins/grid/presentation/widgets/cell/cell_builder.dart'; -import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class BoardTextCell extends StatefulWidget { final String groupId; + final bool isFocus; + final GridCellControllerBuilder cellControllerBuilder; + const BoardTextCell({ required this.groupId, required this.cellControllerBuilder, + this.isFocus = false, Key? key, }) : super(key: key); @@ -30,6 +33,10 @@ class _BoardTextCellState extends State { _cellBloc = BoardTextCellBloc(cellController: cellController) ..add(const BoardTextCellEvent.initial()); _controller = TextEditingController(text: _cellBloc.state.content); + + if (widget.isFocus) { + focusNode.requestFocus(); + } super.initState(); } diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart index 9f919f2dd1..08331f6021 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart @@ -10,8 +10,6 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'card_cell_builder.dart'; import 'card_container.dart'; -typedef OnEndEditing = void Function(String rowId); - class BoardCard extends StatefulWidget { final String gridId; final String groupId; @@ -19,7 +17,6 @@ class BoardCard extends StatefulWidget { final bool isEditing; final CardDataController dataController; final BoardCellBuilder cellBuilder; - final OnEndEditing onEditEditing; final void Function(BuildContext) openCard; const BoardCard({ @@ -29,7 +26,6 @@ class BoardCard extends StatefulWidget { required this.isEditing, required this.dataController, required this.cellBuilder, - required this.onEditEditing, required this.openCard, Key? key, }) : super(key: key); @@ -87,18 +83,27 @@ class _BoardCardState extends State { final List children = []; cells.asMap().forEach( (int index, GridCellIdentifier cellId) { - final child = widget.cellBuilder.buildCell(widget.groupId, cellId); + Widget child = widget.cellBuilder.buildCell( + widget.groupId, + cellId, + widget.isEditing, + ); + if (index != 0) { - children.add(Padding( + child = Padding( + key: cellId.key(), padding: const EdgeInsets.only(left: 4, right: 4, top: 8), child: child, - )); + ); } else { - children.add(Padding( + child = Padding( + key: UniqueKey(), padding: const EdgeInsets.only(left: 4, right: 4), child: child, - )); + ); } + + children.add(child); }, ); return children; diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/card_cell_builder.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/card_cell_builder.dart index 83dbf584e8..b292457193 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/card_cell_builder.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/card_cell_builder.dart @@ -19,7 +19,11 @@ class BoardCellBuilder { BoardCellBuilder(this.delegate); - Widget buildCell(String groupId, GridCellIdentifier cellId) { + Widget buildCell( + String groupId, + GridCellIdentifier cellId, + bool isEditing, + ) { final cellControllerBuilder = GridCellControllerBuilder( delegate: delegate, cellId: cellId, @@ -62,6 +66,7 @@ class BoardCellBuilder { return BoardTextCell( groupId: groupId, cellControllerBuilder: cellControllerBuilder, + isFocus: isEditing, key: key, ); case FieldType.URL: diff --git a/frontend/app_flowy/packages/appflowy_board/CHANGELOG.md b/frontend/app_flowy/packages/appflowy_board/CHANGELOG.md index c4c6495533..d8eceeefee 100644 --- a/frontend/app_flowy/packages/appflowy_board/CHANGELOG.md +++ b/frontend/app_flowy/packages/appflowy_board/CHANGELOG.md @@ -1,3 +1,7 @@ +# 0.0.6 +* Support scroll to bottom +* Fix some bugs + # 0.0.5 * Optimize insert card animation * Enable insert card at the end of the column diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/reorder_flex.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/reorder_flex.dart index 63bd9638b4..90556440b4 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/reorder_flex.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/reorder_flex.dart @@ -381,7 +381,6 @@ class ReorderFlexState extends State dragState.currentIndex, ); } - dragState.endDragging(); widget.onDragEnded?.call(); }); diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_phantom/phantom_controller.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_phantom/phantom_controller.dart index a3e0f5fb6e..0f63266e51 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_phantom/phantom_controller.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_phantom/phantom_controller.dart @@ -2,7 +2,6 @@ import 'package:appflowy_board/appflowy_board.dart'; import 'package:flutter/widgets.dart'; import '../../utils/log.dart'; -import '../board_column/board_column_data.dart'; import '../reorder_flex/drag_state.dart'; import '../reorder_flex/drag_target.dart'; import '../reorder_flex/drag_target_interceptor.dart'; diff --git a/frontend/app_flowy/packages/appflowy_board/pubspec.yaml b/frontend/app_flowy/packages/appflowy_board/pubspec.yaml index 6bb2feabfe..a9adf5007a 100644 --- a/frontend/app_flowy/packages/appflowy_board/pubspec.yaml +++ b/frontend/app_flowy/packages/appflowy_board/pubspec.yaml @@ -1,6 +1,6 @@ name: appflowy_board description: AppFlowy board implementation. -version: 0.0.5 +version: 0.0.6 homepage: https://github.com/AppFlowy-IO/AppFlowy repository: https://github.com/AppFlowy-IO/AppFlowy/tree/main/frontend/app_flowy/packages/appflowy_board diff --git a/frontend/rust-lib/flowy-grid/src/entities/block_entities.rs b/frontend/rust-lib/flowy-grid/src/entities/block_entities.rs index e691ed1830..6e271a0fb2 100644 --- a/frontend/rust-lib/flowy-grid/src/entities/block_entities.rs +++ b/frontend/rust-lib/flowy-grid/src/entities/block_entities.rs @@ -120,17 +120,28 @@ pub struct InsertedRowPB { #[pb(index = 2, one_of)] pub index: Option, + + #[pb(index = 3)] + pub is_new: bool, } impl InsertedRowPB { pub fn new(row: RowPB) -> Self { - Self { row, index: None } + Self { + row, + index: None, + is_new: false, + } } } impl std::convert::From for InsertedRowPB { fn from(row: RowPB) -> Self { - Self { row, index: None } + Self { + row, + index: None, + is_new: false, + } } } diff --git a/frontend/rust-lib/flowy-grid/src/services/block_manager.rs b/frontend/rust-lib/flowy-grid/src/services/block_manager.rs index 1bc9b9dc42..9619bf52ab 100644 --- a/frontend/rust-lib/flowy-grid/src/services/block_manager.rs +++ b/frontend/rust-lib/flowy-grid/src/services/block_manager.rs @@ -164,6 +164,7 @@ impl GridBlockManager { let insert_row = InsertedRowPB { index: Some(to as i32), row: make_row_from_row_rev(row_rev), + is_new: false, }; let notified_changeset = GridBlockChangesetPB { diff --git a/frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs b/frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs index 8b89aa72ae..013f59cd42 100644 --- a/frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs +++ b/frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs @@ -98,6 +98,7 @@ impl GridViewRevisionEditor { let inserted_row = InsertedRowPB { row: row_pb.clone(), index: None, + is_new: true, }; let changeset = GroupChangesetPB::insert(group_id.clone(), vec![inserted_row]); self.notify_did_update_group(changeset).await; From 32a1bbb6b949a232bcda0205cae52fee3993b3ce Mon Sep 17 00:00:00 2001 From: appflowy Date: Mon, 29 Aug 2022 18:15:17 +0800 Subject: [PATCH 071/106] chore: optimize cell cache --- .../lib/plugins/grid/application/cell/cell_listener.dart | 6 ++++-- .../grid/application/cell/cell_service/cell_cache.dart | 9 ++++++++- .../application/cell/cell_service/context_builder.dart | 2 +- .../lib/plugins/grid/application/row/row_cache.dart | 3 ++- 4 files changed, 15 insertions(+), 5 deletions(-) diff --git a/frontend/app_flowy/lib/plugins/grid/application/cell/cell_listener.dart b/frontend/app_flowy/lib/plugins/grid/application/cell/cell_listener.dart index 5da5fce86d..4805ad8b7a 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/cell/cell_listener.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/cell/cell_listener.dart @@ -11,13 +11,15 @@ typedef UpdateFieldNotifiedValue = Either; class CellListener { final String rowId; final String fieldId; - PublishNotifier? _updateCellNotifier = PublishNotifier(); + PublishNotifier? _updateCellNotifier = + PublishNotifier(); GridNotificationListener? _listener; CellListener({required this.rowId, required this.fieldId}); void start({required void Function(UpdateFieldNotifiedValue) onCellChanged}) { _updateCellNotifier?.addPublishListener(onCellChanged); - _listener = GridNotificationListener(objectId: "$rowId:$fieldId", handler: _handler); + _listener = GridNotificationListener( + objectId: "$rowId:$fieldId", handler: _handler); } void _handler(GridNotification ty, Either result) { diff --git a/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/cell_cache.dart b/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/cell_cache.dart index 1f14c7c54a..3d816b21d1 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/cell_cache.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/cell_cache.dart @@ -33,10 +33,17 @@ class GridCellCache { required this.gridId, }); - void remove(String fieldId) { + void removeCellWithFieldId(String fieldId) { _cellDataByFieldId.remove(fieldId); } + void remove(GridCellCacheKey key) { + var map = _cellDataByFieldId[key.fieldId]; + if (map != null) { + map.remove(key.rowId); + } + } + void insert(GridCellCacheKey key, T value) { var map = _cellDataByFieldId[key.fieldId]; if (map == null) { diff --git a/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/context_builder.dart b/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/context_builder.dart index 8ab486a48c..d716133d05 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/context_builder.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/context_builder.dart @@ -191,7 +191,7 @@ class IGridCellController extends Equatable { _cellListener?.start(onCellChanged: (result) { result.fold( (_) { - _cellsCache.remove(fieldId); + _cellsCache.remove(_cacheKey); _loadData(); }, (err) => Log.error(err), diff --git a/frontend/app_flowy/lib/plugins/grid/application/row/row_cache.dart b/frontend/app_flowy/lib/plugins/grid/application/row/row_cache.dart index 68c8b6f519..d6cd387e74 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/row/row_cache.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/row/row_cache.dart @@ -52,7 +52,8 @@ class GridRowCache { // notifier.onRowFieldsChanged(() => _rowChangeReasonNotifier .receive(const RowsChangedReason.fieldDidChange())); - notifier.onRowFieldChanged((field) => _cellCache.remove(field.id)); + notifier.onRowFieldChanged( + (field) => _cellCache.removeCellWithFieldId(field.id)); _rowInfos = block.rows.map((rowPB) => buildGridRow(rowPB)).toList(); } From 16d4c1ef8ed38aa240b1d85b62914bc43b09836c Mon Sep 17 00:00:00 2001 From: appflowy Date: Mon, 29 Aug 2022 19:33:32 +0800 Subject: [PATCH 072/106] chore: update appflowy_board_version --- .../packages/appflowy_board/README.md | 1 + .../example/gifs/appflowy_board_video_2.gif | Bin 0 -> 241509 bytes 2 files changed, 1 insertion(+) create mode 100644 frontend/app_flowy/packages/appflowy_board/example/gifs/appflowy_board_video_2.gif diff --git a/frontend/app_flowy/packages/appflowy_board/README.md b/frontend/app_flowy/packages/appflowy_board/README.md index b33c4d7883..38561d13e3 100644 --- a/frontend/app_flowy/packages/appflowy_board/README.md +++ b/frontend/app_flowy/packages/appflowy_board/README.md @@ -7,6 +7,7 @@ The **appflowy_board** is a package that is used in [AppFlowy](https://github.co ## Getting Started

+

diff --git a/frontend/app_flowy/packages/appflowy_board/example/gifs/appflowy_board_video_2.gif b/frontend/app_flowy/packages/appflowy_board/example/gifs/appflowy_board_video_2.gif new file mode 100644 index 0000000000000000000000000000000000000000..71aac5f06e99178f0aac2f6eb52de37459e5d27e GIT binary patch literal 241509 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%0D|oE15o)5;XTyS)XCu66?@E>|oJjTF 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 z

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 z"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 Date: Mon, 29 Aug 2022 19:42:33 +0800 Subject: [PATCH 073/106] chore: update board gif --- .../example/gifs/appflowy_board_video_2.gif | Bin 241509 -> 298299 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/frontend/app_flowy/packages/appflowy_board/example/gifs/appflowy_board_video_2.gif b/frontend/app_flowy/packages/appflowy_board/example/gifs/appflowy_board_video_2.gif index 71aac5f06e99178f0aac2f6eb52de37459e5d27e..d2a29020ed6d1b4854bde23f9bd2676be1c4fc6d 100644 GIT binary patch literal 298299 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 z

HUCRPT@F&7)6Baq8_M*A6NL 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 OpIWa=Y~h9jI{yol81C`_ literal 241509 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%0D|oE15o)5;XTyS)XCu66?@E>|oJjTF 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 z

^rhV7Adv-2<%4M*z{rpi1(>+=&Ppo8TmgC&76%bK+&EMOl7^ZeUm)l5{9S^3+= zWTvGm8Nc}uMQWSnEy&=~i?lUhIy?-^8UqUoPyrd96xclm4@i4d@T4F{3O zzRBF%Y}D5;1eyU`Z_$zYAUlJ%g3~M~O$F0^fRaR&2|CUpVu|2`;-z%18JOA8!-g)i zn@{hEkNiSh;Yyb%N!IlS2{IU_cL>iGlh4#c#@^f<<~B`e^qw7jIa^1aMwgzNmpW}6 zIB)-QE_yOdYI+`AG~B577bD4o5zXD@OJ|v3^|b0^%JW^VuSejdl3CTS2|lzLfBYD|B$KMq z`pxO=eNvm0MGHM7_=Xso?1eEpAkbp3F}dMzokDbpY4GgRvqc`FBiUWm#orQYAc1z#9pNNpE`Ov#=NDL)k2qPb;Fq_u0+Yra!HUg+m_D zp}rbjlZd8$S)w;%r6(N5tUu&EJ-Q*2%xzndTtBucmnP)n>LL-n@yYLL43qwBw2nfq zLbh!7rA?MX)+-66_1TFp&+=Y(hCXzDR3ufb+Fzp2dt$0qXEoEj9*>)^-sEUCIx#m@ zI$B8f_4Hb8nq@l64~MztzRLb`NAN=_JHDC2^4~Tg0K)fWQ)f zfr0dQCBhHP`2Y$>|5OKo>H5EiIWP28|93Ixe_+2)C~Aaa&KB1U4OQD?c`pk8MdkGW zYs{JU(rZ*Fzj>@Te*_pW^_P4ZmT1dK=iI8(6 zXdOgB_nS=P>?4{ifUG364uS`tMl{KwK`r;5$If=BY^bVIe$Jg166)L9DJ<)PKNcAo0^{+>)2nOj5o|KE zkaE0RMIDN{S5M8<@byE#$nk#r z*v}---*8PGz)%=-Isj6CWQDQl zcE1N7|C!;xV}<|1koYSr99WfGnLGR*bRYObD<~e7v(_v3Jy`|H3geJB6{u7iFQ|r7 zPnA;msA8kk{40e`f4DzCv%&-Q-wlc1y-ojghIWl$I9T`=O8C6J-!pR&SGZ_`@j zKTB45wa#XkVRYxL=Eml*{60fZ8f~UHY~*jIx}0u9lU4Te!5l_^QP>ov^LB)7`aJX3 z!loY~#Ku3gg8AOKY=Z*ON`+r%=(O9T0zu1_AI@sieDBN5FJ<4I)%mika^(YeZiRLo zWX8K0nz3M#r|~XX<%?YH3e;&_wU!DkZ2Fkf4=rpe*)fL}HWBVLoo*yCH(#EjFn_#- zcVTJ);-NCN!czw`wjpy4FtnqKsWT=oN}3*2fL|9`72mhmEpNhhV|Y|T9A;XP_wap@ zbuMk`u5~|CA?n)!s$uEM9*(75t05kffw#lB5LBBEVT=XKPtT}>ZAL-NgElR)0t*)7 z&%Y5`PcXj@sUA?Z(5TGDaeh)!*&V1+S^gpMhBdJ(w)nJb5t+?=+JZ!*TySPUW*h3P z#`}7<jJIR-(-^=4T!jiUaG+zE6ae!1Vs&rsqnqAZ9{qVp z@B^3CW_B0+gCwFSpSy5u;;ks2g<$de!u3Y1iG`6%Ww0e2u5ofuLmp-rT0Y=+IYMGB^kB>dY?g9SRqFaUV|IL2W7)g0i@N(>!iKzDP%n!t}a;L+V2z~~g5 z56+ZKDtVpPS%!D1Duv(SxYt9NbeqZR1f z=%*)roU$>@o-Xd4{FvC#GgWSGEtlu9QBvvPrO6YnYSFo>a-N~T@RiP+Qp+zHB*lmr z%?oVn(WXD={8n`8cGR4}JrVlt{dvXOV3^p;TNk_2oloud0vu1?m7h-4PLV)vTrVIO zQ}0!7*OYN-KErXv^6psrP4azC!1?KFvO?cZ(D%2K8}N?y=&u*QZ3Ad34EeE`rnj%xMAFij$5bkK_c2sHA2V4KKZ9Q)|?7~4e#Kya+;RUdic zNp9~5TO`#xZHldaJR13I`9W0)C7oB5eS)keM|s9W9++oC!Y^%2#k~gPtwy~i?+f`( zN3RE7Ij$^@0)M_ZfAfF;=A+*)DJ$Sj_2n4`4xRmyd~xEY?B_bD8Ze`cIzs1Z5Ei|0 zQynWscNqFYb;Y^agLbzi@Xrdj-v{Am00<~!?QRfu1>`5aVIga_LOby$+(b_o3B;vV zI(}=V+Z~M0Z49?XnfS%ydu=3~0?%rf0Ji(4S@cckOTY27_9?00j+QR%Y4 zBY4O=ip=h1VZWt&bRt{fI<0;%T1VpyCOjrjM)#Qo!n%2iym*Wv}RvLq#48TEJ%?P0;69FBoGkFfD@Pc^EhT^OthM z6^P?XFg8Y%&)liWkI7#VJrW^xmg!Ilhl42QzIb5}?eLrz;e*ag6-eh1m{-`YGfVK2 zjdnd+@vf7Te4-jJR^4uUJx=SpAvM-$<6ktS(wO_Qz-dSZ;5M|TQ_Dn*fshyh`Qp)< z$j{M%gZJK0BZ3klVSp3iVC+QmtNrGLwuFx6{kx(cTm zd=wp0iDN}dUs}gig;I?qDjyD*=b)r}8j1bItPahwt(XQOKge9=`L&btuRxeq7?|y9L;y9QEENRNb2XCj9$)7oKy2&y66;ea{CaiY9N8=~Hsq)N{YD39&kH z#W@p+I)m~k7;gs|XCT3irfNtbSKcC%aLAm(;=T|kOEd;q8{JZyBr}H+^KS`ttv)1s zb(UV6C+Ec>a_0X?_?NoCxkMA}M{-ovTqIECCF4u9Q%B)kQ9Y&!zVEdBcxbQtbni-V z3Bb&E&_(b~jylod5`)mm`*A35->JDjB0NJ`%e{dfg`1O^v0Hx_yee%(Z1^6*4=2+{ zvb?A$MRX(Bz$O8gi%PEW_UZu#@7onWhFPOFfZ9I6{<3njEUCQ5Duvd;^{0;zOgNlN zUCnZ1v5#V?9Vn>yS{K(={K)Yh81jhj$RacXux-mg!f8RATQYw5zF|}yLofVLfHz~F z5E=tRngArn(wjn18~KHM+r3z@$eh+{fPAnH_CxYcI@Nh0Tx!pbPOoFMNE{+Dw;McnvLi6e9wjF!4&GA0Wr$4o3N%RP#TjOTO`+T>9Op@dI zn*QFJrAW!K;{VLkMfZWdO&=T0@`qpSAau&TeM->QtVLb-@2y#n0I@YcTC+4SBV$qW z+#g++_mo4Xw`Pq5=dk^$H48`zXeKq_$eD;-HPR=5-_Q9i_**XrrY#`E z6#MO?t>7Pa5ZBAciAR(h`iB?8o&?8fh$#kw-z4sPQr!iAZ}H;sjMzwi5B`SbXixxH zj>Dj&<>?b8LhT^O?@?Ibj(N2GQSubA7_tFR=$%FrXBFR**t(0+mVF8zvdO~zkT367 zAQz$Jn{|asrx1s%kjbH#FUkl~Opu&#G+{Bh6@WfcTKHrn4r3#H4&~g_j-6s1MhJ=w`3II@4g>$@+(+3 z$oS`~f;|0tT7qXf7IA~ zI|m+YeSX$BSXH7ETNu2d`WEJv#hWb~WpO)@lXZ=~lwImG#yGGAAm0PyJZCb0Mo!iI zHQ7_3Zch5~c{^%K8MB-1m!)=qN@WUZIwr|y3>kT?k0?rJUT8;Jc1&sL9}3&wy-Qz4 z&9!Uy*|y{*CIw<|DVE`Luo@^xR?{o!OgTA3L4XOAP_7NnzYn> z_(G3@`cI z6|>ys_L{rNoK7&Tyax64dG1t)@x4D3u>y~MsF+v`pU5_hM&{T&cpS1V zg>^ZC8dG6G0st|ae*UZQJPU1Px_Dqg`Z9oKw_)nz=}EpL<1Qn3Zc+{0&eno^-z^$% zV8l1)J>k*1A%bI%M}AGUey~}Kq4V7Rz0=ptXy&(pbIfZ@oDRbaaW;iX4ZB@IjZ5$5 z9hUSLj}7*#V!oJOY!EJ9HJg8g+~_&aftKKgi&{55_q_@2K3f~rwJ#wb)QP*Sye{Ta zX9_(iFurCa`o{G_U@ta2C^?VP>vd4C9MgDoq5e8UZGQiluqI50%K{9^w-$@rh2-|B z84N^s7+=}r48of&ak+-kxS``5-XFo=xlFcUmo80xELoGaLnkH93*lpkjnmz-r$#O} zyM}mrdGtqJcfsG5VORIZXwDku795ghG8WH<&YG`ozG(&{E+fb}Fm(NFQ+*1v5BLI8 z^2hBDh{-biU)%mjMO143YEykXs}~>TIrJxJa6EI^3tm#w@TYajpU9!=OPjlONyK6B zU8ryM*xo;e2LC^;OFVDBLr>h(Yk!K>`M+yO;OO+$wPV>e)T9NbFY7uepyj88!{Hb~ zp{GT5&STctU~`u zo2fDis$N&6#-A4N$rG1ZQ(nMSC-yVl% zstJ!v)-V8IHQonm^eYw3)GB{+GLx!hQmk`WFQ|q>%o0tOCzr(1@nn;3?&UlxP51y^ z1Qa;DRO$OxD;4mlwxi>{BcQ=#GzD?BdbZE)amD<0%kfrvqdB@svNB4CasW8Ao<-t_ z8l^e5Vq`XXW0-HNzVCEF{4dTwf|`RVa)U1m)@uvZ9#5ht)Jt!o7)U_gyQ*GHfPC3} zZky`IC{EMGhEI?*`7U}Vd{zgC;@g0GrGuPU;8-N=MTfJ6H=62YgKPZ)G>YuW?~thK z(q1lh>cIodOkqT*$F`KxJ@HZ}mZkB^I8Y=j&&gN($6^dK+y`Ms0tDEvv514$dgIAn z0tTM<3Bb$utXlD>;?FaL@x57#XnZK4EB&GcoJA!|j;L??eDC2HuChV>Lko@D@c3r+xD@1t_Lq^KvV^i%r4rJLk9-5J|{+-_&m&z zn3iQDgu>@}&;QoO+Czk}Xj*3$8y^lXC_8PfM7;c-$gv0+Cq%C$#2`o;jQV=1NVtXO z`myZ_yu8*aCd$Mz#uNk`sxM?F&9s1htBxKq^AOTrzYG_!Jj|AEVM#Gb!tL%k06&o? zyy#*fISQ}LyDT>jy1yc~0=vE(w)|r0dKeVN4A;p(h&tZO08&^U4u?B9xDv1xgb}69 zqR?~4w}()?BLqE>Sr*J0&EkVmpkylhh+e+MWW_ za(D-p9r|6CsrWHEgoL1Zcs{^*#CgA>4^P9UXC5Fyv=h+*L+rgO6t2=1HXT9cKqO9^ zq!sMK11b8$XoYvV@vV45_EQevti1^&y@7P@ z8bVs~65;CBWIYrM@T#4no_a&-L0JCC*DyWa;^twnGwwF#aE(O1_ZZ8i5%*5xpKdXx zIYQzIIY9N2;UiWS3RO$Aw2{JNLSf?Wyg=-M90<~a$P3cR$7@14+}x}!1Awt6BB42G z61PpKI(qywF04py8R+ug_ATBPAd!QHsMV>`UB@y0l%5>@n}f;P`;+I-5iqTW90=pP zz!>D>h#7hW*wgO#uk0wI&sSWy=SiN{fLYwmf!5PW@cXFyRnh(iMjPcJ$$B;^U-F*8 z9rep)EI$e_QmLlH6Kw4(dP;~~6jHR=N(a*Gn5=bq$?eqYD-g{(Q8No|g&i!^JX4Li zYe{iIE7C%cnr^y`De;jh(!uLjsU*TJ3kfOGqc)l9etudJKWJ*qP&hN>bXu8wQ)DPc zI5QG?T9t>ks|^aDooqNQ@k47dc_XEEvVbCpEu9a8}nN zQ(_(0KfiH0$y(Ox`^ZITVMq0>VS2E{uC{++&k1T%K7l?-Qj3R)_5`10N}Z^(YRz7$%S6(Ay`NArw7fqqEAz3sWI9Q~FAxqZ#vT&vnzdhq!o?Wq8 zIH3iuM*M2=LZe&?lQdnKS*H<6F5p%fuT;C%*8hR#!1=>5ZzoWjp>(}*s3S(!QVC2z0Saar<>_U$3$Y)jaz@#%6)Jy8@4 z{E_vCznCsB$L79=fh`m*?R_sU_+K$y>UdwGQigKTchTrRWx2%~7W_Jpw;IZKx<&B} znxEGiCXyjOBzS^oqZsiky_W4^1f~i{5xq1}kri>*)P-XP7w;Duxv zZ6v`jB0>c)f1=mm>HZ=NjBYzCS_#_m8E;VVm-O1NYnnKB?(#l^LSBZ$Oxt~7Oiz9X4FfaZepZs3mNJ)L0jMa1(raDx-=OrG$+!QS_xS^R z*>6MT{DHl&X0}L6(;(sgz+Q^90dVyz2T%bF(HA{s+liGaI!2Aw})6A~Y1{z%a z4f0`4qUrwHt0iyVM3v7!=(Qig#Zc2_wDLd1UjDi163^oLpohBj`fz~r+x5|i81v2X zxbmIp@@jK<3E~oUBQs~g41VA`uzGXG>ch-`-#Tw~=whoe>sCQ;%OmXKpkDyOd9wN# z{rbX0R_Ju zMoPDXEBCwMiS_~5{$Q_%Ydxlj6;E)u1XY%n8t!>Ai6+T%K}_j=LLqD& z@s2VfH~dc|;!SDjF=H z>LTxN6dVz0RFrQbv{8<6ZN_GNMGZN}+EV~mYmYFjYs+YjB1~YCfDs?AAM0xpx-X2v zX0MZte+-DgoVw=tkTR(|Wp=gz)d&1YV!zW1FhP^p@yyTvy#rb)0~ip!GL7A3&t!Kb z7$68EtZrci29OJq+?#xAA`pGD_%=)UoavJczl?mE%*e&AvwpMi^IB%e_3@MiLN!FZvhhQyCX`c0r7fTpgR{2aBvzNwKz1qaNiSuAPt_grxo34--{GA z9Z^iY?Ox*kFIlK<^R^viaMDlrTvCT4twHBhA^ix!BE@5gx4=L40;Xu4R zF9BnKJAt}8j2_Jf#T>w0OgseiD4dGUqs*OFS{sG3hl)O=x8*6Y6^2`!8r+mND1WJo z48JdW4g54@eVK`cA_zmWBM5=98jAE_$L}HIP4`4I)k5;l+XN1_jBxnFAirRFN{F4< zk|hX(ELKN@9a{zpbV4K~Mavf#lN|lgB1tO_DIaBNJtV1I&Y~5%Pp&X~oWnJkFKh4g zg7w2O$sWER&xX(o@Zd_GxJ<$GxW4fmo|Am}mvkKJ#{C{pC|!gY6Sk&rqQvQ>Pz$Y4 zrCU;|lqn6^x8JS!5xZ9i7e@r(U#Pw*Io0sXVtbna;HX1EXrjNM30-Wr5#Rz26iqlP zJ~y=UoR&koZnV@8X8Kf5OCo(1^f?=61|maCAj?IDNMGDXV{rjFwk<$D`i4&plVvC@ zBGhc3ke))+1#7C<;Xv1_JR{AgwYIhZkaUphSV?ScDQt-~YtZ}~7(p^hTeM<(Mtb#?oJp8Y#;;Zr$czUBdN7qNrEeyA z7p8}5YVaI7+-!s9X7^&7q1{b(NAv{8I&mLkGz5)TooB~~>?=OM+;_OoK!`|>*VJF} zO>n<(Y03e=j#NpB_S{{C)g}Y``dZ8}+j(UwN9C0Pz@6$^A&~e=7>;PqJVe8JRd=1V zN^BqD+;|y-KRTnM$@baV|&<5(-P4^gL9y9prtF9wf%D;_1a(TsxM_+;+?<)XJP$ZuE+{;E~ zqmL%TP@j>8JJqx`<2<=(pPl9xuc19$ zGf_Gr{`AolOEq6Y*#q<@F_k<%0StOrD#<*z#&B8&BvOxN42R4tD;%mbdVB&r! z!!??v2NvHlEQGTSgUQl`8fV6F&a(r7+-m+JSpFBX48)9H9c;*(>G(bQXlhZMl5n? zKUSWOwsq1-7~;;COWFKT?PObi`>MD$+%%8C_AzoS zmdY$?z#cKffit@6H{h^+jKDJtE!`(CM&Q{=?V*09kQyBt7BIFJJhKu4`Lf-GJxsQi zRYsiqFpjb^?mOUTr?2*KW-U^QEKkJJkg~N9AJxH_v8cJ9VXy$$EB6s$ssT(&$YucN za!z?PL;$wD(^xp0nx$K8oEriRoSBwpVwq8i7&|PcrEDG8wi-YeF3WAH%pNXFljfpx z>Rxis13n!|@c@Hc$jw#@p!67p7xxW>#pkR_cMR;gbM`Kl6WnLxx#ROX1>)|H$MWPz z%*|yPpuA<_XQM}BDIQj)i>2ayYw4|1tDEg^RLSxL{<9Nrjjt6u=yt?W$=@fh8!WBl zj_s$v`qI~cMj1WamwUu_Ku_5$&zA=5!%pQ_EbIqK3(`2k7=Q&-C?gD42VhOV892g_ zj>YnGa7yde)A2VPBlShphQDobBVWb{11o=lXJ^quTs*_5t_}X^jlF+{!M%*}H7@WL z01~57eq!fS?(Y`o2c{#{YR@(3gX6>|Q^rg4Wd{VCo(Jtj!(x-__w>;9#j{<+gI!qA zh!Fwiu%Ne8z?5Yabgd(iTcyo+cg(S`Am|Z5oFJqz3f124<4&9ekSp z5hp((3);oB6gMf5JPmkj2d+?tRd(;d7&x++Fsbw?N_T4g&9G#q?o^ko#RJg^xcitvpT zG%B<_MnYI38faw@K8~a{8I30&s>FeYLh7XGXopb_lZ_Vz5e)TJ!E9X*YE@Bg$pcy% zMi@ELn*^|Xa`0OqIekTdqi}>l*8*G{fPGoeaPob29{R5;E2nHGzym-!obeyF1H~+1 zY;Xf0EN%t>SUgLgpoFk67BrT!sP@k2TT8t#78LboG9?iFpqkAj3uTXtO=nz{FjyE^ zd3l$4apeyxi8e1`-)y@VUeFU>U}#2R`vT&CFdS2cVan;!pE#q#{QY+xg3(8iTpaz; z-Qyl?VX)7T-)AHAY{{(_<5_2<)Q|{+N;i3lbpT*9JnOIAtFX+PO=4ql^>ia1o z^oYTHu#t=2c?SR(-df_&k-Chrq?L1`#cT%BAcLC2aZBPQ(i*_*;RQhk9D-?jurP4h z_TrUkG4X!+PiN^KBRgjJg0rhbrXGXg!KMB&u1lK$)r|zzL?=KQ02{0!ql$Cn8gT-u<{XaC7W6$2NNMQJF+5qRiAIJlK-kwkg!Uaw@T!ylCY77 zFQ!Veu}XrMm+quWp0N6PkTx`wjO+pqgaCj57+yddS&e|he}{>!2;n1gzxt~(&*v*Z zPfD<>iNp^~?8g=|32+yX|K|>z-|I9~ZIOPh)7TiO_%3_opyPN1e2B?6R|C}}IUKoW zU6sPOnL2&91ndj7_}+E0o*Sna!Irb9LM;LM?{Aq*7NK&`UJI8ecarmCfsvg5-W7|IjU@cOVRddjcA?%nOm*{jSXGU)(}ostPvtXaFtjM=3jo zn$BxzI|R?}je7_R-DCef6JkAP-XgSh@AWgn!cFq6#7BZgN&C-lCPa+lN4HsB3TpMQ(5pCZ9TEAuK ziB^c;%?HuzY1gs!MEfk?&1JLINKO!e*5A^7PxnmNk4u46EO>=$Ub~6@mLll++RAA^8{! zYa?m88>CVmVVF7`B`w2k`ZsZ*P)bGwFxq7BNGNuAKx12X;t_Zb*n*s9k5bRp4E!Cq zAaRa8M{=l$d4dQ$BOpb=`?n*TeoiXk__s&^a7doWLu0^ex@dc>K|>MxMEPYS#Z00P zTYLP8pTeD@v#_fTzfxU>h4CzBGlKxq#ASe}A@uBf8t!B~=>f9%D?r|_`D8R{&u7cW zv$5UtrL|SZGA_|`32lwDnw7w?e24|$>TN|b<8U$6Y9SSnyAm0Wf)_&zUQhq9!o%57 zcUs#Vbd?>cq*(=tm}*j#cv01FSW2+?D)lAb@Ha+Md4J!9T>YS>}umstQAsI83kIr(G7m2TqDnWIy-Ka+|G~MUt*3n36w%`4Fcs9Vp zWt^18eSl%KD4_0g&gbf8UDy|mg_^>5 z`+m%_sq--uSxaUy`N-GS;RJbrHC#-`lmV^qk2O~$H(0pXD>weKSto0oGR0IKS`)(P zkr)(q?)P~V0y*UW4|``F6?NCQ`(a>U7-EJFiJ?PUQc#AH#vzoD1}W(h5QL$dpYv-0MEi`@UzbbJqEL7K_=xy=VXS{(i6Pb1mMx{HT*J z{ygE#ovMIlSD6bztc|&TzXL;!3*a^nugqkrmc!{}Q?rOXJy#%Z#XAII`3~1p|a``!nhT`G5dP?%`%$qff|$`uoMB!)~Y(>6|%hw4j`$yBcuS3B%Tw9Tl;@w-H^=}eM5Vb@& z$==t;NBWy3>l^CAa*O=u)~(lW;|lb-31r}kp>eqp0WgaIkKh26Mp(Cko$hz&`$n9J zQCBH*@a8RxYz-95e4fGJt)z}-iQ2Q+P?VFVvA4^fi@dHZoQCiRJbvwMm-Bl6{*3IzK@5e520B^ z0XqmWsbG$TuG9!b&$*X z?ya38=!7c{&22@!Qg>PlfSsOVa--wj?$Dqxwj+7~icrZ2VE>>#rd?T*@2z8yn+fqN$+7=d@fy;}IPiTq{ z5J@n^)a%pMnwoL&DKAFyubJ&A;Si=Lm=YLF0HS*{^!Nw?yc?bZ1b`Yn0*Wx8E5?|Z$YP3$l*Hp&n~mqs~C5#7EE zo`5~n7Efm@H_(>yI!9mwt3%tHT)0$W(h>={-@Tab!}@QN$MA5C9H2id!|JwPUK>!Ox!rz9vjHUXFpbjA<~%6z9u*~-ook;pzJGc&j0(l|yIo|FcjBb*YfBZ~8G^cOvTM4O7E>lzLK zJ!}lxyKU^*JCG|m`=yO&1SS`#E9MnnbHCf$<11JRaRR2S!R-g;*GO+x(C%vi>!FkbmH*bs<4`^vPz+{(yH=FNM(P8b#s<<)j{PbeU%7@L&FNu zv|rV1bX9wn^!BP!CxzDHi6$=DF9xbWU^))-PuRD|U#;58zy#XgbaWsIVnv)<)-HbP zD-M%jt3uphDLm6zm45?>zS|Q+hn;yh;e*;IbDtlF=5uktG-m2d)jWuC2K9EKys9N6 zRV5mG;|#a+F2__)_KN(Xna~0*;XDLf`HPDk$o-#5hzVeNvM$P#I(pxyG<{q<#I85|fKDz*KxiBhQG z^irW7RJY8a{*ccHCtOzQc7y}tkNw0pdtHIZvQEx2gF07VdZ9ZZogiwbr# zSsnawpQJyvNF)scRBJ!mUFxNx5OhAJu3L+WK8xB_Yx=e}LV~l(UDgTEY}Ax{!en5j zCa1}3w|el>?-{MI&0quBYa8$F`Ns(eY^c2trMN{PE{1Qyj1{qMCrFU%`Dh?@0>yQ< z9&dHHMhkk_a|!e66e9H*FT{@ZoR<|J%CLcDxj0^H-GUq>0zg~5(mp> z`pJXY=eoc^fPXi&wFTXO>mJ9P%aij(_KwKhXV$Iwd-wSGW|0bNp|6Hc;>)F>ze#?> z#1)oPu(K|iqWl-f<6~9jmSkM;}fj#HJuM(;C+-{NnBHL}v4=-0Uabb?ky zXNiBT?tc3-{#KW1jIxdt2KQ?1AM=o?y`TYHewVkL*v1dgD+3^G-0u!yc`(yhyg!N` zuT;qViqEs1w@p>yj#hxaG=4FgnPJVBO>UViL$pDOh4^URhMUaTBu&2?*y7SIT@>%t zJ0CCPFgLkvrbQ`9=V|3FI;5Ar?ygWoxOMC`FkBLEZ?`^qaf$&{KM$j|p#*~{ZH|^q zgNVg_=1@om92QE&8oA6sQkz-0_}|^;tVW9Ih$uh-X*m6JZnN9JG01t9$c!-lS4Jdd zPF9x_z7Dbe?dl(8&EI^qyo#0l8Y0a>0V~6*2pc*k++VCn?#=C3D^eyWXhOr@Ds|zg zY%OO3$NX2fx$kq;jXVsF!$tSlAt*0YoY%DAqI+Bre;e|sD8pu(t2oWuFQ+6g90DkP zl;&Xg2oqURkx$w%&kLx&im3sVmrT#kR5Y=+1hM4WjYqN2>~23{p**tuSlhUq=}(N7tAL#+wKpzZekL${ZxmSnF6QZ-Q->G#p0?o=3D8##7(vxe4m5;Z;( zY$35~K~#>n7}ZsN1%hAK(R00Xvtg0Q{uU@zqw(~Opc(a*`YQ<%N^;?jfOdSZl<_E9 z3@!HZeDzE0y^*h}@_gCnb~@3r366vNK+o$jTR9mL?BOcHFp6 z_nylI66)2Dw?+na;!BxhN)P8Fo>gtnOi!KN93s_7o??^4HO*KoAb&KEPd@cBNVeD! z9t^%D=_ToF+;KnnuW2`Q1}MO4ph=A|()*$Ex3#L(&vE<<5~Z(3<~ zvgVx`O0aQdCvh?xMt8Wnuqo_bE0~kwZi{N?nGP@^P6WoIWai`ecY2QlKPo-V<|&QiZ1b zs5{E;qIE=m3sMI0j${Z4Z3$>g7k9*GjFEk%jkEaJPnsTEbKS3&Mt z$v4Ag7Ig^%i*zTBSu3+qeUl&~&le6>9U|_$9!fdEinZM-pM<`;rgYD*`qN#DpdhKh z9B0E`cFtpN)}p)H_Bh-@IFHTqf^Fgjvc?+*2IhCMNQU4n3k535%ASb2sURqZ+vuIc zE+$e(hSlpGg#wqFHUZgb;q{4(aL$K^3Ee7Hj%584Ks|Iw^tW<+Dy0!A-^}4AV*sla zg&av`d1>u&CoEdJQ?>*~9`=o&JpuWSn(VgN$Wf=+p<&sa=LTIMF$L>=cYsp%L{7|Y z(ZY34`m56P>F+X^T-m#IKQrM}TMiZE&(?q&O6?TMlJ6^(XF3V_weN;%RPpfu)y z|1z(bf)4M4>0BEQ=SL(Cb{iF?EftHI!Rl2GQI-I1QALv$ilpVq7QXlN`|t*;NL`*F zAenKHmvXxniMWu?2S{i!vI%itP)I&pQWu0OeqtY0YK-s(3hS0BFdiWZdKfexFc+`u zf;uD2Y`*7Bo<)G4n*;iscWa3GyKiBZCWG6}Ax96uSB^c6`ljwT%Wo+@pc!vWrFuhC zxyQ>~vPkD$N7;l4`Xul55sDkCPl2R;Ym2v>>31F?9Ns6mK40+$8j`^>quC#+sv%D1 zE#EayR3cmQRhe6Ztsj#oJngD#W!4^^&x@=DH&T#acO7JDdAF9BkZcjBofk0Vf zb_7gw#F-L2sE5Z4j)`sX4c$2OCl@p8|4abKUP@J_YU73Fn|FD7Qn9p8Cw`Dvz z{%)S|S)sZIy(>QWS(&otwodlRPb-pYZqv;-0%|q!&nj&-?-_FKPd5!8RGILf|mLKgw`2i_*FckDP_)Tlr|u;z*1^LhSDCY+z<;D4{@ ziu$8$>p$SXa4&^Y{yY8)cS+fw0k3Trt)i9LDStVV|7V785#Se`VI1whoU&ey z_MqDX&(EXE+;$&_1cq-jQ10CvWD7@-3cxbZ!3ZjnKVbS;(aYDGqe9pwuG`2Q*@fT| z2&tXzrLJ&OQYz!olUpE``Dm{z!3O$&rS65FkDpwgvd=OKoOsuIAn|6#*vxySH8tCN znlEr+HRo@0dPA!5g1N-};*N&zr$+24%hsm+pEY_$o@+@9rf-I&NbcAVSN%1w{FQBt zhJj?=Z-#Hv7yIsuza_n#G_G}p-R~ZL_2T~LAyrvSqgE^S06o=wZ{hsas~_K8onrLk zTQ3rbm7kbBrf9`r`ol_@1CFWe7E#kXy$hNE@y$hJV6-?>@Wq@uKm-gXLKwtpE$k>v zFPJ`-Y5Cz-yvkVpnS-ezM|7dUK=Vhx& ztsow2b)_3o?-Fx4`muTCCF`rnmsPhoto0mh?xtZy$86rl1N&=PumK5kcbgs_t|Yq> zLW*pko(%?+$06NDn)jm=u59NW!qUf)NrZNJ93(9^`Fo66n?>pZ1Dh)Ge&y-;ecIbw z32CPEE%}LZhr4dD|=`w$_UOd5u?WSt@YD(m)fP)p5>EpSE-y+bI@anQluowQl z@t5S^qQ10hPTvz$J$=Mu-;UQfU7ZVxDR=24&h~cAA~8#I?xM`z{L;p%E#uZrZx-vO zzz7}u+D9$;UAUdhMZ>)l>OtpTdb#)Aw`9Sm^m_wWhzzehl}OIJA*r%0>R@$8&x6lWlOalX8 zU+gdfRiC5DT$zP~`VM)9 zOyCYRl2K#ZJEqu-5t*gC{2^m+^D@~Ds3Flc%Ixehgf}-Uh{$2iL{=7f(wiMi9K;eL zgE}O(ok9%jyK2(J1@8zf*}7Pxe>Egr=yxN5GkbG)zr$Mm~4;Ue6V{n?ix<}e7vLZLJY=hFr63!gq) zJ9s=pGoS6DRX8Gpwuq~M;W{sx1QLK-9-U@-m6b8crlLIZ(L+B=@rYbwSc>yzwX1C?)jKxudwLAhhb;;b&s2cPJ-?h}b|($mpS!?o+T{g`}+Vgl@0zQp-M{q9$ec+{o>^N?I`+|d{nIRq4nK`hA* z<_m%xM|lfJV+k|A;oGagCBLAN1XP`!%MDg=mkz;jN&%fUc&3k&EjEqZZ zSM?^kLDzPjB3hnfd{+>*+S<5@q$1tbTreT&cxj$aC=n@PsFrLy%pcNKdV?{NyC52-&yGi>H`IqqY{`XU zfS0uQZOL}=!py;46s3`1Lv6L&wI4d+HbWJ^>UX~n%(R{yJPrEIEZ0eYSQGivEH~SO z)$gJ&%yQ~j{jRtjlZo07fj5Q*>v^8}4W$a5)Y_?h$*i>#rFhKIEfEY2Tx~D&Rt-K< zxnrWA2Y=LfV^4Qv6lzV)4!KpOL2o4b>G(6NSCbk8v`Ld&MbQtF!L3zy`}qtZjJ0v6 z@^5B2lBYhs7iPKdFE=mDa!)3dE$f)|kKr2cpTJITh$UfWm*l!?RXUSam?4`_c}((> ziD~f?DvS$NH-!_~vy!55RTWf8efvU!mYAoJD%969$H0%f%5e$95CfZ?;fl4L=^EDZ z>>5cJk+1NS69Sa-AdJ#O2gDKIkH_?FSewa><0LmyY=8x3U#8Oyk7Ad`d#89~_*N^J zOz1(W#v1RP;{k)=y0pf@HGv1mZ_G*SGkP`FMY4|v{|XRH4{0Juuuojn(vX|CK)x4ITVPBCSNe8aJdA_4sNW9XVt4?Ec z1Lgkle~3%)FRnyyc-TFI0=|Z0J$v^sIjq>@b24(|*ZOnY#qoLX=m{Sowsb&BcHbuq#ZX|sw-n3CWGPI*3trnd*)f?MLR9H<>=V+y`eVR zUr7f4*>&$f`zvE?DW2#jm@eR=+jBI++!5$=I_RK-oJLsQlZya$-3zW1&*v*r+!x4x&_G#6 z-Xkj|qCM_!7#63R!pmrCzEpn~8M)v(ObpTGL6!)|iM-03;OGft2yBsV>5v~Q<;8GV z5sI!(xj0sQmdOUAujblZInP$Ewd_czf6wp8eSc9R=04*&(J%FsKN!S+_R$SsCJqna zuPu|`b|c(U7Hn5;NtO&O*zSsT5GzGf2x51mv4W>qfbt&zdVe+C{VTQPt-^&?v3+H7 zs?1xT8(8<1)9$HgIo^)TmUzxLht8c1)s@h4lxuX6>oEy``dRL!jr8Z8kDmoin;ehg zFKfVDKg8QnU12&ptT9JM5$b!N8mxfutwjU{ek6D-#_?H4dMmdySfN?<)SZXu*kOWC z?AtzX3XVF=03{mN4LWXoe)&v#z1`!T4%|{lx^FHaK?(&)Z)coWwi(~YQR+&J z{P@1a*fw;zZIr*i@{aVqUlHRH*fm1)bB$b(rYVmuoQaszV+Cvj>18k&#GB)TCcfnL z8P1whR#SlGwTd#W&L+Y$6Ij?d*^lkum$2a0W?fbH{2-3O)%FzCtW82ni`9I}a(l7@N1CGp?D9zJE%kE+j$8L;!!R#}xQV>;#scvTxWjyI zR|@7D4&4^(o8IYc#IznTZt0n`BH&8&c&8(|SG}OOf~hX~m|FKUkUi8jrgZvqKLa7` z5Mcls?3*e4XNMM<*A@&WIJx7q%lt_8b1RE=(=Ice)HW-1(dL&+bk~l0RQ3AC*i;gU zNpwy*m2Wn=eU?l+JrXI=2e4Gx6(uzOz{fVvZv2;3edNdkG0(z*My=(U8zV&~$?*H5W>f+&%{d{;c zzdgLg#$YgE0qewewlwOT^+aJdiloAKk5nzzpk}$kZKc&|RRuI(w!zB29S3F{_9)f1>lf_sVOOLS)2ddv6-_JRIE0e^*V{^{s_UJ`? z2F1hdGeP*@rRVb45t9GhBet+K);;V9<*)~XlRHhjc?g+@>l_T&T~$uReX?2IV5~q+ z#BVolW%lc$1oowOTa@SL4~V;3^e8S)Jwt@?pw|R|J>~8-hx_mxGac@=U#cz9l_9l<_Q)_57z#)5(%jJepwdNpI63 zax|urtXgcidM8h^UD&%cA4FrmYVh`UY#RgRz_4ZeVJU+4Jf2p_N+k@{Dur3wy-F00 z3(-{s*xwnO5k+Nj@5N^Nlnw=!Dc0p|9z#DrL#mmi?#?)_9icbV>xmt|+TypTxrn(4WGvr|MH!yQ z59AmfY)e#;H5=s~9M~!X^m^00n(yM?xay$cfBtdLyn`f@%>Ai zFYOgI>!TsYuFy-+hYEiZMST}LF0!BF8vZ#hZ0R>}fs}l|cNfkiyGIVOR;gV?Tx@>H z2DSO^F4p^tF_N`~9q6MTH__v#BurAQz0H>mwc#LYq1XKkeLA6WInvT{nVmJHFz#IB z>13hDIzB}CGzzB%r+cDUQ~kspU^Op$1wROoEA=%Ibrn>q-QMDn&w}gEXKAjUX2FB2 zi19KCvf|UE#ed#FW|1eTn@LH1k`1y#h4AxsF;(AjOPg^SJmoF1Ll0SJ) zD{ttB9FZa$BY6rjO#G7`iW5bD(oN;;O7jmuj%q5}?_aQzs)~Q#z=bw?3o-Tkc|E&A zDJ{CBmySB&kjx0w&M(O`LaMOm-OP?!hkK@8YnlkXG)|Qg&TTS_jy%8tFGu~-z~w#6 zH_kvV4;u%viwqk_0ltmIyB%WicITJU$GJHT;xMMsQ=Lh5 zRc+=lnQ%3hgh7@+&*(}6>PV&Jbe6aDMF@6N z&z>30mrex#{%ZR7e0{^onJ!7V?2G5V;=r*QL%VG7odnvN21gXxwYz zc+F%0bR09=xjHzhSS=BkMIfatspQvf)D`>n@c`u5BP0Chfohbw14TdrQ8W2o~<;?(b6Hkd)3 zNN@7rx|-FPBmB88;)1MWEX8Ske~Y7`u4uO082gifamrxFf56L(MBn?3399pNS94gh zbXkJ+)s}TI)uP7vz-JtN=$Yku0`3m76#_OTnZp$6f)NGb|K5lHvaZEI% z#K(!N3dt9EP9AIkStzGPt94VNrBb8_^nbKi)&*SrOF-KH&nUa_>Hfi-{=r9V*fVxF zz~!H+=aNp?n7O|NN>M4nUjlFof2y|`RDV;E-}qGaOZ6OKCjP%2_VT~|K0^NsLD5Lp zjem}Od>}6Gh&kQ*@K2#8vggqIa_`<0s$(U-*tCK6Y>LqMS3iHj6qp$SSoRl}->bKO zR?jzn{8)c^{MqF^vLs1%J+<^w5b(KT7OI6~lN@y_wwb3st9UdNN@*0CA58C@p%b=l zvdN+b|142}z-)0@$a6$*vIJ9P_E|=dAM{f2)`zZHDoI~gW|Pmt8z0pSU!6^6$qIUA zsif}weMK&5H*rHO2l$W8tSnJoP2=pVJ5&YG}Py)vHFzJcBDaXFV;$X{F*#A}W$(-uJiF z+r=msWd*ZF(ZZ7*x#}#B`uo}LG++{|^=S&ZpT^-Uy?-}}*MIMsY`D+Yh#8idul#AZ zSDM2F&l`lh=gMd+D_Fc<4n?q+HFPL*&9gQ?%aw8nR-Q(kevRN%zenZcoEKy%8BO;l z1I?MW`oV?KVU|#c?oI|unWyaLYXZ7f#e4Y77z0ftzk?nNhx33+(iWT3>LnGpw|HnA zi*7^hQ_jGrKN6RA1@Ox9itYtd)ZI=GwhGD`ET|F50xVYB9e_KB*rKfB4;hff^K)D640#l2L}EJmYTOUg<*PkBdjHl~Lw^(X!Y87yO>6u$2PF#k{l^ z@7KbEq0Q#RVw*9GI2z#75Iq?2`>Y!|K@F7cqAF62 zVjV32mZO1A%3-)}`lu|=ZbByZhZGfy3MXa4#H!;gDf=euj9VPC!9C%y{#6dC*D6sd z-&|9>VBE)&g?bh4sbTdj>=Kl@VT0ckS(`Zo zyZ2}in>L_|`#1>j_X{T+=n3cpr2qb8L*}gdH%j%-I=<&QfAe2Spj|gcgZh5H2IpMq zUpw+yU7XVw8v|R8rmB`{m?hGBv^*|5+q4DE8RULVEbbw)w=R1BBt-KuXsUcL+jBmO^YQE~ z"CH^oNYAbMTBuz68RQ($7STtmR|B$z83c&wvdlD@2p7I{`9Rq?o4W1jQT;3Q_e zLUcpsUE*C+E1^k1;jTj+HIv+jg}%A}fP7 zNUx9lwG)%%JT;E%1F?e5??SgkTXTMp)V}BZ7}JP6-XnROt(C3b-~l9>DjFPIA$^5M z^LG*`v36(j!(>N+jT>)l3gblfT&m;!vXWmN)X_baJ=4l^sYz8k9chK*^0~ei>b;1< zrFn<$B*VO|P-i9DClQzMx;pg25%}JzDpjDuZKUnSfX0BVE+6_C4%B!LO9Bn>l9!OH zjJ@S%c`{NQk`ype{OD>n?<-gLtC~#>&L`p(T3jTzT0hzRsGIixj@glTtDSp1(itZY z+HY1^c<`h!e6OOZ=f?dvpDPhu^Xtv@kMA6h_9<)Cae2!`axz&& zDlCU4dt`w)NyZ3KwC-BDeMPe3TQO7+isaVGexaD-1RYp1G>e;BFv?d$sbnmbCYFWo zJ#8`sl0|d*TT+tMX+?SN!*}?=m!&%hyw|=mr@vbk2$XuR z%%Dp#x5}j2xJl$|nV4}dx!GHS8nIlO%vqSUBgxLtHtIyiZi)6OUzV_&KcU#zhb+M!*6+OhJA z(Yge{TV}LL6}diLZ>P(nwCv;`Ie|YJ5hQ7ZFzCf(^OcB+`HolWTw$#@*wzrICmRwG zV|hRY)&Pmb@MOs4V`e)krw1bVSG%$@bHZ@lM{kc~mMMyHUlloV#7x(!-hvan`lMik z6C#esQJ#AcX&czsAx)~s#T^vP0)*g_O1N#kY<} zxL=jHR_$lSP2A{p0rCjx#5oi4XcNgXD{5H{G@p1ATCl~6wEne4iP}fCx^*A z=UH8RGs5qF$sMt%jHUy3>xnQc-dmfZFT#)lsyniJpIdQ`G!K}0?}>V!-d%A3;*aoy z(n-FqF*xS89t89Sc)UX^w<8`&Er!sRz;4kRf9*pe%LE;e1i1`LF9fhS+^b_zHw$M+ zO_Z=UYoYBN4Y|HtA1~nqzHpsL9DKy=T~h8>TUtbZ5L6frCBza;J841f1=yEOB0*kR zZPyGx4C8b+e}{1&Cl=x^$p?nnY%<4nE)N2Iyx7d;bJ-hT|tKz+EHxrfMe4c!5v#+4sB zzcPtIYqgvA4G)VHtABO+A<{*|jX}u8smHE(O0R;Q78PFF%DrWy%;=M;{+^S7cDX(Y z03!(GNkpABxB&q(K*Ayd(~H)%ZlgF}{F!rSZj6;=f>b1N{`?CK69 zv3#HhW@VbJ1Q;0M)NlapCcr}iaL<*E%x<_jalyKK?=C%mC^Y(TmeUtFYbCQpL?=md z2Y_p{;gg>fMAze~T?|CjfCeQ?_{1oEjebZQ8Zs-_XczhXD5gP$vc=BoaDyw8JGR|G ze}FsISuQ$mGWG_$;&^0?yml-|+icW8drC4+Wh$;hl5)0*o3u7g7B8MMEPmQ2j*=y+ zS2v0zKc1#L?ps-uW9>CPJh_8S1-@yqTl5y`<5`c3FeH=2!WeGR%pu}@%(qlAi z4!!3S&#ns%@k-o6C(<`3GR!5?`zB5xp$9^V4-(~fkrb9WKtIeW)xS&Rv@J*J$Ts(o=LsbaO`RTt>7MOg$h*lp2-Y+@C$(oZZ?_x)0BIh{`%d=JXtAJju+7ih_N? zm_uV5xTnlX8hv3c$Ix*eSS}A~rakN{EQ4)}W)}@RLq9&5BdallO`(%NAz_8bso;F* z=cr74hdl5A=?)qTCZ#=COgmGBJw@hI`H?UrX1wEp9qi_SScnQ?1+2WJj2)yQqabz% zSms<_APzK=?h)=7F$n+wLIMCn1glYxiSzRZ?H{dUM|%ui9wjvb0Lb$IDn@{i(Y&qW zbSQlmE=v}$BUN6FR0&c%Rt}vuga&k^2|^0bTuA^hygDRdY97%Sb5h~x{Gnaw%q|2o z3MHt`xOP>1)ipbd2Rjx>I+~Dt5txrAtb>z$f)mxXr{=>Tg*>D@7~Udwqq0I+*3Kx1 zd^g>&qIk8v*k3NEm#zeW##c5ZgdhnIcOjq}0?a6Ye-sC>2wbnpuj_!4 zBe#fMW?=F~TtH9(BeGB!P8xy)6poTE!*NUyP!nV!oe*TO0-!hWD2EQ3IbTdZknxBf z&#Q*u!7gbk0FVlRJ_P_4AOI31q5W?8Zcu(L@00Jx&<04BU-XkT1ZfBWAS?uK*u@uy zK~i>~>>vRqi_l;Ipst4Cu^N%^D9*tsXndX&F`v0tUjCq?ymyXRd>#N91-yn;uNYN7 zFoyzmL1pEDn~TtXsVC2mGZDNv@<`}D`f17{R18B02|?q7;CSbYxTGKJ58!wJ0KVlY z5P%>W%1WlVQ-zAEGONgV1i{~l&W960Tg{k9eq?!Nk#06;)4}kh?4RF1DLpqQ0hu>{LIQ>1ICYEAAqUcPAz~1It>4GYPwkB~ z9~+UEOAL3Rt9dD>?G;a=pK-j+!YZMvr5=4Xf+j!7hQR7qU2AOLlD5tR*#Ov+1#}Ps z;734r;6&trdWRo1fSQ)vxfbdN2{6o3LA)=Cp*eW@4fJdmVhF=a1>o1ANey8Tfh^F< zNkibB(r?kFkLN%k2-0;J#Kg7i6P&~ZjwkiDZR#y31py`IB{EsWH8;;q9|fgtW$w7< zI&{bp+QS4K+M>3K&E}yb$I!Q`ooa-g)pO7z^;e(rvUgldjr-e8d^<)jzn-?wZnV!G zeUU0Sm|FRuV6TxCms&;d2<8hleQB?w^?RpT62R* zW@dqX2W>31M?r_;Cm18~Bei#3cCoETmtlOB*=C!x*lD7`iB7 zalP@I9>?nnFQbT?Q(nBxcvab1=@`ix^*Tysu<}mYDqx_;f3V3viljMr(6RqeXqdb_ zl8dK)@4ZNv_*3XS5KjN%83 zf=OO&0Y<*P9NL!=yX#LzCN%~-CS&y-qs<+oe>HaLjCx{kbk&E^`&iaCnX!yg23e~` z@@ky#{kSsaSUAPlls2CbhH+xnO$~R6e^$s~+%3_wOI(^HN&ecTqRHfqfJv3yNwrs# z8t*5y&L*#G>Mtf=UDchs9WYgvJYf3m#K z+iQ|-s3_7}>BW`l7Hzr6KCvzercAx!GFlSryc5O#W=K zq=GE_RH$Oyf`RtCNIJAric^;&;;#NPYdy-XSqqj~?%KIQJ8kl{Io~O@HEXTp9qJMj zDp`tzbd|)woVV%xbE2zbqNw@F$SAFb8_yN>>P6q)MZGH;g0(Zo^=Rq^vB!B1Q4hR& z2OfK;A~wIrEx+LN_BDq8?ejCT*=v%L`|*cB(Qs~I*U9(ujPvJ0M9&uAUJ?GVvPMq6 zLX-qsAg^EG)fHqf{BWove@7E>N}{N|w_w6XcveE)$4I_Eq~GnikdU`9NkJ7qIsfrY z%_3CeTb07H$y?}>j(&p%1rz^75QHL}Zy^Yhy9#}Vgrvz^xia$41}%%?D;kPg424n@ z=MfhNLB>R|S~N&v-J-ql+Y&l5=s7e8;gEp1{E%v)!1hKGoG`JIP`H~g98Kgw4wfRL zT7VgJhZ+qm>Yxt5QYc6`J7L}~)Iq?=@xxN$+0rEqnC5koBv+M(UP{)*%VA2$V6nAt zc#tf>de8?|3rzI};)4#NdOe2JIoFlKWNLl!+5-FoOtV!aAqaApX(1O*SiHOPVxMF% zX#JV1L0C0m%5_*CoY3yu6}xLhWr*9~iq^A2jY0|+-G|B5A8k$TgRQ9+a`G07#8;6h z!g^B3Ae`_hKnb4bzZeReVaZEYmFy)p|||lbVNdK(>P2g zT!~vk?sh|+NEvKB$yL1l^ZJbG<%9SyPqf~DFx|lYx*@ToU$&+X5#Iu86D7b21#loJ zxUrm-%Co%nZafGil_&@K<@x6;C?%g44PTOKSNHEzI4~_>kVHx5>ks62Bv88u(rcyh z->`J#r0^g}#M(LpIy2$AUe->?#7w7t{kKn&;ISE?9&-pkD5pr2X2?`DY^TpM>~{{Brv zX_xK0ci{I$mG3)o-~AW9mu8?JG9Lv8zLjhG9ujyI0ObPy3jTou{c-vL=)bgVC?l&D z+y9Bmq7*)Re!R!!`ump61(jt@HDcEIqBL28l|`vq4q_z)*wUn~jUMe!iLpNKppPz_ zN-0ae{LZn9(fAW3DCu2Z-HN_)rdQji(cd1|#~VznPpZKjXKGvv2i|X{SsB+&lrZYO znc;jJXsmm_cfkLtJbX4*L$mp7>8r`=qhTX;`>_hO$(Ti7^J?FAK;Q2$oD(jh7 zav*McuIc4<(UI~1d(3d^{K9_Ra+Ov#$GhsIty?%V18bM{tMgY<$!21+PS!ro-2HB* zvOA`nZF?@Ht8TG=T(=;xF!?=Og!JM5d&iAqe1_nKYw3nz3&#YDk=o7pvqA9OMy4yd zh8+1G%Wjh&BFruk7h5FDK5)agzDB9%RIr9%$O;IEQpZ z4h7s~)iaZ4aW8#)U+tm9q;a1bnS3dLZO48)W^_S3Y|oG0$lS{BQ07) z`jXr*b+~r>8C*voLQYmlNMrlcybN_dY8yG~cdNs6XvchK~wJkMd>N1w*W#d3HK+ z)q_JJ+^y@KQJgWucLaGAzN&w*8SpIV=PTPqWw|Jw_5~#+NGl>%|n@9g4ilyGskIgT~uPaqB zFUY;gG|+Q7kv-f;FS^UIz=z!|0C@et;&tnwa@)WzOVrEk>ji|IS*~@5%+0i&v7EU< zmXT@xDZnkOjNPRln+v|5~JDzQ#wr&kWAkl`;=m2&{iMkS>$_) z+C%UhV(%N7-zrmr**SIV&$?RCZ1^M!%Bw_pVlzDi58J;kC30)uW26~r?3HK5)Ajz; zCqK)sa*2E#jaDpTPG#>I^S|L5F{jRnlZmz{R=0YMD&mID^$K0bk1f_H{>PA_5|Q|T zuEOp|7z6gRYN62F!1fv!=$fiPt5@2_+?ZJ6jA2sq6BL0X#C&tu=&syapo~ zJm?3FXMk0P^p?ZLS=g*;@!Yy0@A(_SF{jp2*XZayZzrd(;@y>4*}`ZQl$B>Mhj4Lq zunyS^tUt1|u(hn4>kAj1%>A)%qX6u^IWx!c7{VAQS6faICDV{grnIgInpX`l)yv?z zR>l!1HS(05vM{=c5%IKHA&ZkTYu3ji2L3@rZBN9K|mK&T;oTqHKx3<;8@%Hk#yLsh$=k+y2x_BuL z9!abX7-sfN6|1FgT$S3MZtn3ciw*sxpFCjHd&7(6eo>?Y#e>Otj9*$kj}x&~t^Le< z-h*Ow6MNek%JD&mX_opqu=!TqyVFdIKn*3w>$=fn!&)oNcc|XoQcrq6w?0+K9FWX> zIR>UVxkxqu>ZFz#4`DOhk)xE1mPa-)`2vZ znEhwvx#G>k?KMz_$k`xpd`;U`01ntIuZAwj6^|NYMZ`MWPG)Tr$+n6hE{k&G$^+nO zYFH873?GlX$$?KKD9BRWw@ay5M#!b`zu0@vrzRi1-}XvELP$uVNJoQ8Zvhbj5d)%h z4Twq+44?>#ik+&Vh;%~l7fHT7j z-Y~;3w5)5b&v(nxxwAEfQuyS%Fj`c3Jn4QBx)l2 z^$=9f9mqXR1_PHzKTtBxpe~9p!46y%5Vazp1ZaLwPP74XfImo`n&@yKa~P%00Vv^D zkegY2fXCX&B>)h}ZUGrEeb`y8U;zj~ z0gI278NLg#6$Etv;zt(oMapc}9^1vp2$A3T;K@y_OfXM%(ttBS_F+Xs!VUxurmjKj zSa$=VZh8E+EWV*nupqD`5n@@4=PrUPgI z-_h(qb(hfqY}T6|H$gwbAO(Uwss_OlwRotp=+69+p6!z|REPZKT>hVICT;Hwoc&4$ z56(`hsnbAZcUnQmzShEPY0a+GmZe+4-lDbcfz_$4!1leCQe|Kjm+vZK`n&4x%wf-f zb^tCX#GS27zf*PS@)F<4oq(H!xf9K%9B2f!RRq=#B!HVPdfxnnD5& z5;=wQ-65^F2+Vt1mppFaoqptdASz{3_!I{R9JAo+;X!*ut60~==1FAdlH zNQb0&;0sUq@eVSNKtuyjlQXyw8cU)V<)VVgT!soR8Uut`V_&aAs+f(s`9X@|yU* zkZ6G|c_W^4VqK>X2=hpGnMugn@sCA&euz=pijfY9*$uV|?kGD#Wx2cc691{PFfSEiU zhZE-g3qDDhMJD=2eS+Iqf?G%&(fZ&)-S`V^{OO)JPf7G*qsg(vC!UGlNT^>#jJa&W zOm^(`N>nA>AXcepT0`*rerLoY%GPvOevAY-rS0tpW|64#(yKB;t+z&fqo;x$(ip-SZ&_Px{E42&C-p6+2`^#{w$Nbn zbmn3TJTqae-m;+!{TG)qF|%VYW@WLRAsJCQ8H}L}2+s@lnD}T{nu~7yr{ov#nHRjq zX>cCQ*DflDm(+@9!m1|~F7kqF4ZTaw+@quplV2cIGUX6yTs)X>groyWndu#w;@cTJ zWVA+T_6|06s4KflIIDJyrtIZA2zHmw z4g4jNU}+lX*}~i%N^Y@NuC+)ivNh$Lr`!+;-K>e7C86)rK`LyX$^fh25x;PGnXYJ$ z`WN4r>91tbj$Vm>X)6)f#-=21=d8CB^0#K5l*2wk0Im#-)i%PKiFlL(&17P%8E|VR z#G8)cmMCVD6{WkgI3@DtDSy%DFE$dx!*y`*Z;WysJp7wGmkkctJO%|s5jG6SeJUot z2uSh7wk&3igqHGxFs}8bEP5%4fpK-kL;^r02~*A}WwHMz(ZMJ}#zEcRFy6cBKDGejOC5p6nVvl$(innY!2#; zLn0{Ul|=R*alo5_(Zhi<-J9c>bzR*r+J!N5`1-ycY=V61{$T#n_)4r_EK zH6SPriSnsPIqW*n^d{t`9uZyX-Lygh9U>st5+UmZj1n8%;<*>;FRSU$Lj=q_`MkHT zrJx<``{k4oA~4;ZB%TU=Z3SU7@I@kUo75oAtmWHDxg=5!)EF6*Ia@N_$GWWKoq&%rFbj9Vd%=l$=(HLk_pJek65X6G>CjO1d$Lg_yS9 zTuiwrmvv(d^Ul8mcr zec+v*;c)6ftzOQG?%v5h`Em3svHsbTeM$RxGQ7IKYNs!z^>I~nuTJ(c#rjmcc|K2y zZkTp&T~5DP)Q^~X&nn64(@NSY$>_d7@0i1?rO>G>MSP`sPjB{O8gt%m$2%uyhN++g z%kgx^qn^kwJ9VK-l1 zmrrMIiI3b9=e^@Ar!w9XXCmep812!Q=UH)TnmFS7Y2+zt^rqTqfVfP}5qtdDXyooF zMSLvg#8{l&Sc30ZQuu8ryM3trl0@7N|)-6Aevn`&#gZ>eS&AE)UNe3vF^*hg7vA`3n^c3O$MB{ z+040=S#x3H+SE@J&!V01w>8Wd%@nzA6{>a#ZhvKuDYqLT#rmdCCG8t);n_XJ*u&4T zXOD&V^NtbC#G7VB{ATj}X3l`_1=6X{yLxGNOvS-$n{|TryN=Kh*63*?citScCIRw`Q%nEC`i+GRekZ}%>Dgy9#6hZNodDTDObVk?rqMNXC?f$A;B!Z(|G7Uh?-d%fNCY;N}N1atv)IrK|e1pGOQrOc;SzQPKY0}X$ zGE*~OsjI?L`xTjy_zNFqciNPbTj#}fP$$duIG_G_l^(@Ww(sb<<@c69$*sRQGk-b7 z@o?xcN)E1{J@@PMl~b3Z*1u#eUA~iI&sujB-f+AVd-cl3caII1E0H&vH&zEXZa15; z>14Lr?*|8r-ueE%Q~vu)lIi>Q-?(^+KXfywnrBXd(HplJ+PoP)&Hle{lX74y=Hyn~ zm92!QTS-s11eLJ=8HYXq-T_Jf=~H4Z{)bQbCgkCs_<#A7K|5#g|Mn@nu(l=%|E#po zb1R>wxzYc3%lWYGZo!9thRhIZw(5Dr?2HF;HAVlwPdV)JIpY8O6{%!NqiKiF7LS#D zKVy>iHh)S!zJ^e9exww?IG;j~(ClNcuMA41eaPKL$o1quDVnG{7XC9b(N*>S z`O1Od!rsIIjpN7z{oAo7sv)Pt~K)8Y}dOOtZJC@ z*isLt3@gs!R=>=trF*hXE*1ugENwRCtQX=n><{WZj#M9Xj6uo-s-;$aFl=Taliobx zn|sgZjvs@h_DsK>lYL0Gkg8EVnbmc2T_h`9*@y9ZUhPN17HVwzQQN!bK$70~16`r^ zn+iVCc0vpks_CxM(`pry#!+p(=Mt~q*@d1=7xIsgsM7XF$UWuO!p=8ev2|7={@%MY zIyx?&SI_Eg+x}3-2RnW$0BLC=`+&*Qd1%z|;~7~LD;9ZVZ_v?@*<#`!a`>85QSa~# zC2g~}^+_;kCce=VRdnOV6370FBMmKEU%a`vt|CbC1lJedck*+K4~7rg7wfqrlVAePeqBn59oPpEC|3!>S%3G`O5Q zh}G|8EKwV+UlUPLL_wK7@^e0steZ`YK%wfRs3RV_&P_6JO7;hF9;G;3rP3G&dI_9T zc@=!!LRmZAE--C+CSSp*{q+0~6p`HK2Q!-0-N|fobzeE~L;m!L$owmnts=K{_pU?x zTPqGV7zvB~`IONx9AXoAS0tAbsI22E1@#BVb;~^4|4J~?wb#}1o8vxQ?3YgKIa6=B zxeJOvJYHLLy4_7Iexv3SjPQvra_^8ESR(4yJ`FTCeWLfK`EA_0gH1{arso|~s^2M} zE^~KZ`5BYAKfm#^Pj=GBoNF8WMkXPC;44&pTcLQ)?%;>F;;T%<}d!5X_aE0n}4JP&uW*dCDryqUeMnaCUU?Gi1 z@7Pvb?0~k+PVZdUrP|mm;=}Xprt0|{jUmGI)bkBmDd!sD_b99mzLw$m61Z2WRhw~f z3OpwKZk2$P|o!5ZuhZCU90AUwKVHOzN_EwS+(@X z*(UXsI3Md7>z5j+qM}M&3{1xR+LkJE5phX-hf6I_U$1#+9C!1|a{ef*A94IqrJ`+d ztgU&UfL7!B!0r5Y$>HIRm;JK0UsQ&O&cS}*P90sncmMMwoR_2gWBmW~klFXgr*fw@ z8h$r2Bg%cgF578+_}#SGSN`O;$&?@ScQb&dk^7dfba{2f!_T_=|IT5+j&HVNBWZ!s zrZWiJ%{G2#e9*p<8CMUZSJ2yW&ubP-uVx)B6SI~-T_n11-#HG8ZAV3_qqo$pAd~c@ z#-#{Ip~BTItE#0zm8dK0P6w|btfu9LL(aQlFIAdeDKozJGS9T^(4pnZivpM8kA!`` z?qJb%Pat_uY7q!!}*7`8v%M& z%ure@n!^2LQV>5%7=>CO%&aafSK(eAIllJD|0p4qU=Q~+!Q*4HP@Mf@UzbCGRumi&#=Le|%T9b4j=4r>s z0rLwyZloA?XQprr`6XZXwSlG_4b*Yy0n%MV`!~Ff6VRk`5lDa_k&}JR_xmv<%>6za zIm$8N4*`GZ2N-~d=Hj6Mg~t{EI%0j#&H+am$RGwFcO4dY36aF<$4`NPRiH5HiPv9| zhctXsrEKKP5mO^vFe2g>0~m1U5XE7MG+;%|=Z!Sj{JM|44AvO{u))qU?hjZL$Uz)7 zg>>KNB_L}4OsqZd*rgyr%87nXsF}vSGcs5i99WJG}P1704(-Xeqf-9aKU*rPPi z{WeY*?%p2oaF6QE$_jKQN*NG90Rd0O zC^Qr9En|7V!wI;>44~J692xg6Ie5wt`9od2uQ`TAo$|Ss_3WCXf9(Jl#7yx`X(lb=D+hXpa%Xd?3CxVHjhRb*dE1~=QD=OTI$Lrgj2q;<8|0U_V?;%Qy;RY zP7(wqqg8P%oXCDG!nm)W1d;CCCtL>{i=Ij%l_Xj`za{UxAKA0@|-A^^aE8dCjnBtQ(=V?G1H0X!fZ z=MgSS@&ZT4jxS&iu@58R{2;E0EwPEsE5W=TMWSj8Oas&|IuJptjGJNTR zD3H`$7ok}X+T#S?yo;^qe8f)zN`qtUKEQ5KFR08(MNz_b4#I-lbsy58vQF^x08}zZ zLHrmr>wKsW9@<*S^==F{FqqoSk#@}yUax@~l}#hkpsiiWjx1(+^AV{O8k9&#mmh%< zfpkbK=K)P*izasO_akR!`n{2Tx%+I2YhUaM-{gt*FY^>&aoHTcNQHXwXtsDW-~#X) zRM&MX{02|9K9zUen#U`XH6|z$nwd15ne#>v7wR&sp0!giD22FMmXKxWa#6wNwz7?l z(hVDNo2-ul*?^p&kWIFHXm;AsY%XKxvH5K28&>I1jA2TSaYK&bcy>>M^*RwVfXfx# zAeq}>PNoQ^KrtKiT>I@@(su45J=d`N+F32s5CJ_*%X1sgyS<&~&OUNNK>s4=YUbq@ z#N;ee(BJ9#EP6g!q=3Ag|707@7Hc1e%B>So;jD3VWH*{3Qb^gx#Ml%jconi}=*SfG zNMYoSeB>p;J8ij{ zHbwbz*aEGR)`p_e8&}2R^1cCuYdALZEn#C{x>o6+P3f>!=^9Y@jaqE_Tj0eF%oDki zJY#H!R#A&_5yyx8cL?bn3)C>Jq)!VwB2xNOtMsE+DI~gdqM`V8ckYZA?-By(UKsOjwveQ-M1_oVM8OdqFLkuUl3-ISR|)XwD0EQ3L6^o z74NJ&_Wd}T7g_e5R0jEgQ8TF$WH}UlC$Xvf<6#ndg+T*Hm&R}NtS~A$|CFo{^L@-Q zJ~kyRCW45p86j5kL>C=Dj+GY0(B-g|ctj?%=E|!|v43<~7Pfq-q0|S2@zpBnqq8q1 z0Z;&Nb+3Vcz(93NG`(wF8cGJ_(BG&~fC}7`f0@Xr6uDWvl~TTz!p5m}{w9SZDb?## z!9A^#|6|GGxs#7$N8C{?cU1D9zn1Km8%0{L?hTPEM@9ZiktJ}I<4|l-vXX>mF>AmQ z=vivb8t!$U95R!NS)-%vaG-5w)vyT0I8`t)tSH&!9+o4gg9( zLvatL^bbZ_v@ufd-->Jv5+E@V;<%S8I24;|CevQ=(xGg5^_`@+(0&f zi4;Nj<2eyH4i=6>oQTn(z13qwR|<^HnyYawr$sxE;et4mj(o315);$xj@oGHU{aAn zX=s3~H^H`qFZI4_FM31wF6E`ak-7|IG73mUKobAXCA>Y8+00~{u?&b31ENcO$2p1S zGDUl)2^>wpdWW+Qq`mREfyrdFYP{wQ4%cH8--`?{TC~LsW4ne^*k8mqe?$>;EofdW zgvIt_-FxT%<;T+7p)D1a6gIlIF26Zkv-)6VYdzI*7I=(DDs)a9G@7Hc=gW4 zu1x9HOUaV=1BjuXlxiJh;X|)n*IV@Vw9;Fj(DT4huiS?RY%v9DA=CF}e*>47;c&bx^!KCZa?5u8bP zMwLdTw?w6nM}8aZel@;sQ*Ev|;pOeZF!4#c_@_!cbduTRp&OG=`mBeFCo}sdiLCL758_kJiWBBHKB@#GjZ=!>KCbR!%|)hvqCTwG)I8F`aSEJ`V2dh%iWE$cCN%n^)F;tE~RBGO~zwqYD}1iq}n_59y%@_Hor1; zdO0g{5ht~faUSWAyVNpeSAw>$r=IwwhE#|H3hLD8^adXGdh_lomh(`ZZXo;`>Jr-(77YBU}Pd zd=HL}_kRPv6H51V3f%d=_jA7qM0FFH{c6aEl_tmTt~yvq2OTzrN2$Y`@mu@Xw#&G= zj_B@hHreE5POg5Ng0A)-Hmisd{&i)@}IVU3gSGwJQ-9LgnHuLq(>er2R%iTz2-jl0~meN(sW!>p- z8EE_ECTZ0dzxd`HX7zb9ERo1V>kgNGqVHT$s#sGgUsr6F=Kj8Z=)gvy&acUu)!!Kg zOEKSlz)Qc&PTc2Row&B@-Mq@D&kK+G9W43tX(iM5`|nir=7c1BI1HgsZNfMAvt8|2 zBxYk=a=R%@$x!*{!52RqW;e(ARwv4~!uq$y-*ERbz9N*J63<`o&ro_ayEgGev*X-a z6Aj;S5~ZPR4X(mB-1(Ch`Dgjr!pODtQRNj0XDnH5YI8(+H|FdvF&Xq?0|Fk_k;@` zxDFI^C9m-W8&Q?Dl`4ei9?*&|l(=vX|G0#}6lGM^@^Tz?PceDQ;X7ZniMcc0Y`JT+ z?No3_QRe|m#mV^+sn9P)4XP8|_r2MMrPYfOnU!6hEy!*|z!19Cym+(dQX`{)vHczs=WswU6z8MH<)7g?a8#{SoRl z!9zyUchY}jWL~XZMA%>40wV&1)73|gxs|RDsO3JeKptU@J~MvG0=Xm`=eo~|*{5J# z5?q~27pS^NW*=o-qpB@j33bW*$Q7ug@_F2IbaZ{-w2rmq0i!(MBN0~Sd!Ox2#~Y2! z#bLJLb7uz9ExVt+6gDE=ME3h%k}cR&Gm(BJTxe?V^2j38>b2r%NJ`>2*Zh;Cm+P)a zfp<4^^`W(kADx=q{gabfGF4yI>Vr4mr|Q@x*-8@=MnfD+RkRjQDs5PYDnEL*5T+{O zyma z<17KKQY{}oDt0zlq{*)2EB*a9&DmZ-p&RdMbA^B2Ss0lIb{*!^(B!h%buKYw-3!3hv zl8gP#R{u7m)+!gho7qVFq@VrT$FDL>AiQd3{_^itk!`FwJr(tvhvs?|YH%gopGN0Y zzmBC(vC*ov@W`U~|^VaGaK z(O%4es)-kH1H(4n6lS1|X(r-QXPZC+^BF!|g8NV6VR9EUNTW9s{kXGz|0?Fv&cJ~Q z)`~%$te=vRe&A%>_@(B!PtJEj@;l3M9>u)0 z99+eM0bxoeCV$XL$?SZWT_|sOa^O{0LnA{o-XHN<3eMtuU8eT?RV7~JT#d|=J!4th zA5)zsDPNk^X)AXm7Oi*qx>$X$$?e$qt{s!3cgGSf)Xt^eOlPU^fu|q&=j!%W@ClfW=7ACiWky$tsuf8NzhSxl2?VOkS^|18mY}r1OtZ3;d zphdvvBNyPt(8gPl?_Y#Hd*1@U-(AfXilX{EEq%;ta&eWrruFV&LRH$Levg_u5#m?P z-t?sOs>uKxNe-nX{?s=xOYF-~UxY;c!31Rz&97*~gb$uNk=rqlLQA=24euHs2!-ZX z6|GnprHbBm)>XLckchlDc$~$RHzu4GUTJb$c;+D;d%T{2{dktVB$*{&fN8Q?~*p5v=`KZVsTNb@|?V zI2#1V#sME<2}JEU3a7!SI}enBmKc0yxQzqO)Rt@a2`VLG>*sy@K-|=uJzDUMuzzjrkVcF4!O=1) z(&ZEsL6@7qGaSM#0sf<|R({c*d?i2nx?gZ>i(v3Cu%WFrzrw{|(2lx@BNEh-o?o6ryqqx`DJqMOqJ~%mGrCd)f+f^SL5#6=Eg{7GG9^)}%47@V*A>Oi+ss_E30N13Le64Z$HJ{G#9#oV@p}?hq;?VFa3Yq$OL9o&KgywlnVjDW<@%xWLw6O^yA&2y_Vkz6gRo*?X9k z;Zxvf&^msi3A7F-yp;G|?X+zlP}M9f|B(7it%G*;$jE8#t;jWjvKzs&gqa`vCffMb zPPyusN=&EVeMLn+dnWnWPx^iGY05X}lyQ@oek$1hA|y9Nk>Ie=;0oclKZ(`B`Ut!f z?1ZUfFCN+4E4s=;XiP$*Q2WXC?`9wi@G*LV%V2*5Gx z6hDzVKd~e~K{zIJ&REAB!)}wMJp5&n{N?KW<&yj)thA0={IvzuHqq)H0UAl{H?%g~ z|C8k*Nh{Pa9j$K__>GAJS8v(0Bj|PXagfNbE=yQhTjBNe~F3-J_LQ*`QLmUF0E}Zwb3Wm%C=U9b? z%U~&;=sDmSx>HBzZV0#{q%!F_Yz_Uw&`@d=?V=HyHyV~>h^1(R@L6f-gD@90uvJOn zJH)V;E+UczVJjqTlSV{7JbVQgp7%3!gNf$I46pLQerE)nF!yHzoHcjspkc%;IhgYU zS`|)=f`oN;Vs?p9>zkOV&X8WX{|R#xNho62D(LxWaF9mC?aoNp-3Vf^);lZg4ikap ziXNdv^#o&gG_VaGksRzGnjXEsK60@$cm>ZsIvoSApv({apA>q77P4*_#&A&|B4KA) z{}n>(ShGWDG@E#ZbfO_!!4R$xwj4M|i`{k){^){c0hl}|zwg9oHh&rc09_n1p2*eC zME8-=o1@%(W86FLsB$J|mmH7o!aOYy9U{e@()=GqH2QB5jSFHI(O+fQ*)rB7Imu5G zu{`>h^*oe~Kv>65EF`$q$GZ~4dX9_ z_WTW^DbQIuNKqIvi-$w;h;KMF8~FU9Llu)j*#wLY6?oMdwnht0@QlA*7=PKC)0@Wm z{a?l&69KR#K(^l)Pl2&BY^FO3=J&kR8r6^#4CBF6TVuZA|N4O51mqnoB$L4PKLF68 z5mM=&E}A}u7AZ^$7Q(Dr#a|45Zav1Wv>1ybp{KYqX6yf9Knx6%&43tMo|_qKvM(NO zVb+Z>o5F}aLSQBy?n(vp*rpwWs>7iEWk5KXg8{O8F^I1ySilA&bR&H`IGsex7$IRK z5SRvh#v%YXGcXf$Kz=)6)hNDfE2}~iD~#jVkWI@Z1d`aA9rjU8AvM!I?O%Pg(m#E) zkyjvpO7Os9POeipu{&hcGb=*~vB+A)AR0Jr$8*_wA(4TZ436?1P5DJa(dvUaqCvVu z^lxh*lYm~KCH{@2*&ZVaEsllo;*fjPw0Q=m8VAFOWa`Vt3|bcm7d;9SS%mMu7wO!N9vU2r7ezDcyI>q{^QlP6c#v{2`8GgP1<^s0!u;T)EYgrA4iwb;Cj zU#@W$iOHI%x^1Fyk|PQNdYP6QzFtsAMb>7EzKXSN&<+sH4{HeXPA)DEDg9H=xmLsW zQg3gTN_#8ofbhn8+Ci`CyGI*dZXRDJzodx1_F4{->IvnQdD`i%Z8=(5Z2MA;<>J>K z%bvuI@VgnjklM8Ls%hzXkg3V*(bUGVs>aUyerhgJo2kv)f1c0Y)E2R7G7)M4Uk;oj zw)~=?m1LTyPBnM^^h58oV7Y_e4fye2ZWWmHLvZVGr?rYrwvM+;^6;AE&b1!C+;*Wt zRMJtq{&4fe)i$y4HdzJjdWUnOr_bqpG2a_(Q&MQ=yTHRI*`ATp-dbfibJh5Lb^GC+ zcAqt6)fMxcYLh{IQ}xU3hT3G)9lpcnr&yJ5E{DrYetbg`duzM%=8D4G%a`9ia(H{= z^;@^ex3_oRy1Tsv@oK?T2x?C69%~OK(Fi`(S$(9F%u3UL zI*C=#C4{b=e72+G8QxhD`_9+&NJ=l(Rzd$z^xL`^o=k;q!RW))2lZJ81uQDx?nts>PPfqyQ$m*WI0KHIg43jFXeDUFLx7}r<*L7KZ7I$plZV9iye7hhof53-WEIR z;{)W|d$}vzPFB6f{s8wn`t|fuxeuB5Tm<)bmT(WhR)Mtk=>>L$&g^T*?h($s<(t-r zkm)APu@XWQ+4fhww&yTA4_cG{<_A@lv~%e}mi_tT{Qsf299P!_Jn7en{)#9Z=YZ`oOr1-E=5XzW<|7 zp;1({QCr2gM>@SrHUcZ}`=Ktpm(T7a?}-5&Uu^s3o;sf%SRePHzK`ag=stIckTY)} z4&6ORP{Oy=BU*DZ_(TslN(?7Fxsr@K4Mist$6`r2WzKbyKd|9_<} ze@_wor7oXa?~H2y>#WZ+T(h-WLFJe3P(^6NznfcI$DV&Rw*iRKx}UjiuF{$!#@*Gf zdX#+<+xO+}3Jeurs`%z+b+b_K8Y?eIQRZSvt7~^JLf`lz)uzNfz38YHeVJeesW93O zASTvNBuPVodmjpv>ld8jigigkO43 zZ&q*rptO1x9b&REoO4S%Ve6{ZwOfUA@y@k{QvRg{?Y2n=OBd-$dQBnD!8XqIQvP>z zy7IkhHhI6MJ<#DloXRufN|YWo%;zRKugWgi#`AVKrry2_?#?7hbJ`KEv3`+IYSTk z3?JlbaHvNj8ZVmtEPnMpPll_pVDm`W;j{;Ocj|&;im`<(?H|E~OR9q*1fHA6Z9>17 zeRWh!(eDnClF#~~qPwTGd`|Yjxz7sfuXysgWIkk?uGI-tRbyq8)W_HKR`x!J9ljWm zw&eCppn%INz4v3+Yn29XXRE&cHr1}#H}dI4b+8k1z%Hio&?^&)htgxmq|!9nQObzhQRM2> zxV7Y_%@vc~`(?pSrfToP%9ra}5B;_aRn|Fv{f={tMr5{B!k^`XaxL3G)J7pE?0P!x z_CwTWRTXpY`aaG}C%)qF{FbaNUo5v@{prM6G2JEtT2#I8quH@F!MyIu(k|uq+Hb#B zPVJnbn^-=*iqf6d`56*5C|}>@ls3lE*-(hZn}g=A)bG4YhJ5T2{^|Z4^6+SD5cRCz zG=&N-{E_fy?VO}&T$r~WoR2q;lPFUi13mw5>f$4j>S)+5GL8=t42?t8R37EqCYlR& zXI^9l8S0lZzCSm7-Dz6c+99PC9fC8>;+N%pBWtr3YSrs#TiyBQf!%_?9d~w3lZ&)U zin-Rrt4xuR&bR6fYZ303okTtxzSA0Ci}Z+d!v5@hr?b5lbyNG`9$?g|CsG;ZKlxcg zu&eXTN30wa6vEk8aJrO!S|+JCN9tHt*I6&s*swo2G6qK77AY~Y=9>Rkb36Vcp{_Sq z>2X(&{kCf2Y3@AybE95Ik)KJOrg^G~UA@j)vB4UxFpoT=z8f|_Q$~C9)L(Y>-S+yK z%HYn|Xtj<@4tyT75ZT+YfbU8sJten#)v(AhrY z56^3_mlEw346T{Q)Tr7cxh%e4iG9J%89G~+%qWH%0jY~@`AxF?d(lGP}E?t(` zVhvg30H`<$8|r0U)#5vF&)d5BqH$e^?}dBEZ7!7yBs7E`?NboI+MN<&fU9^w2((2i zvH*7oPbHtp2#BYppkN2Fo%{l9SIzA)g2iYT(7MZU#MB8P%KoLR@6ORO2!a!6bn9Lq z@DSkgPbRCrfjK26!Rg2he{vl341CTBDRd@Oxt1{F8Z5y{Bt#L6Ri25N>Ru z1c{ky)G1DZo%e&DUD$F&m?T4kGij;#TMnR$JRmGQ?FvKz3I|33Xe}NIxXm0mN^OBz z(kc`I0#ptl3n5fd|Egrk&&ck6f169aTfwT_007Am1F<7VKS;0q)J$GyH?#5S*hAJXTh5N^$-vp%qA-2>^NQ zV)k`Be&wVN0mg9@v5rI>fl7ApaaejP<^T$keIt-^4IsX3)r;y}KQy?5*?i;;VbMEs zp^h-bfU5}G`#K|V?7Lv!O;#r&Uj`8Y5{fjq%;#~Wo@_CsAm1YbpNgK-l; z`+j$_gIGeD?O70_d3U&8ptW)fzx3H<^?)M)kbd;u{pREK2By*08Et`xO%)wRJ>BTx zj$Zbol3w@oh}&A+nZ9}l*Tim*n{L@b44+QikGc=k@a1xGDggMqXI~L?%70vL2SzUH zf>_XT68yx|bAv<={hUPcMOl*b8rYz9z0d5&_`4Hdn)+XY^og9el)Z6TFpjkiuTvwi zom?+DPRN9Wuhl&8_L^(0y1Vt)y97Gdf`L-=V=;c5_m77lZUa!QGyK_gi7KWq8VBC1 z6<|wac^ePLd7PikBQ;6BqKtha2HxnFTm3UKQ%0jzU9>EOyf#grpRZlR$2GUiqfuKxV`JZ=zno~dfYVIZh$=wF9@I4JX+RUPc9 zXMn9Sq`265bOc9Q+B*I+;EE1ejXxWzVVAM+{26~pMv|SyRl|aZp(!q><6J^YMnlif z=`jJ!hI?3DQdomW7!!ZNS5lk}n1%?LIb!%*kMPc<@NPp4TRT#mJ1fJ%ekY8LjlR1_ zj3z~lZ=x6J5jCB9z85g9GU05}#D+l&Ns&u+kt?H->@IbRNsB*%+dV@EXNh!^7K$#F)>ac0&Drn1;0 z!tpN(PJeWW8VQcKDU64GhC-jGl!%lHi@dn_=~S~F2kQ<7~@ zv0pR8LK3}%lebA&U)kt|IgFNPJR~!Efu6k3g4yqyL=8!5To8EV83&a>7uBc4)W@yS zVqhwk3J$KqfaozGtD1-vDrS}zQ>^*oM_0_x z*zhEfV6T3}9@BTt(YyL-z`f`;0iDF*MW@472;nC50^#ViK1a`{%KU-9KBFq&% z?}CoC&M@+foTVqu%lx&Opj1qhHCC%TeBo*shYgiQPG|F z^dyZF>y^1f$%Hzh*2XfqTqD`raeD;JU*y1m_|wtXmLP0umPmzGVqe&~=Vel%I(W|Q zmKO*zMSCeu$JprhVk%fShaI90yZl#(O3YzD()y=FB^PYCV?{!8hj5f0YwQ{h#Ga7* zg@+&huMqW+7gsg_+@YWgwKzXgK*DU+Mg6NXdCE~aT4UB2i6?JFOl#)!5&ttpr4{@Q zQGvXFhp6tU@p2g~_d?t5-1)74x2V*HqHlOaB94-Vz(_K{d!*tWE})WF43P7E>+{cc z6+c-jp7x}&`HU?EYRyD<+Hi#;fIctRzf*cljytW<3896G*4Swqj0*zD`DgQv9imFI zSMV4Zwudy22kQ_qmE$0B9Edp%g%ZobGcjw}=thy8lSVmbST^yHWlV1h8eU9eL-_+C zxxc8nO0pG<&^#y>F)sw+rEykL0g@aN3cwIF&OIClFMH3AO?D93%EDFa%dkbx4gV9IEf?EsukwwNmXkj9_n&PF&UL$0H0X%qx zi1}=cw8MjcwN&~R#;wrv<8sQyJlR`-Id{e|)&Ow*BETLFgfd`U6QBX(DzU=UQ3Phr zJHX<^HYTT~WPtY{XWOm+Fw3f8S)gr#Xr#wd)3 z8>yW`gD^tE?O9`uuxNB-a=YQ{7~kYpshoXdv)BOZ7QNREdLgAy5N0~HL*sRa!pf=h zGaaQ?VY=FHc6u5v`e3J1+iZH{UNyAYnd-fOzLmR%y=e5-^70#`L%vMr zelR>}xiHVCyCAK*=yi9FPnVBcXJ^})H0_@1%RROKtG#=TX8Vle0RB%1k&vNTTp|Ri zwS-2i6Kz#0REK{Wv9;^zv~D#cO>NWlLhDYd7jYkgY=Yj>#;v7HlgNxFA&S!0(BhQR zi&a}pW&d_NXWQ91J$tvEv%P*^J(u&G^Zd^D_k73-AzyK=MbR>(5Rz|PV*7|Kl=4rM zhhiBuY~_s>WmKMH=Zp`SMptf9IpsNSk}G@sRmv9IXlK>!i>ezIimC;rd`UHGp%kpd z-4@u6d`R3j+tpi-d)fkqeGNXJ=^9Aw9;@$O8tPu=gP+R0$`Oo3i|&OP@Hrbq1gSMc zYRoJ3P;4$Tz?)KIyl&PK2lki-dAkJk;0Y`8=l_&9d5axm13v_72Rwc)io9xrs37TI zJ|dGHfC!1-q9`kl*&1TqrpgSRF? z6!5c!K;o6pm=(T|y#A-2z6O-n`Mjy_zZeNYCGTsBzEfPiJaRoz3w>)NYfXssjgc@f z3Q3B?`<3}O)sD&19ixYpO|xUlQphrMfBEJ3fkD=VJ*sOB(*xo#MH$$9<A5;WxpR`wtgFGPi??4A&$zC2Wo4RO>44hSUQi3NT+BO(Id=9<6`ApI;>=_|G_Zd;Mzh2KUt?KxC}G9D!anX zNpE$94{MQGjte3sf6|6V2f6HX>Rp zJ6j7|imdl0z|GcrU#z2j(^(bDkKVSW&Of^i`)v7H2ey>voJKncBype+rTs(OtjK2e zg?G*YbOW@|;jy??5KJ?`QhAq9CP*P1czFdzyYAW#0Qz_U;Jl|pWu|Chl+Dne3(Od~ zN9gD&&n_B?A5~ok0Fukg`_Yr0_I(sIV%2;!&>U#W%%tHZPqWO>%KHigMn7Jzqgw={ z;o%cRnMW}fb{V#tZ9p5+%-;v}&58(fMIFPM8)_HI=&P$Y8_x@59`OJ%-EK@qli~u| zHBz`nvc$Ev@i%HfZp3SABdFiGfCSzeir7?#=;+j}@uw#>#J?;h`v^T}cyc(Q2EfXe zD`eAd<1uoqS&$fkrNw$mZL#7#OpLWvcn91>!b_RjR6YE-5gr6=JdbkN{mmnTd&3)0 zXsU#R!D7eJH+`0rmyqPW`K&+27~~Xe_l{BPK5lj?BMwTQ4c&oWAS9+| zt!;4+4`(lP4-eHbO3~6hPNRz5h`Ke4KaicE(8NduC;fMFx3qWX-=jBHnP(ph2?d53%LshzU%nYr*z@CBGeuNW6aJ-`MOAoI|DNXEl9 zJ}cBXU%<+&+BzZ0A%nTaUYW(q5q^R9jX@(+6=`>;Xs>y%Z~gRybKz&kp~E>v>WkcU z$IMhwjJo2%ysEWzovMKSbtR*7xfX37bb-=pw+YgN1WsC8Y-q1lyb$C3W06Wjo}|99 zV>*qyFZTJazCKk=)Ady+vnFVNtR27Q29?9l{AQR%zIJ?lUf-a)2z%05F{E$l>u%<6 zn=SV^Y2pxNVfy)4`x#4zWPLZtSj({6?xb9mv-0~qhmbG&B6(GQ`3C#S`gYY-f@QAO zI^J&BYDSI^-R^ngTv%_R$w`}_6Dw>3Qa@gS`kStw9Nzxlv( Date: Mon, 29 Aug 2022 20:11:42 +0800 Subject: [PATCH 074/106] chore: appflowy_editor 0.0.3 --- frontend/app_flowy/packages/appflowy_editor/CHANGELOG.md | 5 +++++ frontend/app_flowy/packages/appflowy_editor/README.md | 2 +- frontend/app_flowy/packages/appflowy_editor/pubspec.yaml | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/frontend/app_flowy/packages/appflowy_editor/CHANGELOG.md b/frontend/app_flowy/packages/appflowy_editor/CHANGELOG.md index bac617937b..2d76371553 100644 --- a/frontend/app_flowy/packages/appflowy_editor/CHANGELOG.md +++ b/frontend/app_flowy/packages/appflowy_editor/CHANGELOG.md @@ -1,3 +1,8 @@ +## 0.0.3 +* Support insert image. +* Support insert link. +* Fix some bugs. + ## 0.0.2 Minor Updates to Documentation. diff --git a/frontend/app_flowy/packages/appflowy_editor/README.md b/frontend/app_flowy/packages/appflowy_editor/README.md index 8e239b6004..e450a5a7c1 100644 --- a/frontend/app_flowy/packages/appflowy_editor/README.md +++ b/frontend/app_flowy/packages/appflowy_editor/README.md @@ -21,7 +21,7 @@ and the Flutter guide for

- +
## Key Features diff --git a/frontend/app_flowy/packages/appflowy_editor/pubspec.yaml b/frontend/app_flowy/packages/appflowy_editor/pubspec.yaml index 9eb730f213..6816037b9b 100644 --- a/frontend/app_flowy/packages/appflowy_editor/pubspec.yaml +++ b/frontend/app_flowy/packages/appflowy_editor/pubspec.yaml @@ -1,6 +1,6 @@ name: appflowy_editor description: A highly customizable rich-text editor for Flutter -version: 0.0.2 +version: 0.0.3 homepage: https://github.com/AppFlowy-IO/AppFlowy environment: From 4b7713d7b806396e669b32e63d99f152ee045f8f Mon Sep 17 00:00:00 2001 From: appflowy Date: Tue, 30 Aug 2022 09:39:20 +0800 Subject: [PATCH 075/106] chore: reduce rebuild when end dragging --- .../appflowy_board/lib/src/utils/log.dart | 2 +- .../widgets/board_column/board_column.dart | 2 +- .../widgets/reorder_flex/reorder_flex.dart | 20 ++++++++++++++++--- frontend/app_flowy/pubspec.lock | 4 ++-- 4 files changed, 21 insertions(+), 7 deletions(-) diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/utils/log.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/utils/log.dart index bd2f39e926..6f35ae4195 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/utils/log.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/utils/log.dart @@ -26,7 +26,7 @@ class Log { static void trace(String? message) { if (enableLog) { - // debugPrint('❗️[Trace] - ${DateTime.now().second}=> $message'); + debugPrint('❗️[Trace] - ${DateTime.now().second}=> $message'); } } } diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column.dart index ee1b501e8f..2065d88ba2 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column.dart @@ -112,7 +112,7 @@ class AFBoardColumnWidget extends StatefulWidget { this.cornerRadius = 0.0, this.backgroundColor = Colors.transparent, }) : globalKey = GlobalObjectKey(dataSource.columnData.id), - config = const ReorderFlexConfig(), + config = const ReorderFlexConfig(setStateWhenEndDrag: false), super(key: key); @override diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/reorder_flex.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/reorder_flex.dart index 90556440b4..bff76cfe52 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/reorder_flex.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/reorder_flex.dart @@ -46,12 +46,20 @@ class ReorderFlexConfig { // How long an animation to scroll to an off-screen element final Duration scrollAnimationDuration = const Duration(milliseconds: 300); + /// Determines if setSatte method needs to be called when the drag is complete. + /// Default value is [true]. + /// + /// If the [ReorderFlex] will be rebuild after the [ReorderFlex]'s children + /// were changed, then the [setStateWhenEndDrag] should set to [false]. + final bool setStateWhenEndDrag; + final bool useMoveAnimation; final bool useMovePlaceholder; const ReorderFlexConfig({ this.useMoveAnimation = true, + this.setStateWhenEndDrag = true, }) : useMovePlaceholder = !useMoveAnimation; } @@ -370,11 +378,11 @@ class ReorderFlexState extends State }, onDragEnded: (dragTargetData) { if (!mounted) return; - Log.debug( "[DragTarget]: Column:[${widget.dataSource.identifier}] end dragging"); _notifier.updateDragTargetIndex(-1); - setState(() { + + onDragEnded() { if (dragTargetData.reorderFlexId == widget.reorderFlexId) { _onReordered( dragState.dragStartIndex, @@ -383,7 +391,13 @@ class ReorderFlexState extends State } dragState.endDragging(); widget.onDragEnded?.call(); - }); + } + + if (widget.config.setStateWhenEndDrag) { + setState(() => onDragEnded()); + } else { + onDragEnded(); + } }, onWillAccept: (FlexDragTargetData dragTargetData) { // Do not receive any events if the Insert item is animating. diff --git a/frontend/app_flowy/pubspec.lock b/frontend/app_flowy/pubspec.lock index cdfd764668..8466696ec9 100644 --- a/frontend/app_flowy/pubspec.lock +++ b/frontend/app_flowy/pubspec.lock @@ -28,14 +28,14 @@ packages: path: "packages/appflowy_board" relative: true source: path - version: "0.0.5" + version: "0.0.6" appflowy_editor: dependency: "direct main" description: path: "packages/appflowy_editor" relative: true source: path - version: "0.0.2" + version: "0.0.3" args: dependency: transitive description: From 3eaa31c68c65bd99b4be67259226bb93d1cb26bb Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Tue, 30 Aug 2022 14:28:17 +0800 Subject: [PATCH 076/106] feat: #931 highlight the status of the currently selected style in toolbar --- .../extensions/editor_state_extensions.dart | 8 + .../src/extensions/text_node_extensions.dart | 90 +++++++---- .../src/render/rich_text/rich_text_style.dart | 2 + .../lib/src/render/toolbar/toolbar_item.dart | 147 +++++++++++++++--- .../render/toolbar/toolbar_item_widget.dart | 5 +- .../src/render/toolbar/toolbar_widget.dart | 1 + .../format_rich_text_style.dart | 6 +- .../lib/src/service/toolbar_service.dart | 5 + .../toolbar/toolbar_item_widget_test.dart | 18 ++- ..._text_style_by_command_x_handler_test.dart | 16 +- .../test/service/toolbar_service_test.dart | 12 +- 11 files changed, 240 insertions(+), 70 deletions(-) create mode 100644 frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/editor_state_extensions.dart diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/editor_state_extensions.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/editor_state_extensions.dart new file mode 100644 index 0000000000..56b0c7726f --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/editor_state_extensions.dart @@ -0,0 +1,8 @@ +import 'package:appflowy_editor/appflowy_editor.dart'; + +extension EditorStateExtensions on EditorState { + List get selectedTextNodes => + service.selectionService.currentSelectedNodes + .whereType() + .toList(growable: false); +} diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/text_node_extensions.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/text_node_extensions.dart index 119cbae8d2..9fd85fdb76 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/text_node_extensions.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/text_node_extensions.dart @@ -29,56 +29,63 @@ extension TextNodeExtension on TextNode { } bool allSatisfyLinkInSelection(Selection selection) => - allSatisfyInSelection(StyleKey.href, selection, (value) { + allSatisfyInSelection(selection, StyleKey.href, (value) { return value != null; }); bool allSatisfyBoldInSelection(Selection selection) => - allSatisfyInSelection(StyleKey.bold, selection, (value) { + allSatisfyInSelection(selection, StyleKey.bold, (value) { return value == true; }); bool allSatisfyItalicInSelection(Selection selection) => - allSatisfyInSelection(StyleKey.italic, selection, (value) { + allSatisfyInSelection(selection, StyleKey.italic, (value) { return value == true; }); bool allSatisfyUnderlineInSelection(Selection selection) => - allSatisfyInSelection(StyleKey.underline, selection, (value) { + allSatisfyInSelection(selection, StyleKey.underline, (value) { return value == true; }); bool allSatisfyStrikethroughInSelection(Selection selection) => - allSatisfyInSelection(StyleKey.strikethrough, selection, (value) { + allSatisfyInSelection(selection, StyleKey.strikethrough, (value) { return value == true; }); bool allSatisfyInSelection( - String styleKey, Selection selection, + String styleKey, bool Function(dynamic value) test, ) { - final ops = delta.whereType(); - final startOffset = - selection.isBackward ? selection.start.offset : selection.end.offset; - final endOffset = - selection.isBackward ? selection.end.offset : selection.start.offset; - var start = 0; - for (final op in ops) { - if (start >= endOffset) { - break; + if (StyleKey.globalStyleKeys.contains(styleKey)) { + if (attributes.containsKey(styleKey)) { + return test(attributes[styleKey]); } - final length = op.length; - if (start < endOffset && start + length > startOffset) { - if (op.attributes == null || - !op.attributes!.containsKey(styleKey) || - !test(op.attributes![styleKey])) { - return false; + } else if (StyleKey.partialStyleKeys.contains(styleKey)) { + final ops = delta.whereType(); + final startOffset = + selection.isBackward ? selection.start.offset : selection.end.offset; + final endOffset = + selection.isBackward ? selection.end.offset : selection.start.offset; + var start = 0; + for (final op in ops) { + if (start >= endOffset) { + break; } + final length = op.length; + if (start < endOffset && start + length > startOffset) { + if (op.attributes == null || + !op.attributes!.containsKey(styleKey) || + !test(op.attributes![styleKey])) { + return false; + } + } + start += length; } - start += length; + return true; } - return true; + return false; } bool allNotSatisfyInSelection( @@ -111,29 +118,44 @@ extension TextNodeExtension on TextNode { } extension TextNodesExtension on List { - bool allSatisfyBoldInSelection(Selection selection) => - allSatisfyInSelection(StyleKey.bold, selection, true); + bool allSatisfyBoldInSelection(Selection selection) => allSatisfyInSelection( + selection, + StyleKey.bold, + (value) => value == true, + ); bool allSatisfyItalicInSelection(Selection selection) => - allSatisfyInSelection(StyleKey.italic, selection, true); + allSatisfyInSelection( + selection, + StyleKey.italic, + (value) => value == true, + ); bool allSatisfyUnderlineInSelection(Selection selection) => - allSatisfyInSelection(StyleKey.underline, selection, true); + allSatisfyInSelection( + selection, + StyleKey.underline, + (value) => value == true, + ); bool allSatisfyStrikethroughInSelection(Selection selection) => - allSatisfyInSelection(StyleKey.strikethrough, selection, true); + allSatisfyInSelection( + selection, + StyleKey.strikethrough, + (value) => value == true, + ); bool allSatisfyInSelection( - String styleKey, Selection selection, - dynamic matchValue, + String styleKey, + bool Function(dynamic value) test, ) { if (isEmpty) { return false; } if (length == 1) { - return first.allSatisfyInSelection(styleKey, selection, (value) { - return value == matchValue; + return first.allSatisfyInSelection(selection, styleKey, (value) { + return test(value); }); } else { for (var i = 0; i < length; i++) { @@ -154,8 +176,8 @@ extension TextNodesExtension on List { end: Position(path: node.path, offset: node.toRawString().length), ); } - if (!node.allSatisfyInSelection(styleKey, newSelection, (value) { - return value == matchValue; + if (!node.allSatisfyInSelection(newSelection, styleKey, (value) { + return test(value); })) { return false; } diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/rich_text_style.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/rich_text_style.dart index 6270127610..93d088d66f 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/rich_text_style.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/rich_text_style.dart @@ -47,6 +47,8 @@ class StyleKey { StyleKey.italic, StyleKey.underline, StyleKey.strikethrough, + StyleKey.backgroundColor, + StyleKey.href, ]; static List globalStyleKeys = [ diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item.dart index 9a1b2f1c02..ff94dfc111 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item.dart @@ -4,38 +4,43 @@ 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/rich_text/rich_text_style.dart'; import 'package:appflowy_editor/src/extensions/text_node_extensions.dart'; +import 'package:appflowy_editor/src/extensions/editor_state_extensions.dart'; import 'package:appflowy_editor/src/service/default_text_operations/format_rich_text_style.dart'; import 'package:flutter/material.dart'; import 'package:rich_clipboard/rich_clipboard.dart'; -typedef ToolbarEventHandler = void Function( +typedef ToolbarItemEventHandler = void Function( EditorState editorState, BuildContext context); -typedef ToolbarShowValidator = bool Function(EditorState editorState); +typedef ToolbarItemValidator = bool Function(EditorState editorState); +typedef ToolbarItemHighlightCallback = bool Function(EditorState editorState); class ToolbarItem { ToolbarItem({ required this.id, required this.type, - required this.icon, + required this.iconBuilder, this.tooltipsMessage = '', required this.validator, + required this.highlightCallback, required this.handler, }); final String id; final int type; - final Widget icon; + final Widget Function(bool isHighlight) iconBuilder; final String tooltipsMessage; - final ToolbarShowValidator validator; - final ToolbarEventHandler handler; + final ToolbarItemValidator validator; + final ToolbarItemEventHandler handler; + final ToolbarItemHighlightCallback highlightCallback; factory ToolbarItem.divider() { return ToolbarItem( id: 'divider', type: -1, - icon: const FlowySvg(name: 'toolbar/divider'), + iconBuilder: (_) => const FlowySvg(name: 'toolbar/divider'), validator: (editorState) => true, handler: (editorState, context) {}, + highlightCallback: (editorState) => false, ); } @@ -59,103 +64,205 @@ List defaultToolbarItems = [ id: 'appflowy.toolbar.h1', type: 1, tooltipsMessage: 'Heading 1', - icon: const FlowySvg(name: 'toolbar/h1'), + iconBuilder: (isHighlight) => FlowySvg( + name: 'toolbar/h1', + color: isHighlight ? Colors.lightBlue : null, + ), validator: _onlyShowInSingleTextSelection, + highlightCallback: (editorState) => _allSatisfy( + editorState, + StyleKey.heading, + (value) => value == StyleKey.h1, + ), handler: (editorState, context) => formatHeading(editorState, StyleKey.h1), ), ToolbarItem( id: 'appflowy.toolbar.h2', type: 1, tooltipsMessage: 'Heading 2', - icon: const FlowySvg(name: 'toolbar/h2'), + iconBuilder: (isHighlight) => FlowySvg( + name: 'toolbar/h2', + color: isHighlight ? Colors.lightBlue : null, + ), validator: _onlyShowInSingleTextSelection, + highlightCallback: (editorState) => _allSatisfy( + editorState, + StyleKey.heading, + (value) => value == StyleKey.h2, + ), handler: (editorState, context) => formatHeading(editorState, StyleKey.h2), ), ToolbarItem( id: 'appflowy.toolbar.h3', type: 1, tooltipsMessage: 'Heading 3', - icon: const FlowySvg(name: 'toolbar/h3'), + iconBuilder: (isHighlight) => FlowySvg( + name: 'toolbar/h3', + color: isHighlight ? Colors.lightBlue : null, + ), validator: _onlyShowInSingleTextSelection, + highlightCallback: (editorState) => _allSatisfy( + editorState, + StyleKey.heading, + (value) => value == StyleKey.h3, + ), handler: (editorState, context) => formatHeading(editorState, StyleKey.h3), ), ToolbarItem( id: 'appflowy.toolbar.bold', type: 2, tooltipsMessage: 'Bold', - icon: const FlowySvg(name: 'toolbar/bold'), + iconBuilder: (isHighlight) => FlowySvg( + name: 'toolbar/bold', + color: isHighlight ? Colors.lightBlue : null, + ), validator: _showInTextSelection, + highlightCallback: (editorState) => _allSatisfy( + editorState, + StyleKey.bold, + (value) => value == true, + ), handler: (editorState, context) => formatBold(editorState), ), ToolbarItem( id: 'appflowy.toolbar.italic', type: 2, tooltipsMessage: 'Italic', - icon: const FlowySvg(name: 'toolbar/italic'), + iconBuilder: (isHighlight) => FlowySvg( + name: 'toolbar/italic', + color: isHighlight ? Colors.lightBlue : null, + ), validator: _showInTextSelection, + highlightCallback: (editorState) => _allSatisfy( + editorState, + StyleKey.italic, + (value) => value == true, + ), handler: (editorState, context) => formatItalic(editorState), ), ToolbarItem( id: 'appflowy.toolbar.underline', type: 2, tooltipsMessage: 'Underline', - icon: const FlowySvg(name: 'toolbar/underline'), + iconBuilder: (isHighlight) => FlowySvg( + name: 'toolbar/underline', + color: isHighlight ? Colors.lightBlue : null, + ), validator: _showInTextSelection, + highlightCallback: (editorState) => _allSatisfy( + editorState, + StyleKey.underline, + (value) => value == true, + ), handler: (editorState, context) => formatUnderline(editorState), ), ToolbarItem( id: 'appflowy.toolbar.strikethrough', type: 2, tooltipsMessage: 'Strikethrough', - icon: const FlowySvg(name: 'toolbar/strikethrough'), + iconBuilder: (isHighlight) => FlowySvg( + name: 'toolbar/strikethrough', + color: isHighlight ? Colors.lightBlue : null, + ), validator: _showInTextSelection, + highlightCallback: (editorState) => _allSatisfy( + editorState, + StyleKey.strikethrough, + (value) => value == true, + ), handler: (editorState, context) => formatStrikethrough(editorState), ), ToolbarItem( id: 'appflowy.toolbar.quote', type: 3, tooltipsMessage: 'Quote', - icon: const FlowySvg(name: 'toolbar/quote'), + iconBuilder: (isHighlight) => FlowySvg( + name: 'toolbar/quote', + color: isHighlight ? Colors.lightBlue : null, + ), validator: _onlyShowInSingleTextSelection, + highlightCallback: (editorState) => _allSatisfy( + editorState, + StyleKey.subtype, + (value) => value == StyleKey.quote, + ), handler: (editorState, context) => formatQuote(editorState), ), ToolbarItem( id: 'appflowy.toolbar.bulleted_list', type: 3, tooltipsMessage: 'Bulleted list', - icon: const FlowySvg(name: 'toolbar/bulleted_list'), + iconBuilder: (isHighlight) => FlowySvg( + name: 'toolbar/bulleted_list', + color: isHighlight ? Colors.lightBlue : null, + ), validator: _onlyShowInSingleTextSelection, + highlightCallback: (editorState) => _allSatisfy( + editorState, + StyleKey.subtype, + (value) => value == StyleKey.bulletedList, + ), handler: (editorState, context) => formatBulletedList(editorState), ), ToolbarItem( id: 'appflowy.toolbar.link', type: 4, tooltipsMessage: 'Link', - icon: const FlowySvg(name: 'toolbar/link'), + iconBuilder: (isHighlight) => FlowySvg( + name: 'toolbar/link', + color: isHighlight ? Colors.lightBlue : null, + ), validator: _onlyShowInSingleTextSelection, + highlightCallback: (editorState) => _allSatisfy( + editorState, + StyleKey.href, + (value) => value != null, + ), handler: (editorState, context) => showLinkMenu(context, editorState), ), ToolbarItem( id: 'appflowy.toolbar.highlight', type: 4, tooltipsMessage: 'Highlight', - icon: const FlowySvg(name: 'toolbar/highlight'), + iconBuilder: (isHighlight) => FlowySvg( + name: 'toolbar/highlight', + color: isHighlight ? Colors.lightBlue : null, + ), validator: _showInTextSelection, + highlightCallback: (editorState) => _allSatisfy( + editorState, + StyleKey.backgroundColor, + (value) => value != null, + ), handler: (editorState, context) => formatHighlight(editorState), ), ]; -ToolbarShowValidator _onlyShowInSingleTextSelection = (editorState) { +ToolbarItemValidator _onlyShowInSingleTextSelection = (editorState) { final nodes = editorState.service.selectionService.currentSelectedNodes; return (nodes.length == 1 && nodes.first is TextNode); }; -ToolbarShowValidator _showInTextSelection = (editorState) { +ToolbarItemValidator _showInTextSelection = (editorState) { final nodes = editorState.service.selectionService.currentSelectedNodes .whereType(); return nodes.isNotEmpty; }; +bool _allSatisfy( + EditorState editorState, + String styleKey, + bool Function(dynamic value) test, +) { + final selection = editorState.service.selectionService.currentSelection.value; + return selection != null && + editorState.selectedTextNodes.allSatisfyInSelection( + selection, + styleKey, + test, + ); +} + OverlayEntry? _linkMenuOverlay; EditorState? _editorState; bool _changeSelectionInner = false; diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item_widget.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item_widget.dart index ce89eef126..4fefa8eadb 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item_widget.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item_widget.dart @@ -6,11 +6,13 @@ class ToolbarItemWidget extends StatelessWidget { const ToolbarItemWidget({ Key? key, required this.item, + required this.isHighlight, required this.onPressed, }) : super(key: key); final ToolbarItem item; final VoidCallback onPressed; + final bool isHighlight; @override Widget build(BuildContext context) { @@ -23,8 +25,9 @@ class ToolbarItemWidget extends StatelessWidget { child: MouseRegion( cursor: SystemMouseCursors.click, child: IconButton( + highlightColor: Colors.yellow, padding: EdgeInsets.zero, - icon: item.icon, + icon: item.iconBuilder(isHighlight), iconSize: 28, onPressed: onPressed, ), diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_widget.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_widget.dart index 395c6818bb..18c2cdc0b1 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_widget.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_widget.dart @@ -64,6 +64,7 @@ class _ToolbarWidgetState extends State with ToolbarMixin { (item) => Center( child: ToolbarItemWidget( item: item, + isHighlight: item.highlightCallback(widget.editorState), onPressed: () { item.handler(widget.editorState, context); }, diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/default_text_operations/format_rich_text_style.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/default_text_operations/format_rich_text_style.dart index c4f765f2f4..038f3119af 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/default_text_operations/format_rich_text_style.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/default_text_operations/format_rich_text_style.dart @@ -157,7 +157,7 @@ bool formatRichTextPartialStyle(EditorState editorState, String styleKey, } bool _allSatisfyInSelection( - EditorState editorState, String styleKey, dynamic value) { + EditorState editorState, String styleKey, dynamic matchValue) { final selection = editorState.service.selectionService.currentSelection.value; final nodes = editorState.service.selectionService.currentSelectedNodes; final textNodes = nodes.whereType().toList(growable: false); @@ -166,7 +166,9 @@ bool _allSatisfyInSelection( return false; } - return textNodes.allSatisfyInSelection(styleKey, selection, value); + return textNodes.allSatisfyInSelection(selection, styleKey, (value) { + return value == matchValue; + }); } bool formatRichTextStyle(EditorState editorState, Attributes attributes) { diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/toolbar_service.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/toolbar_service.dart index 8dba7dcb8e..5d79c44824 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/toolbar_service.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/toolbar_service.dart @@ -102,4 +102,9 @@ class _FlowyToolbarState extends State } return dividedItems; } + + // List _highlightItems( + // List items, + // Selection selection, + // ) {} } diff --git a/frontend/app_flowy/packages/appflowy_editor/test/render/toolbar/toolbar_item_widget_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/render/toolbar/toolbar_item_widget_test.dart index 87ae922d91..3d212691cb 100644 --- a/frontend/app_flowy/packages/appflowy_editor/test/render/toolbar/toolbar_item_widget_test.dart +++ b/frontend/app_flowy/packages/appflowy_editor/test/render/toolbar/toolbar_item_widget_test.dart @@ -11,17 +11,28 @@ void main() async { group('toolbar_item_widget.dart', () { testWidgets('test single toolbar item widget', (tester) async { final key = GlobalKey(); + final iconKey = GlobalKey(); var hit = false; final item = ToolbarItem( id: 'appflowy.toolbar.test', type: 1, - icon: const Icon(Icons.abc), + iconBuilder: (isHighlight) { + return Icon( + key: iconKey, + Icons.abc, + color: isHighlight ? Colors.lightBlue : null, + ); + }, validator: (editorState) => true, handler: (editorState, context) {}, + highlightCallback: (editorState) { + return true; + }, ); final widget = ToolbarItemWidget( key: key, item: item, + isHighlight: true, onPressed: (() { hit = true; }), @@ -36,6 +47,11 @@ void main() async { ); expect(find.byKey(key), findsOneWidget); + expect(find.byKey(iconKey), findsOneWidget); + expect( + (tester.firstWidget(find.byKey(iconKey)) as Icon).color, + Colors.lightBlue, + ); await tester.tap(find.byKey(key)); await tester.pumpAndSettle(); diff --git a/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/update_text_style_by_command_x_handler_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/update_text_style_by_command_x_handler_test.dart index e29308ebbc..d7afacb27a 100644 --- a/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/update_text_style_by_command_x_handler_test.dart +++ b/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/update_text_style_by_command_x_handler_test.dart @@ -2,7 +2,6 @@ import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/src/render/link_menu/link_menu.dart'; import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart'; import 'package:appflowy_editor/src/extensions/text_node_extensions.dart'; -import 'package:appflowy_editor/src/render/toolbar/toolbar_item.dart'; import 'package:appflowy_editor/src/render/toolbar/toolbar_widget.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -91,8 +90,8 @@ Future _testUpdateTextStyleByCommandX( var textNode = editor.nodeAtPath([1]) as TextNode; expect( textNode.allSatisfyInSelection( - matchStyle, selection, + matchStyle, (value) { return value == matchValue; }, @@ -110,8 +109,8 @@ Future _testUpdateTextStyleByCommandX( textNode = editor.nodeAtPath([1]) as TextNode; expect( textNode.allSatisfyInSelection( - matchStyle, selection, + matchStyle, (value) { return value == matchValue; }, @@ -144,12 +143,12 @@ Future _testUpdateTextStyleByCommandX( for (final node in nodes) { expect( node.allSatisfyInSelection( - matchStyle, Selection.single( path: node.path, startOffset: 0, endOffset: text.length, ), + matchStyle, (value) { return value == matchValue; }, @@ -196,11 +195,6 @@ Future _testLinkMenuInSingleTextSelection(WidgetTester tester) async { // show toolbar expect(find.byType(ToolbarWidget), findsOneWidget); - final item = defaultToolbarItems - .where((item) => item.id == 'appflowy.toolbar.link') - .first; - expect(find.byWidget(item.icon), findsOneWidget); - // trigger the link menu await editor.pressLogicKey(LogicalKeyboardKey.keyK, isMetaPressed: true); @@ -215,8 +209,8 @@ Future _testLinkMenuInSingleTextSelection(WidgetTester tester) async { final node = editor.nodeAtPath([1]) as TextNode; expect( node.allSatisfyInSelection( - StyleKey.href, selection, + StyleKey.href, (value) => value == link, ), true); @@ -244,8 +238,8 @@ Future _testLinkMenuInSingleTextSelection(WidgetTester tester) async { expect( node.allSatisfyInSelection( - StyleKey.href, selection, + StyleKey.href, (value) => value == link, ), false); diff --git a/frontend/app_flowy/packages/appflowy_editor/test/service/toolbar_service_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/service/toolbar_service_test.dart index 9d833095e7..c979f6121b 100644 --- a/frontend/app_flowy/packages/appflowy_editor/test/service/toolbar_service_test.dart +++ b/frontend/app_flowy/packages/appflowy_editor/test/service/toolbar_service_test.dart @@ -1,5 +1,6 @@ import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/src/render/toolbar/toolbar_item.dart'; +import 'package:appflowy_editor/src/render/toolbar/toolbar_item_widget.dart'; import 'package:appflowy_editor/src/render/toolbar/toolbar_widget.dart'; import 'package:flutter_test/flutter_test.dart'; import '../infra/test_editor.dart'; @@ -30,7 +31,16 @@ void main() async { final item = defaultToolbarItems .where((item) => item.id == 'appflowy.toolbar.link') .first; - expect(find.byWidget(item.icon), findsNothing); + final finder = find.byType(ToolbarItemWidget); + + expect( + tester + .widgetList(finder) + .toList(growable: false) + .where((element) => element.item.id == item.id) + .isEmpty, + true, + ); }); }); } From 940289c6e3701f141e926724f1c146560845fde1 Mon Sep 17 00:00:00 2001 From: appflowy Date: Tue, 30 Aug 2022 15:21:53 +0800 Subject: [PATCH 077/106] chore: add board no status column --- .../board/presentation/board_page.dart | 11 +- .../src/entities/group_entities/group.rs | 4 + .../src/services/cell/cell_operation.rs | 6 + .../src/services/group/configuration.rs | 24 +++- .../src/services/group/controller.rs | 123 ++++++++++++------ .../select_option_controller/util.rs | 25 ++-- .../flowy-grid/src/services/group/entities.rs | 2 + .../tests/grid/group_test/script.rs | 23 +++- .../flowy-grid/tests/grid/group_test/test.rs | 55 +++++++- 9 files changed, 212 insertions(+), 61 deletions(-) diff --git a/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart b/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart index f6bfd609c3..09b82c57e8 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart @@ -15,6 +15,7 @@ import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flowy_infra_ui/widget/error_page.dart'; import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/block_entities.pb.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid/group.pbserver.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../../grid/application/row/row_cache.dart'; @@ -154,7 +155,11 @@ class _BoardContentState extends State { } Widget _buildFooter(BuildContext context, AFBoardColumnData columnData) { - return AppFlowyColumnFooter( + final group = columnData.customData as GroupPB; + if (group.isDefault) { + return const SizedBox(); + } else { + return AppFlowyColumnFooter( icon: SizedBox( height: 20, width: 20, @@ -172,7 +177,9 @@ class _BoardContentState extends State { margin: config.footerPadding, onAddButtonClick: () { context.read().add(BoardEvent.createRow(columnData.id)); - }); + }, + ); + } } Widget _buildCard( diff --git a/frontend/rust-lib/flowy-grid/src/entities/group_entities/group.rs b/frontend/rust-lib/flowy-grid/src/entities/group_entities/group.rs index 9cc138bc0f..75b133344f 100644 --- a/frontend/rust-lib/flowy-grid/src/entities/group_entities/group.rs +++ b/frontend/rust-lib/flowy-grid/src/entities/group_entities/group.rs @@ -81,6 +81,9 @@ pub struct GroupPB { #[pb(index = 4)] pub rows: Vec, + + #[pb(index = 5)] + pub is_default: bool, } impl std::convert::From for GroupPB { @@ -90,6 +93,7 @@ impl std::convert::From for GroupPB { group_id: group.id, desc: group.name, rows: group.rows, + is_default: group.is_default, } } } diff --git a/frontend/rust-lib/flowy-grid/src/services/cell/cell_operation.rs b/frontend/rust-lib/flowy-grid/src/services/cell/cell_operation.rs index 294aff9885..ecff3b951b 100644 --- a/frontend/rust-lib/flowy-grid/src/services/cell/cell_operation.rs +++ b/frontend/rust-lib/flowy-grid/src/services/cell/cell_operation.rs @@ -176,6 +176,12 @@ pub fn insert_select_option_cell(option_id: String, field_rev: &FieldRevision) - CellRevision::new(data) } +pub fn delete_select_option_cell(option_id: String, field_rev: &FieldRevision) -> CellRevision { + let cell_data = SelectOptionCellChangeset::from_delete(&option_id).to_str(); + let data = apply_cell_data_changeset(cell_data, None, field_rev).unwrap(); + CellRevision::new(data) +} + /// If the cell data is not String type, it should impl this trait. /// Deserialize the String into cell specific data type. pub trait FromCellString { diff --git a/frontend/rust-lib/flowy-grid/src/services/group/configuration.rs b/frontend/rust-lib/flowy-grid/src/services/group/configuration.rs index 546afb8a32..29f8678729 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/configuration.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/configuration.rs @@ -31,6 +31,10 @@ impl std::fmt::Display for GenericGroupConfiguration { self.groups_map.iter().for_each(|(_, group)| { let _ = f.write_fmt(format_args!("Group:{} has {} rows \n", group.id, group.rows.len())); }); + let _ = f.write_fmt(format_args!( + "Default group has {} rows \n", + self.default_group.rows.len() + )); Ok(()) } } @@ -41,6 +45,8 @@ pub struct GenericGroupConfiguration { configuration_content: PhantomData, field_rev: Arc, groups_map: IndexMap, + /// default_group is used to store the rows that don't belong to any groups. + default_group: Group, writer: Arc, } @@ -55,6 +61,15 @@ where reader: Arc, writer: Arc, ) -> FlowyResult { + let default_group_id = format!("{}_default_group", view_id); + let default_group = Group { + id: default_group_id, + field_id: field_rev.id.clone(), + name: format!("No {}", field_rev.name), + is_default: true, + rows: vec![], + content: "".to_string(), + }; let configuration = match reader.get_group_configuration(field_rev.clone()).await { None => { let default_group_configuration = default_group_configuration(&field_rev); @@ -71,6 +86,7 @@ where view_id, field_rev, groups_map: IndexMap::new(), + default_group, writer, configuration, configuration_content: PhantomData, @@ -82,7 +98,9 @@ where } pub(crate) fn clone_groups(&self) -> Vec { - self.groups_map.values().cloned().collect() + let mut groups: Vec = self.groups_map.values().cloned().collect(); + groups.push(self.default_group.clone()); + groups } pub(crate) fn merge_groups(&mut self, groups: Vec) -> FlowyResult> { @@ -160,6 +178,10 @@ where self.groups_map.get_mut(group_id) } + pub(crate) fn get_mut_default_group(&mut self) -> &mut Group { + &mut self.default_group + } + pub(crate) fn move_group(&mut self, from_id: &str, to_id: &str) -> FlowyResult<()> { let from_index = self.groups_map.get_index_of(from_id); let to_index = self.groups_map.get_index_of(to_id); diff --git a/frontend/rust-lib/flowy-grid/src/services/group/controller.rs b/frontend/rust-lib/flowy-grid/src/services/group/controller.rs index fac034f6f5..fc45a6fc12 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/controller.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/controller.rs @@ -1,4 +1,4 @@ -use crate::entities::{GroupChangesetPB, GroupViewChangesetPB, RowPB}; +use crate::entities::{GroupChangesetPB, GroupViewChangesetPB, InsertedRowPB, RowPB}; use crate::services::cell::{decode_any_cell_data, CellBytesParser}; use crate::services::group::action::GroupAction; use crate::services::group::configuration::GenericGroupConfiguration; @@ -11,8 +11,6 @@ use flowy_grid_data_model::revision::{ use std::marker::PhantomData; use std::sync::Arc; -const DEFAULT_GROUP_ID: &str = "default_group"; - // Each kind of group must implement this trait to provide custom group // operations. For example, insert cell data to the row_rev when creating // a new row. @@ -72,8 +70,6 @@ pub struct GenericGroupController { pub field_id: String, pub type_option: Option, pub configuration: GenericGroupConfiguration, - /// default_group is used to store the rows that don't belong to any groups. - default_group: Group, group_action_phantom: PhantomData, cell_parser_phantom: PhantomData

, } @@ -92,22 +88,85 @@ where let type_option = field_rev.get_type_option_entry::(field_type_rev); let groups = G::generate_groups(&field_rev.id, &configuration, &type_option); let _ = configuration.merge_groups(groups)?; - let default_group = Group::new( - DEFAULT_GROUP_ID.to_owned(), - field_rev.id.clone(), - format!("No {}", field_rev.name), - "".to_string(), - ); Ok(Self { field_id: field_rev.id.clone(), - default_group, type_option, configuration, group_action_phantom: PhantomData, cell_parser_phantom: PhantomData, }) } + + fn update_default_group( + &mut self, + row_rev: &RowRevision, + other_group_changesets: &Vec, + ) -> GroupChangesetPB { + let default_group = self.configuration.get_mut_default_group(); + + // [other_group_inserted_row] contains all the inserted rows except the default group. + let other_group_inserted_row = other_group_changesets + .iter() + .flat_map(|changeset| &changeset.inserted_rows) + .collect::>(); + + // Calculate the inserted_rows of the default_group + let default_group_inserted_row = other_group_changesets + .iter() + .flat_map(|changeset| &changeset.deleted_rows) + .cloned() + .filter(|row_id| { + // if the [other_group_inserted_row] contains the row_id of the row + // which means the row should not move to the default group. + other_group_inserted_row + .iter() + .find(|inserted_row| &inserted_row.row.id == row_id) + .is_none() + }) + .collect::>(); + + let mut changeset = GroupChangesetPB::new(default_group.id.clone()); + if default_group_inserted_row.is_empty() == false { + changeset.inserted_rows.push(InsertedRowPB::new(row_rev.into())); + default_group.add_row(row_rev.into()); + } + + // [other_group_delete_rows] contains all the deleted rows except the default group. + let other_group_delete_rows: Vec = other_group_changesets + .iter() + .flat_map(|changeset| &changeset.deleted_rows) + .cloned() + .collect(); + + let default_group_deleted_rows = other_group_changesets + .iter() + .flat_map(|changeset| &changeset.inserted_rows) + .filter(|inserted_row| { + // if the [other_group_delete_rows] contain the inserted_row, which means this row should move + // out from the default_group. + let inserted_row_id = &inserted_row.row.id; + other_group_delete_rows + .iter() + .find(|row_id| inserted_row_id == row_id.clone()) + .is_none() + }) + .collect::>(); + + let mut deleted_row_ids = vec![]; + for row in &default_group.rows { + if default_group_deleted_rows + .iter() + .find(|deleted_row| deleted_row.row.id == row.id) + .is_some() + { + deleted_row_ids.push(row.id.clone()); + } + } + default_group.rows.retain(|row| !deleted_row_ids.contains(&row.id)); + changeset.deleted_rows.extend(deleted_row_ids); + changeset + } } impl GroupControllerSharedOperation for GenericGroupController @@ -124,11 +183,7 @@ where } fn groups(&self) -> Vec { - let mut groups = self.configuration.clone_groups(); - if self.default_group.is_empty() == false { - groups.insert(0, self.default_group.clone()); - } - groups + self.configuration.clone_groups() } fn get_group(&self, group_id: &str) -> Option<(usize, Group)> { @@ -138,7 +193,6 @@ where #[tracing::instrument(level = "trace", skip_all, fields(row_count=%row_revs.len(), group_result))] fn fill_groups(&mut self, row_revs: &[Arc], field_rev: &FieldRevision) -> FlowyResult> { - // let mut ungrouped_rows = vec![]; for row_rev in row_revs { if let Some(cell_rev) = row_rev.cells.get(&self.field_id) { let mut grouped_rows: Vec = vec![]; @@ -154,8 +208,7 @@ where } if grouped_rows.is_empty() { - // ungrouped_rows.push(RowPB::from(row_rev)); - self.default_group.add_row(row_rev.into()); + self.configuration.get_mut_default_group().add_row(row_rev.into()); } else { for group_row in grouped_rows { if let Some(group) = self.configuration.get_mut_group(&group_row.group_id) { @@ -164,30 +217,11 @@ where } } } else { - self.default_group.add_row(row_rev.into()); + self.configuration.get_mut_default_group().add_row(row_rev.into()); } } - // if !ungrouped_rows.is_empty() { - // let default_group_rev = GroupRevision::default_group(gen_grid_group_id(), format!("No {}", field_rev.name)); - // let default_group = Group::new( - // default_group_rev.id.clone(), - // field_rev.id.clone(), - // default_group_rev.name.clone(), - // "".to_owned(), - // ); - // } - - tracing::Span::current().record( - "group_result", - &format!( - "{}, default_group has {} rows", - self.configuration, - self.default_group.rows.len() - ) - .as_str(), - ); - + tracing::Span::current().record("group_result", &format!("{},", self.configuration,).as_str()); Ok(self.groups()) } @@ -203,7 +237,12 @@ where if let Some(cell_rev) = row_rev.cells.get(&self.field_id) { let cell_bytes = decode_any_cell_data(cell_rev.data.clone(), field_rev); let cell_data = cell_bytes.parser::

()?; - let changesets = self.add_row_if_match(row_rev, &cell_data); + let mut changesets = self.add_row_if_match(row_rev, &cell_data); + let default_group_changeset = self.update_default_group(row_rev, &changesets); + tracing::info!("default_group_changeset: {}", default_group_changeset); + if !default_group_changeset.is_empty() { + changesets.push(default_group_changeset); + } Ok(changesets) } else { Ok(vec![]) diff --git a/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/util.rs b/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/util.rs index 349f7391a6..39f581d97d 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/util.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/util.rs @@ -15,18 +15,25 @@ pub fn add_row( row_rev: &RowRevision, ) -> Option { let mut changeset = GroupChangesetPB::new(group.id.clone()); - cell_data.select_options.iter().for_each(|option| { - if option.id == group.id { - if !group.contains_row(&row_rev.id) { - let row_pb = RowPB::from(row_rev); - changeset.inserted_rows.push(InsertedRowPB::new(row_pb.clone())); - group.add_row(row_pb); - } - } else if group.contains_row(&row_rev.id) { + if cell_data.select_options.is_empty() { + if group.contains_row(&row_rev.id) { changeset.deleted_rows.push(row_rev.id.clone()); group.remove_row(&row_rev.id); } - }); + } else { + cell_data.select_options.iter().for_each(|option| { + if option.id == group.id { + if !group.contains_row(&row_rev.id) { + let row_pb = RowPB::from(row_rev); + changeset.inserted_rows.push(InsertedRowPB::new(row_pb.clone())); + group.add_row(row_pb); + } + } else if group.contains_row(&row_rev.id) { + changeset.deleted_rows.push(row_rev.id.clone()); + group.remove_row(&row_rev.id); + } + }); + } if changeset.is_empty() { None diff --git a/frontend/rust-lib/flowy-grid/src/services/group/entities.rs b/frontend/rust-lib/flowy-grid/src/services/group/entities.rs index f4d0ee1652..9afe7f54e9 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/entities.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/entities.rs @@ -5,6 +5,7 @@ pub struct Group { pub id: String, pub field_id: String, pub name: String, + pub is_default: bool, pub(crate) rows: Vec, /// [content] is used to determine which group the cell belongs to. @@ -16,6 +17,7 @@ impl Group { Self { id, field_id, + is_default: false, name, rows: vec![], content, diff --git a/frontend/rust-lib/flowy-grid/tests/grid/group_test/script.rs b/frontend/rust-lib/flowy-grid/tests/grid/group_test/script.rs index 3a5ae9455b..6afeda6d4b 100644 --- a/frontend/rust-lib/flowy-grid/tests/grid/group_test/script.rs +++ b/frontend/rust-lib/flowy-grid/tests/grid/group_test/script.rs @@ -2,7 +2,7 @@ use crate::grid::grid_editor::GridEditorTest; use flowy_grid::entities::{ CreateRowParams, FieldChangesetParams, FieldType, GridLayout, GroupPB, MoveGroupParams, MoveGroupRowParams, RowPB, }; -use flowy_grid::services::cell::insert_select_option_cell; +use flowy_grid::services::cell::{delete_select_option_cell, insert_select_option_cell}; use flowy_grid_data_model::revision::RowChangeset; use std::time::Duration; use tokio::time::interval; @@ -128,11 +128,22 @@ impl GridGroupTest { let field_id = from_group.field_id; let field_rev = self.editor.get_field_rev(&field_id).await.unwrap(); let field_type: FieldType = field_rev.ty.into(); - let cell_rev = match field_type { - FieldType::SingleSelect => insert_select_option_cell(to_group.group_id.clone(), &field_rev), - FieldType::MultiSelect => insert_select_option_cell(to_group.group_id.clone(), &field_rev), - _ => { - panic!("Unsupported group field type"); + + let cell_rev = if to_group.is_default { + match field_type { + FieldType::SingleSelect => delete_select_option_cell(to_group.group_id.clone(), &field_rev), + FieldType::MultiSelect => delete_select_option_cell(to_group.group_id.clone(), &field_rev), + _ => { + panic!("Unsupported group field type"); + } + } + } else { + match field_type { + FieldType::SingleSelect => insert_select_option_cell(to_group.group_id.clone(), &field_rev), + FieldType::MultiSelect => insert_select_option_cell(to_group.group_id.clone(), &field_rev), + _ => { + panic!("Unsupported group field type"); + } } }; diff --git a/frontend/rust-lib/flowy-grid/tests/grid/group_test/test.rs b/frontend/rust-lib/flowy-grid/tests/grid/group_test/test.rs index 4a4d45f952..a52a088f51 100644 --- a/frontend/rust-lib/flowy-grid/tests/grid/group_test/test.rs +++ b/frontend/rust-lib/flowy-grid/tests/grid/group_test/test.rs @@ -6,7 +6,7 @@ use flowy_grid::entities::FieldChangesetParams; async fn group_init_test() { let mut test = GridGroupTest::new().await; let scripts = vec![ - AssertGroupCount(3), + AssertGroupCount(4), AssertGroupRowCount { group_index: 0, row_count: 2, @@ -19,6 +19,10 @@ async fn group_init_test() { group_index: 2, row_count: 1, }, + AssertGroupRowCount { + group_index: 3, + row_count: 0, + }, ]; test.run_scripts(scripts).await; } @@ -294,6 +298,55 @@ async fn group_reorder_group_test() { test.run_scripts(scripts).await; } +#[tokio::test] +async fn group_move_to_default_group_test() { + let mut test = GridGroupTest::new().await; + let scripts = vec![ + UpdateRow { + from_group_index: 0, + row_index: 0, + to_group_index: 3, + }, + AssertGroupRowCount { + group_index: 0, + row_count: 1, + }, + AssertGroupRowCount { + group_index: 3, + row_count: 1, + }, + ]; + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn group_move_from_default_group_test() { + let mut test = GridGroupTest::new().await; + let scripts = vec![UpdateRow { + from_group_index: 0, + row_index: 0, + to_group_index: 3, + }]; + test.run_scripts(scripts).await; + + let scripts = vec![ + UpdateRow { + from_group_index: 3, + row_index: 0, + to_group_index: 0, + }, + AssertGroupRowCount { + group_index: 0, + row_count: 2, + }, + AssertGroupRowCount { + group_index: 3, + row_count: 0, + }, + ]; + test.run_scripts(scripts).await; +} + #[tokio::test] async fn group_move_group_test() { let mut test = GridGroupTest::new().await; From c6d3fd4b68b4de823dd64f5b62c0f1ea31948f3a Mon Sep 17 00:00:00 2001 From: appflowy Date: Tue, 30 Aug 2022 15:26:15 +0800 Subject: [PATCH 078/106] chore: support localize --- frontend/app_flowy/assets/translations/en.json | 7 ++++++- .../lib/plugins/board/presentation/board_page.dart | 4 +++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/frontend/app_flowy/assets/translations/en.json b/frontend/app_flowy/assets/translations/en.json index eca01d1bed..1fd48702ac 100644 --- a/frontend/app_flowy/assets/translations/en.json +++ b/frontend/app_flowy/assets/translations/en.json @@ -221,5 +221,10 @@ "timeHintTextInTwelveHour": "12:00 AM", "timeHintTextInTwentyFourHour": "12:00" } + }, + "board": { + "column": { + "create_new_card": "New" + } } -} +} \ No newline at end of file diff --git a/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart b/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart index 09b82c57e8..608d4582be 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart @@ -2,6 +2,7 @@ import 'dart:collection'; +import 'package:app_flowy/generated/locale_keys.g.dart'; import 'package:app_flowy/plugins/board/application/card/card_data_controller.dart'; import 'package:app_flowy/plugins/grid/application/row/row_cache.dart'; import 'package:app_flowy/plugins/grid/application/field/field_cache.dart'; @@ -9,6 +10,7 @@ import 'package:app_flowy/plugins/grid/application/row/row_data_controller.dart' import 'package:app_flowy/plugins/grid/presentation/widgets/cell/cell_builder.dart'; import 'package:app_flowy/plugins/grid/presentation/widgets/row/row_detail.dart'; import 'package:appflowy_board/appflowy_board.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/image.dart'; import 'package:flowy_infra/theme.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; @@ -169,7 +171,7 @@ class _BoardContentState extends State { ), ), title: FlowyText.medium( - "New", + LocaleKeys.board_column_create_new_card.tr(), fontSize: 14, color: context.read().textColor, ), From 0f334962ceea9a77d2b5d06909a977fc548c44ca Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Tue, 30 Aug 2022 16:25:32 +0800 Subject: [PATCH 079/106] test: add more test cases to toolbar_service --- .../test/service/toolbar_service_test.dart | 121 ++++++++++++++++++ 1 file changed, 121 insertions(+) diff --git a/frontend/app_flowy/packages/appflowy_editor/test/service/toolbar_service_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/service/toolbar_service_test.dart index c979f6121b..418a333189 100644 --- a/frontend/app_flowy/packages/appflowy_editor/test/service/toolbar_service_test.dart +++ b/frontend/app_flowy/packages/appflowy_editor/test/service/toolbar_service_test.dart @@ -1,4 +1,5 @@ import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart'; import 'package:appflowy_editor/src/render/toolbar/toolbar_item.dart'; import 'package:appflowy_editor/src/render/toolbar/toolbar_item_widget.dart'; import 'package:appflowy_editor/src/render/toolbar/toolbar_widget.dart'; @@ -42,5 +43,125 @@ void main() async { true, ); }); + + testWidgets( + 'Test toolbar service in single text selection with StyleKey.partialStyleKeys', + (tester) async { + final attributes = StyleKey.partialStyleKeys.fold({}, + (previousValue, element) { + if (element == StyleKey.backgroundColor) { + previousValue[element] = '0x6000BCF0'; + } else if (element == StyleKey.href) { + previousValue[element] = 'appflowy.io'; + } else { + previousValue[element] = true; + } + return previousValue; + }); + + const text = 'Welcome to Appflowy 😁'; + final editor = tester.editor + ..insertTextNode(text) + ..insertTextNode( + null, + delta: Delta([ + TextInsert(text), + TextInsert(text, attributes), + TextInsert(text), + ]), + ); + await editor.startTesting(); + await editor.updateSelection( + Selection.single(path: [0], startOffset: 0, endOffset: text.length), + ); + expect(find.byType(ToolbarWidget), findsOneWidget); + + void testHighlight(bool expectedValue) { + for (final styleKey in StyleKey.partialStyleKeys) { + var key = styleKey; + if (styleKey == StyleKey.backgroundColor) { + key = 'highlight'; + } else if (styleKey == StyleKey.href) { + key = 'link'; + } + final itemWidget = _itemWidgetForId(tester, 'appflowy.toolbar.$key'); + expect(itemWidget.isHighlight, expectedValue); + } + } + + await editor.updateSelection( + Selection.single(path: [1], startOffset: 0, endOffset: text.length * 2), + ); + testHighlight(false); + + await editor.updateSelection( + Selection.single( + path: [1], + startOffset: text.length, + endOffset: text.length * 2, + ), + ); + testHighlight(true); + + await editor.updateSelection( + Selection.single( + path: [1], + startOffset: text.length + 2, + endOffset: text.length * 2 - 2, + ), + ); + testHighlight(true); + }); + + testWidgets( + 'Test toolbar service in single text selection with StyleKey.globalStyleKeys', + (tester) async { + const text = 'Welcome to Appflowy 😁'; + + final editor = tester.editor + ..insertTextNode(text, attributes: { + StyleKey.subtype: StyleKey.heading, + StyleKey.heading: StyleKey.h1, + }) + ..insertTextNode( + text, + attributes: {StyleKey.subtype: StyleKey.quote}, + ) + ..insertTextNode( + text, + attributes: {StyleKey.subtype: StyleKey.bulletedList}, + ); + await editor.startTesting(); + + await editor.updateSelection( + Selection.single(path: [0], startOffset: 0, endOffset: text.length), + ); + expect(find.byType(ToolbarWidget), findsOneWidget); + var itemWidget = _itemWidgetForId(tester, 'appflowy.toolbar.h1'); + expect(itemWidget.isHighlight, true); + + await editor.updateSelection( + Selection.single(path: [1], startOffset: 0, endOffset: text.length), + ); + expect(find.byType(ToolbarWidget), findsOneWidget); + itemWidget = _itemWidgetForId(tester, 'appflowy.toolbar.quote'); + expect(itemWidget.isHighlight, true); + + await editor.updateSelection( + Selection.single(path: [2], startOffset: 0, endOffset: text.length), + ); + expect(find.byType(ToolbarWidget), findsOneWidget); + itemWidget = _itemWidgetForId(tester, 'appflowy.toolbar.bulleted_list'); + expect(itemWidget.isHighlight, true); + }); }); } + +ToolbarItemWidget _itemWidgetForId(WidgetTester tester, String id) { + final finder = find.byType(ToolbarItemWidget); + final itemWidgets = tester + .widgetList(finder) + .where((element) => element.item.id == id); + expect(itemWidgets.length, 1); + return itemWidgets.first; +} From 6e71d6d32514919a0356e97d344f0ac2dbf3a03e Mon Sep 17 00:00:00 2001 From: appflowy Date: Tue, 30 Aug 2022 16:45:48 +0800 Subject: [PATCH 080/106] chore: fix warnings --- .../board/presentation/board_page.dart | 2 +- .../presentation/card/card_container.dart | 18 ++++++++++-------- .../src/services/grid_view_manager.rs | 2 +- .../src/services/group/configuration.rs | 9 ++++----- .../src/services/group/controller.rs | 19 ++++++++----------- 5 files changed, 24 insertions(+), 26 deletions(-) diff --git a/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart b/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart index 608d4582be..e850186f89 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart @@ -112,7 +112,7 @@ class _BoardContentState extends State { column, columnItem, ), - columnConstraints: const BoxConstraints.tightFor(width: 240), + columnConstraints: const BoxConstraints.tightFor(width: 300), config: AFBoardConfig( columnBackgroundColor: HexColor.fromHex('#F7F8FC'), ), diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/card_container.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/card_container.dart index 0e0a7287ae..3ca934e508 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/card_container.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/card_container.dart @@ -93,14 +93,15 @@ BoxDecoration _makeBoxDecoration(BuildContext context) { final theme = context.read(); final borderSide = BorderSide(color: theme.shader6, width: 1.0); return BoxDecoration( - color: theme.surface, + color: Colors.transparent, border: Border.fromBorderSide(borderSide), - boxShadow: [ + boxShadow: const [ BoxShadow( - color: theme.shader6, - spreadRadius: 0, - blurRadius: 2, - offset: Offset.zero) + color: Colors.transparent, + spreadRadius: 0, + blurRadius: 2, + offset: Offset.zero, + ) ], borderRadius: const BorderRadius.all(Radius.circular(6)), ); @@ -120,8 +121,9 @@ class _CardEnterRegion extends StatelessWidget { builder: (context, onEnter, _) { List children = [child]; if (onEnter) { - children.add(CardAccessoryContainer(accessories: accessories) - .positioned(right: 0)); + children.add(CardAccessoryContainer( + accessories: accessories, + ).positioned(right: 0)); } return MouseRegion( diff --git a/frontend/rust-lib/flowy-grid/src/services/grid_view_manager.rs b/frontend/rust-lib/flowy-grid/src/services/grid_view_manager.rs index 8c9893e4ef..70e4cc29b1 100644 --- a/frontend/rust-lib/flowy-grid/src/services/grid_view_manager.rs +++ b/frontend/rust-lib/flowy-grid/src/services/grid_view_manager.rs @@ -142,7 +142,7 @@ impl GridViewManager { .move_group_row(&row_rev, &mut row_changeset, &to_group_id, to_row_id.clone()) .await; - if row_changeset.is_empty() == false { + if !row_changeset.is_empty() { with_row_changeset(row_changeset).await; } diff --git a/frontend/rust-lib/flowy-grid/src/services/group/configuration.rs b/frontend/rust-lib/flowy-grid/src/services/group/configuration.rs index 29f8678729..fea71ebbbc 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/configuration.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/configuration.rs @@ -285,7 +285,7 @@ fn merge_groups(old_groups: &[GroupRevision], groups: Vec) -> MergeGroupR } // Find out the new groups - let new_groups = group_map.into_values().collect::>(); + let new_groups = group_map.into_values(); for (index, group) in new_groups.into_iter().enumerate() { merge_result.add_insert_group(index, group); } @@ -313,7 +313,7 @@ impl MergeGroupResult { } fn add_group(&mut self, group: Group) { - self.groups.push(group.clone()); + self.groups.push(group); } fn add_insert_group(&mut self, index: usize, group: Group) { @@ -331,11 +331,10 @@ fn make_group_view_changeset( inserted_groups: Vec, updated_group: Vec, ) -> GroupViewChangesetPB { - let changeset = GroupViewChangesetPB { + GroupViewChangesetPB { view_id, inserted_groups, deleted_groups: vec![], update_groups: updated_group.into_iter().map(GroupPB::from).collect(), - }; - changeset + } } diff --git a/frontend/rust-lib/flowy-grid/src/services/group/controller.rs b/frontend/rust-lib/flowy-grid/src/services/group/controller.rs index fc45a6fc12..562f21eb17 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/controller.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/controller.rs @@ -98,10 +98,12 @@ where }) } + // https://stackoverflow.com/questions/69413164/how-to-fix-this-clippy-warning-needless-collect + #[allow(clippy::needless_collect)] fn update_default_group( &mut self, row_rev: &RowRevision, - other_group_changesets: &Vec, + other_group_changesets: &[GroupChangesetPB], ) -> GroupChangesetPB { let default_group = self.configuration.get_mut_default_group(); @@ -119,15 +121,14 @@ where .filter(|row_id| { // if the [other_group_inserted_row] contains the row_id of the row // which means the row should not move to the default group. - other_group_inserted_row + !other_group_inserted_row .iter() - .find(|inserted_row| &inserted_row.row.id == row_id) - .is_none() + .any(|inserted_row| &inserted_row.row.id == row_id) }) .collect::>(); let mut changeset = GroupChangesetPB::new(default_group.id.clone()); - if default_group_inserted_row.is_empty() == false { + if !default_group_inserted_row.is_empty() { changeset.inserted_rows.push(InsertedRowPB::new(row_rev.into())); default_group.add_row(row_rev.into()); } @@ -146,10 +147,7 @@ where // if the [other_group_delete_rows] contain the inserted_row, which means this row should move // out from the default_group. let inserted_row_id = &inserted_row.row.id; - other_group_delete_rows - .iter() - .find(|row_id| inserted_row_id == row_id.clone()) - .is_none() + !other_group_delete_rows.iter().any(|row_id| inserted_row_id == row_id) }) .collect::>(); @@ -157,8 +155,7 @@ where for row in &default_group.rows { if default_group_deleted_rows .iter() - .find(|deleted_row| deleted_row.row.id == row.id) - .is_some() + .any(|deleted_row| deleted_row.row.id == row.id) { deleted_row_ids.push(row.id.clone()); } From 8afa48ca1620edae27683df6b2a5f8d319f2d16d Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Tue, 30 Aug 2022 17:22:33 +0800 Subject: [PATCH 081/106] fix: # doesn't work #937 --- .../src/render/rich_text/heading_text.dart | 1 - .../src/render/rich_text/rich_text_style.dart | 5 ++- .../backspace_handler.dart | 4 ++- .../backspace_handler_test.dart | 32 +++++++++++++++++++ 4 files changed, 39 insertions(+), 3 deletions(-) diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/heading_text.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/heading_text.dart index fff25dd2d5..93defaae8e 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/heading_text.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/heading_text.dart @@ -38,7 +38,6 @@ class HeadingTextNodeWidget extends StatefulWidget { } // customize - class _HeadingTextNodeWidgetState extends State with Selectable, DefaultSelectable { @override diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/rich_text_style.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/rich_text_style.dart index 93d088d66f..2cd03fe389 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/rich_text_style.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/rich_text_style.dart @@ -79,7 +79,10 @@ Map headingToFontSize = { extension NodeAttributesExtensions on Attributes { String? get heading { - if (containsKey(StyleKey.heading) && this[StyleKey.heading] is String) { + if (containsKey(StyleKey.subtype) && + containsKey(StyleKey.heading) && + this[StyleKey.subtype] == StyleKey.heading && + this[StyleKey.heading] is String) { return this[StyleKey.heading]; } return null; diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/backspace_handler.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/backspace_handler.dart index 0eeaf654de..675ee5b446 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/backspace_handler.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/backspace_handler.dart @@ -1,3 +1,4 @@ +import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -29,7 +30,8 @@ KeyEventResult _handleBackspace(EditorState editorState, RawKeyEvent event) { if (textNode.subtype != null) { transactionBuilder ..updateNode(textNode, { - 'subtype': null, + StyleKey.subtype: null, + textNode.subtype!: null, }) ..afterSelection = Selection.collapsed( Position( diff --git a/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/backspace_handler_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/backspace_handler_test.dart index 1976ec3250..da5d22a786 100644 --- a/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/backspace_handler_test.dart +++ b/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/backspace_handler_test.dart @@ -234,6 +234,38 @@ void main() async { (tester) async { await _deleteLastImage(tester, false); }); + + testWidgets('Removes the style of heading text and revert', (tester) async { + const text = 'Welcome to Appflowy 😁'; + final editor = tester.editor..insertTextNode(text); + await editor.startTesting(); + + await editor.updateSelection( + Selection.single(path: [0], startOffset: 0), + ); + + final textNode = editor.nodeAtPath([0]) as TextNode; + + await editor.insertText(textNode, '#', 0); + await editor.pressLogicKey(LogicalKeyboardKey.space); + expect( + (editor.nodeAtPath([0]) as TextNode).attributes.heading, + StyleKey.h1, + ); + + await editor.pressLogicKey(LogicalKeyboardKey.backspace); + expect( + textNode.attributes.heading, + null, + ); + + await editor.insertText(textNode, '#', 0); + await editor.pressLogicKey(LogicalKeyboardKey.space); + expect( + (editor.nodeAtPath([0]) as TextNode).attributes.heading, + StyleKey.h1, + ); + }); } Future _deleteFirstImage(WidgetTester tester, bool isBackward) async { From 071ff06d47bc2b3ce18cb2d6d94e97a0c6c7a167 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Tue, 30 Aug 2022 17:42:28 +0800 Subject: [PATCH 082/106] feat: / supports inserting Quote #936 --- .../packages/appflowy_editor/assets/images/quote.svg | 2 +- .../assets/images/selection_menu/quote.svg | 4 ++++ .../render/selection_menu/selection_menu_service.dart | 8 ++++++++ .../selection_menu/selection_menu_widget_test.dart | 10 +++++----- 4 files changed, 18 insertions(+), 6 deletions(-) create mode 100644 frontend/app_flowy/packages/appflowy_editor/assets/images/selection_menu/quote.svg diff --git a/frontend/app_flowy/packages/appflowy_editor/assets/images/quote.svg b/frontend/app_flowy/packages/appflowy_editor/assets/images/quote.svg index 0f3d33f6d3..1393e71556 100644 --- a/frontend/app_flowy/packages/appflowy_editor/assets/images/quote.svg +++ b/frontend/app_flowy/packages/appflowy_editor/assets/images/quote.svg @@ -1,3 +1,3 @@ - + \ No newline at end of file diff --git a/frontend/app_flowy/packages/appflowy_editor/assets/images/selection_menu/quote.svg b/frontend/app_flowy/packages/appflowy_editor/assets/images/selection_menu/quote.svg new file mode 100644 index 0000000000..5c1cbb4a50 --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/assets/images/selection_menu/quote.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_service.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_service.dart index 7f4f803610..f4f2006af4 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_service.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_service.dart @@ -169,6 +169,14 @@ final List _defaultSelectionMenuItems = [ insertCheckboxAfterSelection(editorState); }, ), + SelectionMenuItem( + name: 'Quote', + icon: _selectionMenuIcon('quote'), + keywords: ['quote', 'refer'], + handler: (editorState, _, __) { + insertQuoteAfterSelection(editorState); + }, + ), ]; Widget _selectionMenuIcon(String name) { diff --git a/frontend/app_flowy/packages/appflowy_editor/test/render/selection_menu/selection_menu_widget_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/render/selection_menu/selection_menu_widget_test.dart index 2711921352..6006fe6a7a 100644 --- a/frontend/app_flowy/packages/appflowy_editor/test/render/selection_menu/selection_menu_widget_test.dart +++ b/frontend/app_flowy/packages/appflowy_editor/test/render/selection_menu/selection_menu_widget_test.dart @@ -38,17 +38,17 @@ void main() async { await editor.pressLogicKey(LogicalKeyboardKey.keyE); expect( find.byType(SelectionMenuItemWidget, skipOffstage: false), - findsNWidgets(2), + findsNWidgets(3), ); await editor.pressLogicKey(LogicalKeyboardKey.backspace); expect( find.byType(SelectionMenuItemWidget, skipOffstage: false), - findsNWidgets(3), + findsNWidgets(4), ); await editor.pressLogicKey(LogicalKeyboardKey.keyE); expect( find.byType(SelectionMenuItemWidget, skipOffstage: false), - findsNWidgets(2), + findsNWidgets(3), ); await editor.pressLogicKey(LogicalKeyboardKey.keyX); expect( @@ -73,7 +73,7 @@ void main() async { await editor.pressLogicKey(LogicalKeyboardKey.keyE); expect( find.byType(SelectionMenuItemWidget, skipOffstage: false), - findsNWidgets(2), + findsNWidgets(3), ); await editor.pressLogicKey(LogicalKeyboardKey.escape); expect( @@ -89,7 +89,7 @@ void main() async { await editor.pressLogicKey(LogicalKeyboardKey.keyE); expect( find.byType(SelectionMenuItemWidget, skipOffstage: false), - findsNWidgets(2), + findsNWidgets(3), ); await editor.pressLogicKey(LogicalKeyboardKey.backspace); await editor.pressLogicKey(LogicalKeyboardKey.backspace); From a7349f43aa2c16df7dd4aad71c6207d901e63184 Mon Sep 17 00:00:00 2001 From: appflowy Date: Tue, 30 Aug 2022 20:54:11 +0800 Subject: [PATCH 083/106] chore: add edit card button --- .../card/board_text_cell_bloc.dart | 6 +++ .../board/application/card/card_bloc.dart | 5 +- .../board/presentation/card/board_cell.dart | 51 ++++++++++++++++++ .../card/board_select_option_cell.dart | 7 ++- .../presentation/card/board_text_cell.dart | 52 ++++++++++++++----- .../plugins/board/presentation/card/card.dart | 43 ++++++++++++++- .../presentation/card/card_cell_builder.dart | 4 ++ .../presentation/card/card_container.dart | 34 ++++++------ .../packages/flowy_infra/lib/notifier.dart | 9 +++- .../selection_type_option/select_option.rs | 1 + 10 files changed, 178 insertions(+), 34 deletions(-) diff --git a/frontend/app_flowy/lib/plugins/board/application/card/board_text_cell_bloc.dart b/frontend/app_flowy/lib/plugins/board/application/card/board_text_cell_bloc.dart index 3df8b029f8..9d1b14c605 100644 --- a/frontend/app_flowy/lib/plugins/board/application/card/board_text_cell_bloc.dart +++ b/frontend/app_flowy/lib/plugins/board/application/card/board_text_cell_bloc.dart @@ -27,6 +27,9 @@ class BoardTextCellBloc extends Bloc { emit(state.copyWith(content: text)); } }, + enableEdit: (bool enabled) { + emit(state.copyWith(enableEdit: enabled)); + }, ); }, ); @@ -57,6 +60,7 @@ class BoardTextCellBloc extends Bloc { class BoardTextCellEvent with _$BoardTextCellEvent { const factory BoardTextCellEvent.initial() = _InitialCell; const factory BoardTextCellEvent.updateText(String text) = _UpdateContent; + const factory BoardTextCellEvent.enableEdit(bool enabled) = _EnableEdit; const factory BoardTextCellEvent.didReceiveCellUpdate(String cellContent) = _DidReceiveCellUpdate; } @@ -65,10 +69,12 @@ class BoardTextCellEvent with _$BoardTextCellEvent { class BoardTextCellState with _$BoardTextCellState { const factory BoardTextCellState({ required String content, + required bool enableEdit, }) = _BoardTextCellState; factory BoardTextCellState.initial(GridCellController context) => BoardTextCellState( content: context.getCellData() ?? "", + enableEdit: false, ); } diff --git a/frontend/app_flowy/lib/plugins/board/application/card/card_bloc.dart b/frontend/app_flowy/lib/plugins/board/application/card/card_bloc.dart index 7f1ae766e6..ad30d2b250 100644 --- a/frontend/app_flowy/lib/plugins/board/application/card/card_bloc.dart +++ b/frontend/app_flowy/lib/plugins/board/application/card/card_bloc.dart @@ -107,7 +107,10 @@ class BoardCardState with _$BoardCardState { factory BoardCardState.initial( RowPB rowPB, UnmodifiableListView cells) => - BoardCardState(rowPB: rowPB, cells: cells); + BoardCardState( + rowPB: rowPB, + cells: cells, + ); } class BoardCellEquatable extends Equatable { diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/board_cell.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/board_cell.dart index 3c0c0298e9..4a7455f365 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/board_cell.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/board_cell.dart @@ -1,3 +1,54 @@ +import 'package:app_flowy/plugins/grid/application/prelude.dart'; +import 'package:flowy_infra/notifier.dart'; + abstract class FocusableBoardCell { set becomeFocus(bool isFocus); } + +class EditableCellNotifier { + final Notifier becomeFirstResponder = Notifier(); + + final Notifier resignFirstResponder = Notifier(); + + EditableCellNotifier(); +} + +class EditableRowNotifier { + Map cells = {}; + + void insertCell( + GridCellIdentifier cellIdentifier, + EditableCellNotifier notifier, + ) { + cells[EditableCellId.from(cellIdentifier)] = notifier; + } + + void becomeFirstResponder() { + for (final notifier in cells.values) { + notifier.becomeFirstResponder.notify(); + } + } + + void resignFirstResponder() { + for (final notifier in cells.values) { + notifier.resignFirstResponder.notify(); + } + } +} + +abstract class EditableCell { + EditableCellNotifier? get editableNotifier; +} + +class EditableCellId { + String fieldId; + String rowId; + + EditableCellId(this.rowId, this.fieldId); + + factory EditableCellId.from(GridCellIdentifier cellIdentifier) => + EditableCellId( + cellIdentifier.rowId, + cellIdentifier.fieldId, + ); +} diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/board_select_option_cell.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/board_select_option_cell.dart index e0607db306..173f569f07 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/board_select_option_cell.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/board_select_option_cell.dart @@ -5,13 +5,18 @@ import 'package:app_flowy/plugins/grid/presentation/widgets/cell/select_option_c import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -class BoardSelectOptionCell extends StatefulWidget { +import 'board_cell.dart'; + +class BoardSelectOptionCell extends StatefulWidget with EditableCell { final String groupId; final GridCellControllerBuilder cellControllerBuilder; + @override + final EditableCellNotifier? editableNotifier; const BoardSelectOptionCell({ required this.groupId, required this.cellControllerBuilder, + this.editableNotifier, Key? key, }) : super(key: key); diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/board_text_cell.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/board_text_cell.dart index 20b1a07085..49048d2472 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/board_text_cell.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/board_text_cell.dart @@ -4,15 +4,19 @@ import 'package:app_flowy/plugins/grid/presentation/widgets/cell/cell_builder.da import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -class BoardTextCell extends StatefulWidget { +import 'board_cell.dart'; + +class BoardTextCell extends StatefulWidget with EditableCell { final String groupId; final bool isFocus; - + @override + final EditableCellNotifier? editableNotifier; final GridCellControllerBuilder cellControllerBuilder; const BoardTextCell({ required this.groupId, required this.cellControllerBuilder, + this.editableNotifier, this.isFocus = false, Key? key, }) : super(key: key); @@ -37,6 +41,18 @@ class _BoardTextCellState extends State { if (widget.isFocus) { focusNode.requestFocus(); } + + widget.editableNotifier?.becomeFirstResponder.addListener(() { + if (!mounted) return; + focusNode.requestFocus(); + _cellBloc.add(const BoardTextCellEvent.enableEdit(true)); + }); + + widget.editableNotifier?.resignFirstResponder.addListener(() { + if (!mounted) return; + _cellBloc.add(const BoardTextCellEvent.enableEdit(false)); + }); + super.initState(); } @@ -50,18 +66,26 @@ class _BoardTextCellState extends State { _controller.text = state.content; } }, - child: TextField( - controller: _controller, - focusNode: focusNode, - onChanged: (value) => focusChanged(), - onEditingComplete: () => focusNode.unfocus(), - maxLines: 1, - style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500), - decoration: const InputDecoration( - contentPadding: EdgeInsets.symmetric(vertical: 6), - border: InputBorder.none, - isDense: true, - ), + child: BlocBuilder( + buildWhen: (previous, current) => + previous.enableEdit != current.enableEdit, + builder: (context, state) { + return TextField( + // autofocus: true, + // enabled: state.enableEdit, + controller: _controller, + focusNode: focusNode, + onChanged: (value) => focusChanged(), + onEditingComplete: () => focusNode.unfocus(), + maxLines: 1, + style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500), + decoration: const InputDecoration( + contentPadding: EdgeInsets.symmetric(vertical: 6), + border: InputBorder.none, + isDense: true, + ), + ); + }, ), ), ); diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart index 08331f6021..6c0aaf3c48 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart @@ -7,6 +7,7 @@ import 'package:flowy_infra/theme.dart'; import 'package:flowy_infra_ui/flowy_infra_ui_web.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'board_cell.dart'; import 'card_cell_builder.dart'; import 'card_container.dart'; @@ -36,9 +37,11 @@ class BoardCard extends StatefulWidget { class _BoardCardState extends State { late BoardCardBloc _cardBloc; + late EditableRowNotifier rowNotifier; @override void initState() { + rowNotifier = EditableRowNotifier(); _cardBloc = BoardCardBloc( gridId: widget.gridId, fieldId: widget.fieldId, @@ -58,7 +61,12 @@ class _BoardCardState extends State { builder: (context, state) { return BoardCardContainer( accessoryBuilder: (context) { - return [const _CardMoreOption()]; + return [ + _CardEditOption( + startEditing: () => rowNotifier.becomeFirstResponder(), + ), + const _CardMoreOption(), + ]; }, onTap: (context) { widget.openCard(context); @@ -83,11 +91,14 @@ class _BoardCardState extends State { final List children = []; cells.asMap().forEach( (int index, GridCellIdentifier cellId) { + final cellNotifier = EditableCellNotifier(); Widget child = widget.cellBuilder.buildCell( widget.groupId, cellId, widget.isEditing, + cellNotifier, ); + rowNotifier.insertCell(cellId, cellNotifier); if (index != 0) { child = Padding( @@ -121,7 +132,11 @@ class _CardMoreOption extends StatelessWidget with CardAccessory { @override Widget build(BuildContext context) { - return svgWidget('grid/details', color: context.read().iconColor); + return Padding( + padding: const EdgeInsets.all(3.0), + child: + svgWidget('grid/details', color: context.read().iconColor), + ); } @override @@ -131,3 +146,27 @@ class _CardMoreOption extends StatelessWidget with CardAccessory { ).show(context, direction: AnchorDirection.bottomWithCenterAligned); } } + +class _CardEditOption extends StatelessWidget with CardAccessory { + final VoidCallback startEditing; + const _CardEditOption({ + required this.startEditing, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(3.0), + child: svgWidget( + 'editor/edit', + color: context.read().iconColor, + ), + ); + } + + @override + void onTap(BuildContext context) { + startEditing(); + } +} diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/card_cell_builder.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/card_cell_builder.dart index b292457193..99bd3f3b3a 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/card_cell_builder.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/card_cell_builder.dart @@ -2,6 +2,7 @@ import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_servic import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart'; import 'package:flutter/material.dart'; +import 'board_cell.dart'; import 'board_checkbox_cell.dart'; import 'board_date_cell.dart'; import 'board_number_cell.dart'; @@ -23,6 +24,7 @@ class BoardCellBuilder { String groupId, GridCellIdentifier cellId, bool isEditing, + EditableCellNotifier cellNotifier, ) { final cellControllerBuilder = GridCellControllerBuilder( delegate: delegate, @@ -54,6 +56,7 @@ class BoardCellBuilder { return BoardSelectOptionCell( groupId: groupId, cellControllerBuilder: cellControllerBuilder, + editableNotifier: cellNotifier, key: key, ); case FieldType.Number: @@ -67,6 +70,7 @@ class BoardCellBuilder { groupId: groupId, cellControllerBuilder: cellControllerBuilder, isFocus: isEditing, + editableNotifier: cellNotifier, key: key, ); case FieldType.URL: diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/card_container.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/card_container.dart index 3ca934e508..e3660d68f3 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/card_container.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/card_container.dart @@ -69,12 +69,11 @@ class CardAccessoryContainer extends StatelessWidget { style: HoverStyle( hoverColor: theme.hover, backgroundColor: theme.surface, + borderRadius: BorderRadius.zero, ), - builder: (_, onHover) => Container( - width: 26, - height: 26, - padding: const EdgeInsets.all(3), - decoration: _makeBoxDecoration(context), + builder: (_, onHover) => SizedBox( + width: 24, + height: 24, child: accessory, ), ); @@ -85,7 +84,11 @@ class CardAccessoryContainer extends StatelessWidget { ); }).toList(); - return Wrap(children: children, spacing: 6); + return Container( + clipBehavior: Clip.hardEdge, + decoration: _makeBoxDecoration(context), + child: Row(children: children), + ); } } @@ -95,15 +98,16 @@ BoxDecoration _makeBoxDecoration(BuildContext context) { return BoxDecoration( color: Colors.transparent, border: Border.fromBorderSide(borderSide), - boxShadow: const [ - BoxShadow( - color: Colors.transparent, - spreadRadius: 0, - blurRadius: 2, - offset: Offset.zero, - ) - ], - borderRadius: const BorderRadius.all(Radius.circular(6)), + // boxShadow: const [ + // BoxShadow( + // color: Colors.transparent, + // spreadRadius: 0, + // blurRadius: 5, + // offset: Offset.zero, + // ) + // ], + + borderRadius: const BorderRadius.all(Radius.circular(4)), ); } diff --git a/frontend/app_flowy/packages/flowy_infra/lib/notifier.dart b/frontend/app_flowy/packages/flowy_infra/lib/notifier.dart index 7eaf139f0c..ac225775d7 100644 --- a/frontend/app_flowy/packages/flowy_infra/lib/notifier.dart +++ b/frontend/app_flowy/packages/flowy_infra/lib/notifier.dart @@ -31,7 +31,8 @@ class PublishNotifier extends ChangeNotifier { T? get currentValue => _value; - void addPublishListener(void Function(T) callback, {bool Function()? listenWhen}) { + void addPublishListener(void Function(T) callback, + {bool Function()? listenWhen}) { super.addListener( () { if (_value == null) { @@ -47,3 +48,9 @@ class PublishNotifier extends ChangeNotifier { ); } } + +class Notifier extends ChangeNotifier { + void notify() { + notifyListeners(); + } +} diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/select_option.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/select_option.rs index 9270f73684..12f94c943c 100644 --- a/frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/select_option.rs +++ b/frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/select_option.rs @@ -154,6 +154,7 @@ pub fn select_option_color_from_index(index: usize) -> SelectOptionColorPB { _ => SelectOptionColorPB::Purple, } } + pub struct SelectOptionIds(Vec); impl SelectOptionIds { From 94a440f773443785c7c3b232f68c2ea560bf100e Mon Sep 17 00:00:00 2001 From: appflowy Date: Wed, 31 Aug 2022 08:58:34 +0800 Subject: [PATCH 084/106] chore: update flutter packages --- .../packages/flowy_infra_ui/pubspec.yaml | 12 ++++++------ .../app_flowy/packages/flowy_sdk/pubspec.yaml | 2 +- frontend/app_flowy/pubspec.lock | 16 ++++++++-------- frontend/app_flowy/pubspec.yaml | 11 ++++++----- 4 files changed, 21 insertions(+), 20 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_infra_ui/pubspec.yaml b/frontend/app_flowy/packages/flowy_infra_ui/pubspec.yaml index e9473070a0..36a247411b 100644 --- a/frontend/app_flowy/packages/flowy_infra_ui/pubspec.yaml +++ b/frontend/app_flowy/packages/flowy_infra_ui/pubspec.yaml @@ -2,7 +2,7 @@ name: flowy_infra_ui description: A new flutter plugin project. version: 0.0.1 homepage: -publish_to: 'none' +publish_to: "none" environment: sdk: ">=2.12.0 <3.0.0" @@ -13,11 +13,11 @@ dependencies: sdk: flutter # Thirdparty packages - textstyle_extensions: '2.0.0-nullsafety' - dartz: '0.10.0-nullsafety.2' + textstyle_extensions: "2.0.0-nullsafety" + dartz: provider: ^6.0.1 - styled_widget: '^0.3.1' - equatable: '^2.0.3' + styled_widget: "^0.3.1" + equatable: "^2.0.3" animations: ^2.0.0 loading_indicator: ^3.0.1 @@ -52,4 +52,4 @@ flutter: linux: pluginClass: FlowyInfraUIPlugin web: - default_package: flowy_infra_ui_web \ No newline at end of file + default_package: flowy_infra_ui_web diff --git a/frontend/app_flowy/packages/flowy_sdk/pubspec.yaml b/frontend/app_flowy/packages/flowy_sdk/pubspec.yaml index 95ca835649..ced9b97167 100644 --- a/frontend/app_flowy/packages/flowy_sdk/pubspec.yaml +++ b/frontend/app_flowy/packages/flowy_sdk/pubspec.yaml @@ -14,7 +14,7 @@ dependencies: ffi: ^1.0.0 isolates: ^3.0.3+8 protobuf: "2.0.0" - dartz: "0.10.0-nullsafety.2" + dartz: ^0.10.1 freezed_annotation: logger: ^1.0.0 diff --git a/frontend/app_flowy/pubspec.lock b/frontend/app_flowy/pubspec.lock index 8466696ec9..199a41baa3 100644 --- a/frontend/app_flowy/pubspec.lock +++ b/frontend/app_flowy/pubspec.lock @@ -84,7 +84,7 @@ packages: name: build_config url: "https://pub.dartlang.org" source: hosted - version: "1.0.0" + version: "1.1.0" build_daemon: dependency: transitive description: @@ -105,7 +105,7 @@ packages: name: build_runner url: "https://pub.dartlang.org" source: hosted - version: "2.1.11" + version: "2.2.0" build_runner_core: dependency: transitive description: @@ -268,12 +268,12 @@ packages: source: hosted version: "2.2.3" dartz: - dependency: transitive + dependency: "direct main" description: name: dartz url: "https://pub.dartlang.org" source: hosted - version: "0.10.0-nullsafety.2" + version: "0.10.1" dbus: dependency: transitive description: @@ -474,7 +474,7 @@ packages: name: flutter_lints url: "https://pub.dartlang.org" source: hosted - version: "1.0.4" + version: "2.0.1" flutter_localizations: dependency: "direct main" description: flutter @@ -526,14 +526,14 @@ packages: name: freezed url: "https://pub.dartlang.org" source: hosted - version: "2.0.3+1" + version: "2.1.0+1" freezed_annotation: dependency: "direct main" description: name: freezed_annotation url: "https://pub.dartlang.org" source: hosted - version: "2.0.3" + version: "2.1.0" frontend_server_client: dependency: transitive description: @@ -701,7 +701,7 @@ packages: name: lints url: "https://pub.dartlang.org" source: hosted - version: "1.0.1" + version: "2.0.0" loading_indicator: dependency: transitive description: diff --git a/frontend/app_flowy/pubspec.yaml b/frontend/app_flowy/pubspec.yaml index 77412464ff..c81b96e156 100644 --- a/frontend/app_flowy/pubspec.yaml +++ b/frontend/app_flowy/pubspec.yaml @@ -50,9 +50,10 @@ dependencies: intl: ^0.17.0 time: "^2.0.0" equatable: "^2.0.3" - freezed_annotation: + freezed_annotation: ^2.1.0 get_it: "^7.1.3" flutter_bloc: "^8.0.1" + dartz: ^0.10.1 provider: ^6.0.1 path_provider: ^2.0.1 window_size: @@ -68,7 +69,7 @@ dependencies: url_launcher: ^6.0.2 # file_picker: ^4.2.1 clipboard: ^0.1.3 - connectivity_plus: 2.2.0 + connectivity_plus: ^2.3.6+1 easy_localization: ^3.0.0 textfield_tags: ^2.0.0 # The following adds the Cupertino Icons font to your application. @@ -82,11 +83,11 @@ dependencies: hotkey_manager: ^0.1.7 dev_dependencies: - flutter_lints: ^1.0.0 + flutter_lints: ^2.0.1 flutter_test: sdk: flutter - build_runner: - freezed: + build_runner: ^2.2.0 + freezed: ^2.1.0+1 bloc_test: ^9.0.2 dependency_overrides: From a2d8fe9e80c1fdde3cfd9d75568ed9a758a389dc Mon Sep 17 00:00:00 2001 From: appflowy Date: Wed, 31 Aug 2022 09:19:31 +0800 Subject: [PATCH 085/106] chore: run dart fix --apply --- .../plugins/board/application/board_bloc.dart | 2 +- .../application/board_data_controller.dart | 3 ++- .../application/card/board_date_cell_bloc.dart | 2 +- .../card/board_select_option_cell.dart | 2 +- .../presentation/card/card_container.dart | 4 ++-- .../lib/plugins/doc/presentation/banner.dart | 8 ++++---- .../presentation/toolbar/header_button.dart | 18 +++++++++--------- .../cell/cell_service/cell_service.dart | 2 +- .../grid/application/cell/date_cal_bloc.dart | 6 +++--- .../grid/application/cell/date_cell_bloc.dart | 2 +- .../grid/application/grid_data_controller.dart | 3 ++- .../grid/application/row/row_cache.dart | 3 ++- .../widgets/cell/cell_accessory.dart | 4 ++-- .../widgets/cell/cell_container.dart | 2 +- .../widgets/cell/date_cell/date_editor.dart | 10 ++++------ .../select_option_cell/select_option_cell.dart | 4 ++-- .../select_option_editor.dart | 4 ++-- .../cell/select_option_cell/text_field.dart | 2 +- .../widgets/cell/url_cell/cell_editor.dart | 2 +- .../header/field_cell_action_sheet.dart | 2 +- .../widgets/header/field_editor.dart | 2 +- .../header/field_type_option_editor.dart | 2 +- .../presentation/widgets/row/grid_row.dart | 2 +- .../widgets/row/row_action_sheet.dart | 2 +- .../presentation/widgets/row/row_detail.dart | 4 ++-- .../widgets/toolbar/grid_property.dart | 2 +- .../widgets/toolbar/grid_setting.dart | 2 +- .../app_flowy/lib/plugins/trash/trash.dart | 2 +- .../lib/startup/tasks/app_widget.dart | 2 +- .../lib/user/presentation/sign_in_screen.dart | 2 +- .../lib/user/presentation/sign_up_screen.dart | 2 +- .../workspace/presentation/home/hotkeys.dart | 4 ++-- .../workspace/presentation/home/menu/menu.dart | 2 +- .../presentation/home/menu/menu_user.dart | 2 +- .../presentation/home/navigation.dart | 2 +- .../lib/workspace/presentation/home/toast.dart | 8 ++++---- .../widgets/settings_language_view.dart | 2 +- .../widgets/edit_panel/panel_animation.dart | 4 ++-- .../src/default_emoji_picker_view.dart | 2 +- .../widgets/float_bubble/question_bubble.dart | 2 +- .../presentation/widgets/pop_up_window.dart | 2 +- frontend/app_flowy/pubspec.lock | 12 ++++++------ frontend/app_flowy/pubspec.yaml | 1 + 43 files changed, 77 insertions(+), 75 deletions(-) diff --git a/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart b/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart index 53038c8d42..a71480c710 100644 --- a/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart +++ b/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart @@ -23,7 +23,7 @@ class BoardBloc extends Bloc { final BoardDataController _gridDataController; late final AFBoardDataController boardController; final MoveRowFFIService _rowService; - LinkedHashMap groupControllers = LinkedHashMap.new(); + LinkedHashMap groupControllers = LinkedHashMap(); GridFieldCache get fieldCache => _gridDataController.fieldCache; String get gridId => _gridDataController.gridId; diff --git a/frontend/app_flowy/lib/plugins/board/application/board_data_controller.dart b/frontend/app_flowy/lib/plugins/board/application/board_data_controller.dart index 79f53093f1..2f9bdc24e0 100644 --- a/frontend/app_flowy/lib/plugins/board/application/board_data_controller.dart +++ b/frontend/app_flowy/lib/plugins/board/application/board_data_controller.dart @@ -52,7 +52,8 @@ class BoardDataController { BoardDataController({required ViewPB view}) : gridId = view.id, _listener = BoardListener(view.id), - _blocks = LinkedHashMap.new(), + // ignore: prefer_collection_literals + _blocks = LinkedHashMap(), _gridFFIService = GridFFIService(gridId: view.id), fieldCache = GridFieldCache(gridId: view.id); diff --git a/frontend/app_flowy/lib/plugins/board/application/card/board_date_cell_bloc.dart b/frontend/app_flowy/lib/plugins/board/application/card/board_date_cell_bloc.dart index 76267ededb..b1110f45cc 100644 --- a/frontend/app_flowy/lib/plugins/board/application/card/board_date_cell_bloc.dart +++ b/frontend/app_flowy/lib/plugins/board/application/card/board_date_cell_bloc.dart @@ -79,7 +79,7 @@ class BoardDateCellState with _$BoardDateCellState { String _dateStrFromCellData(DateCellDataPB? cellData) { String dateStr = ""; if (cellData != null) { - dateStr = cellData.date + " " + cellData.time; + dateStr = "${cellData.date} ${cellData.time}"; } return dateStr; } diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/board_select_option_cell.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/board_select_option_cell.dart index 173f569f07..fb1cc15de6 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/board_select_option_cell.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/board_select_option_cell.dart @@ -65,7 +65,7 @@ class _BoardSelectOptionCellState extends State { alignment: AlignmentDirectional.center, fit: StackFit.expand, children: [ - Wrap(children: children, spacing: 4, runSpacing: 2), + Wrap(spacing: 4, runSpacing: 2, children: children), _SelectOptionDialog( controller: widget.cellControllerBuilder.build(), ), diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/card_container.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/card_container.dart index e3660d68f3..d28e6712c9 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/card_container.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/card_container.dart @@ -26,8 +26,8 @@ class BoardCardContainer extends StatelessWidget { final accessories = accessoryBuilder!(context); if (accessories.isNotEmpty) { container = _CardEnterRegion( - child: container, accessories: accessories, + child: container, ); } } @@ -78,9 +78,9 @@ class CardAccessoryContainer extends StatelessWidget { ), ); return GestureDetector( - child: hover, behavior: HitTestBehavior.opaque, onTap: () => accessory.onTap(context), + child: hover, ); }).toList(); diff --git a/frontend/app_flowy/lib/plugins/doc/presentation/banner.dart b/frontend/app_flowy/lib/plugins/doc/presentation/banner.dart index bd4b651da8..66c1f6dfba 100644 --- a/frontend/app_flowy/lib/plugins/doc/presentation/banner.dart +++ b/frontend/app_flowy/lib/plugins/doc/presentation/banner.dart @@ -40,11 +40,11 @@ class DocumentBanner extends StatelessWidget { downColor: theme.main1, outlineColor: Colors.white, borderRadius: Corners.s8Border, + onPressed: onRestore, child: FlowyText.medium( LocaleKeys.deletePagePrompt_restore.tr(), color: Colors.white, - fontSize: 14), - onPressed: onRestore), + fontSize: 14)), const HSpace(20), BaseStyledButton( minWidth: 220, @@ -55,11 +55,11 @@ class DocumentBanner extends StatelessWidget { downColor: theme.main1, outlineColor: Colors.white, borderRadius: Corners.s8Border, + onPressed: onDelete, child: FlowyText.medium( LocaleKeys.deletePagePrompt_deletePermanent.tr(), color: Colors.white, - fontSize: 14), - onPressed: onDelete), + fontSize: 14)), ], ), ), diff --git a/frontend/app_flowy/lib/plugins/doc/presentation/toolbar/header_button.dart b/frontend/app_flowy/lib/plugins/doc/presentation/toolbar/header_button.dart index 427c5cf559..2da700df47 100644 --- a/frontend/app_flowy/lib/plugins/doc/presentation/toolbar/header_button.dart +++ b/frontend/app_flowy/lib/plugins/doc/presentation/toolbar/header_button.dart @@ -35,15 +35,15 @@ class _FlowyHeaderStyleButtonState extends State { @override Widget build(BuildContext context) { - final _valueToText = { + final valueToText = { Attribute.h1: 'H1', Attribute.h2: 'H2', Attribute.h3: 'H3', }; - final _valueAttribute = [Attribute.h1, Attribute.h2, Attribute.h3]; - final _valueString = ['H1', 'H2', 'H3']; - final _attributeImageName = ['editor/H1', 'editor/H2', 'editor/H3']; + final valueAttribute = [Attribute.h1, Attribute.h2, Attribute.h3]; + final valueString = ['H1', 'H2', 'H3']; + final attributeImageName = ['editor/H1', 'editor/H2', 'editor/H3']; return Row( mainAxisSize: MainAxisSize.min, @@ -52,18 +52,18 @@ class _FlowyHeaderStyleButtonState extends State { // _valueToText[_value] == _valueString[index] ? svg('editor/H1', color: Colors.white) : svg('editor/H1'); final headerTitle = "${LocaleKeys.toolbar_header.tr()} ${index + 1}"; - final _isToggled = _valueToText[_value] == _valueString[index]; + final isToggled = valueToText[_value] == valueString[index]; return ToolbarIconButton( onPressed: () { - if (_isToggled) { + if (isToggled) { widget.controller.formatSelection(Attribute.header); } else { - widget.controller.formatSelection(_valueAttribute[index]); + widget.controller.formatSelection(valueAttribute[index]); } }, width: widget.iconSize * kIconButtonFactor, - iconName: _attributeImageName[index], - isToggled: _isToggled, + iconName: attributeImageName[index], + isToggled: isToggled, tooltipText: headerTitle, ); }), diff --git a/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/cell_service.dart b/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/cell_service.dart index bb750b4b89..48e82cc906 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/cell_service.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/cell_service.dart @@ -71,6 +71,6 @@ class GridCellIdentifier with _$GridCellIdentifier { FieldType get fieldType => field.fieldType; ValueKey key() { - return ValueKey(rowId + fieldId + "${field.fieldType}"); + return ValueKey("$rowId$fieldId${field.fieldType}"); } } diff --git a/frontend/app_flowy/lib/plugins/grid/application/cell/date_cal_bloc.dart b/frontend/app_flowy/lib/plugins/grid/application/cell/date_cal_bloc.dart index c0584a084b..a7124b7a3d 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/cell/date_cal_bloc.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/cell/date_cal_bloc.dart @@ -119,13 +119,13 @@ class DateCalBloc extends Bloc { } String timeFormatPrompt(FlowyError error) { - String msg = LocaleKeys.grid_field_invalidTimeFormat.tr() + ". "; + String msg = "${LocaleKeys.grid_field_invalidTimeFormat.tr()}. "; switch (state.dateTypeOptionPB.timeFormat) { case TimeFormat.TwelveHour: - msg = msg + "e.g. 01: 00 AM"; + msg = "${msg}e.g. 01: 00 AM"; break; case TimeFormat.TwentyFourHour: - msg = msg + "e.g. 13: 00"; + msg = "${msg}e.g. 13: 00"; break; default: break; diff --git a/frontend/app_flowy/lib/plugins/grid/application/cell/date_cell_bloc.dart b/frontend/app_flowy/lib/plugins/grid/application/cell/date_cell_bloc.dart index 4150093275..4d453eca25 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/cell/date_cell_bloc.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/cell/date_cell_bloc.dart @@ -79,7 +79,7 @@ class DateCellState with _$DateCellState { String _dateStrFromCellData(DateCellDataPB? cellData) { String dateStr = ""; if (cellData != null) { - dateStr = cellData.date + " " + cellData.time; + dateStr = "${cellData.date} ${cellData.time}"; } return dateStr; } diff --git a/frontend/app_flowy/lib/plugins/grid/application/grid_data_controller.dart b/frontend/app_flowy/lib/plugins/grid/application/grid_data_controller.dart index f11db25167..55733b9b7b 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/grid_data_controller.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/grid_data_controller.dart @@ -46,7 +46,8 @@ class GridDataController { GridDataController({required ViewPB view}) : gridId = view.id, - _blocks = LinkedHashMap.new(), + // ignore: prefer_collection_literals + _blocks = LinkedHashMap(), _gridFFIService = GridFFIService(gridId: view.id), fieldCache = GridFieldCache(gridId: view.id); diff --git a/frontend/app_flowy/lib/plugins/grid/application/row/row_cache.dart b/frontend/app_flowy/lib/plugins/grid/application/row/row_cache.dart index d6cd387e74..618d73cbc1 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/row/row_cache.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/row/row_cache.dart @@ -210,7 +210,8 @@ class GridRowCache { } GridCellMap _makeGridCells(String rowId, RowPB? row) { - var cellDataMap = GridCellMap.new(); + // ignore: prefer_collection_literals + var cellDataMap = GridCellMap(); for (final field in _fieldNotifier.fields) { if (field.visibility) { cellDataMap[field.id] = GridCellIdentifier( diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/cell_accessory.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/cell_accessory.dart index 9b3f281130..8a88316473 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/cell_accessory.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/cell_accessory.dart @@ -190,12 +190,12 @@ class CellAccessoryContainer extends StatelessWidget { ), ); return GestureDetector( - child: hover, behavior: HitTestBehavior.opaque, onTap: () => accessory.onTap(), + child: hover, ); }).toList(); - return Wrap(children: children, spacing: 6); + return Wrap(spacing: 6, children: children); } } diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/cell_container.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/cell_container.dart index ed09ec3f36..eea58775dd 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/cell_container.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/cell_container.dart @@ -44,8 +44,8 @@ class CellContainer extends StatelessWidget { if (accessories.isNotEmpty) { container = _GridCellEnterRegion( - child: container, accessories: accessories, + child: container, ); } } diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/date_cell/date_editor.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/date_cell/date_editor.dart index f6ddf42fba..d1289b93f2 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/date_cell/date_editor.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/date_cell/date_editor.dart @@ -48,8 +48,8 @@ class DateCellEditor with FlowyOverlayDelegate { FlowyOverlay.of(context).insertWithAnchor( widget: OverlayContainer( - child: calendar, constraints: BoxConstraints.loose(const Size(320, 500)), + child: calendar, ), identifier: DateCellEditor.identifier(), anchorContext: context, @@ -304,9 +304,7 @@ class _DateTypeOptionButton extends StatelessWidget { @override Widget build(BuildContext context) { final theme = context.watch(); - final title = LocaleKeys.grid_field_dateFormat.tr() + - " &" + - LocaleKeys.grid_field_timeFormat.tr(); + final title = "${LocaleKeys.grid_field_dateFormat.tr()} &${LocaleKeys.grid_field_timeFormat.tr()}"; return BlocSelector( selector: (state) => state.dateTypeOptionPB, builder: (context, dateTypeOptionPB) { @@ -349,8 +347,8 @@ class _CalDateTimeSetting extends StatefulWidget { hide(context); FlowyOverlay.of(context).insertWithAnchor( widget: OverlayContainer( - child: this, constraints: BoxConstraints.loose(const Size(140, 100)), + child: this, ), identifier: _CalDateTimeSetting.identifier(), anchorContext: context, @@ -415,8 +413,8 @@ class _CalDateTimeSettingState extends State<_CalDateTimeSetting> { overlayIdentifier = child.toString(); FlowyOverlay.of(context).insertWithAnchor( widget: OverlayContainer( - child: child, constraints: BoxConstraints.loose(const Size(460, 440)), + child: child, ), identifier: overlayIdentifier!, anchorContext: context, diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/select_option_cell.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/select_option_cell.dart index a8d3993a2f..c771586e70 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/select_option_cell.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/select_option_cell.dart @@ -163,14 +163,14 @@ class SelectOptionWrap extends StatelessWidget { child = Align( alignment: Alignment.centerLeft, child: Wrap( + spacing: 4, + runSpacing: 2, children: selectOptions .map((option) => SelectOptionTag.fromOption( context: context, option: option, )) .toList(), - spacing: 4, - runSpacing: 2, ), ); } diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/select_option_editor.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/select_option_editor.dart index c07eef5617..ef0dab83c9 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/select_option_editor.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/select_option_editor.dart @@ -72,8 +72,8 @@ class SelectOptionCellEditor extends StatelessWidget with FlowyOverlayDelegate { // FlowyOverlay.of(context).insertWithAnchor( widget: OverlayContainer( - child: SizedBox(width: _editorPannelWidth, child: editor), constraints: BoxConstraints.loose(const Size(_editorPannelWidth, 300)), + child: SizedBox(width: _editorPannelWidth, child: editor), ), identifier: SelectOptionCellEditor.identifier(), anchorContext: context, @@ -289,8 +289,8 @@ class _SelectOptionCell extends StatelessWidget { FlowyOverlay.of(context).remove(overlayIdentifier); FlowyOverlay.of(context).insertWithAnchor( widget: OverlayContainer( - child: pannel, constraints: BoxConstraints.loose(const Size(200, 300)), + child: pannel, ), identifier: overlayIdentifier, anchorContext: context, diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/text_field.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/text_field.dart index 5482a403cc..022d411f2b 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/text_field.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/text_field.dart @@ -108,7 +108,7 @@ class SelectOptionTextField extends StatelessWidget { child: SingleChildScrollView( controller: sc, scrollDirection: Axis.horizontal, - child: Wrap(children: children, spacing: 4), + child: Wrap(spacing: 4, children: children), ), ); } diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/url_cell/cell_editor.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/url_cell/cell_editor.dart index e68ac720a3..b9e0f1ef48 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/url_cell/cell_editor.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/url_cell/cell_editor.dart @@ -30,11 +30,11 @@ class URLCellEditor extends StatefulWidget with FlowyOverlayDelegate { // FlowyOverlay.of(context).insertWithAnchor( widget: OverlayContainer( + constraints: BoxConstraints.loose(const Size(300, 160)), child: SizedBox( width: 200, child: Padding(padding: const EdgeInsets.all(6), child: editor), ), - constraints: BoxConstraints.loose(const Size(300, 160)), ), identifier: URLCellEditor.identifier(), anchorContext: context, diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_cell_action_sheet.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_cell_action_sheet.dart index cd884cd557..9ea8b56b77 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_cell_action_sheet.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_cell_action_sheet.dart @@ -24,8 +24,8 @@ class GridFieldCellActionSheet extends StatelessWidget void show(BuildContext overlayContext) { FlowyOverlay.of(overlayContext).insertWithAnchor( widget: OverlayContainer( - child: this, constraints: BoxConstraints.loose(const Size(240, 200)), + child: this, ), identifier: GridFieldCellActionSheet.identifier(), anchorContext: overlayContext, diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_editor.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_editor.dart index efa70bb81a..3f497d3304 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_editor.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_editor.dart @@ -56,8 +56,8 @@ class FieldEditor extends StatelessWidget with FlowyOverlayDelegate { FlowyOverlay.of(context).remove(identifier()); FlowyOverlay.of(context).insertWithAnchor( widget: OverlayContainer( - child: this, constraints: BoxConstraints.loose(const Size(280, 400)), + child: this, ), identifier: identifier(), anchorContext: context, diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_type_option_editor.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_type_option_editor.dart index 20440235cb..d9e5eb4f38 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_type_option_editor.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_type_option_editor.dart @@ -110,8 +110,8 @@ class _FieldTypeOptionEditorState extends State { currentOverlayIdentifier = identifier; FlowyOverlay.of(context).insertWithAnchor( widget: OverlayContainer( - child: child, constraints: BoxConstraints.loose(const Size(460, 440)), + child: child, ), identifier: identifier, anchorContext: context, diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/grid_row.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/grid_row.dart index d560f03fc0..55ec7b9832 100755 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/grid_row.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/grid_row.dart @@ -190,7 +190,6 @@ class RowContent extends StatelessWidget { return CellContainer( width: cellId.field.width.toDouble(), - child: child, rowStateNotifier: Provider.of(context, listen: false), accessoryBuilder: (buildContext) { @@ -208,6 +207,7 @@ class RowContent extends StatelessWidget { } return accessories; }, + child: child, ); }, ).toList(); diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/row_action_sheet.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/row_action_sheet.dart index b4390d098f..8c828ec627 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/row_action_sheet.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/row_action_sheet.dart @@ -59,8 +59,8 @@ class GridRowActionSheet extends StatelessWidget { }) { FlowyOverlay.of(overlayContext).insertWithAnchor( widget: OverlayContainer( - child: this, constraints: BoxConstraints.loose(const Size(140, 200)), + child: this, ), identifier: GridRowActionSheet.identifier(), anchorContext: overlayContext, diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/row_detail.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/row_detail.dart index d1ea79b6d1..7d07e9fa3f 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/row_detail.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/row_detail.dart @@ -38,8 +38,8 @@ class RowDetailPage extends StatefulWidget with FlowyOverlayDelegate { final size = windowSize * 0.5; FlowyOverlay.of(context).insertWithRect( widget: OverlayContainer( - child: this, constraints: BoxConstraints.tight(size), + child: this, ), identifier: RowDetailPage.identifier(), anchorPosition: Offset(-size.width / 2.0, -size.height / 2.0), @@ -156,9 +156,9 @@ class _RowDetailCell extends StatelessWidget { behavior: HitTestBehavior.translucent, onTap: () => cell.beginFocus.notify(), child: AccessoryHover( - child: cell, contentPadding: const EdgeInsets.symmetric(horizontal: 10, vertical: 12), + child: cell, ), ); diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/toolbar/grid_property.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/toolbar/grid_property.dart index 02230e5139..08ba3dcd08 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/toolbar/grid_property.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/toolbar/grid_property.dart @@ -30,8 +30,8 @@ class GridPropertyList extends StatelessWidget with FlowyOverlayDelegate { void show(BuildContext context) { FlowyOverlay.of(context).insertWithAnchor( widget: OverlayContainer( - child: this, constraints: BoxConstraints.loose(const Size(260, 400)), + child: this, ), identifier: identifier(), anchorContext: context, diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/toolbar/grid_setting.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/toolbar/grid_setting.dart index f555b6266a..289d84141f 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/toolbar/grid_setting.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/toolbar/grid_setting.dart @@ -53,8 +53,8 @@ class GridSettingList extends StatelessWidget { FlowyOverlay.of(context).insertWithAnchor( widget: OverlayContainer( - child: list, constraints: BoxConstraints.loose(const Size(140, 400)), + child: list, ), identifier: list.identifier(), anchorContext: context, diff --git a/frontend/app_flowy/lib/plugins/trash/trash.dart b/frontend/app_flowy/lib/plugins/trash/trash.dart index 0d17091581..c1622d5b20 100644 --- a/frontend/app_flowy/lib/plugins/trash/trash.dart +++ b/frontend/app_flowy/lib/plugins/trash/trash.dart @@ -91,12 +91,12 @@ class _TrashPageState extends State { builder: (context, state) { return SizedBox.expand( child: Column( + mainAxisAlignment: MainAxisAlignment.start, children: [ _renderTopBar(context, theme, state), const VSpace(32), _renderTrashList(context, state), ], - mainAxisAlignment: MainAxisAlignment.start, ).padding(horizontal: horizontalPadding, vertical: 48), ); }, diff --git a/frontend/app_flowy/lib/startup/tasks/app_widget.dart b/frontend/app_flowy/lib/startup/tasks/app_widget.dart index 6dae04a1c0..fede2e0fdb 100644 --- a/frontend/app_flowy/lib/startup/tasks/app_widget.dart +++ b/frontend/app_flowy/lib/startup/tasks/app_widget.dart @@ -20,8 +20,8 @@ class InitAppWidgetTask extends LaunchTask { final setting = await UserSettingsService().getAppearanceSettings(); final settingModel = AppearanceSettingModel(setting); final app = ApplicationWidget( - child: widget, settingModel: settingModel, + child: widget, ); BlocOverrides.runZoned( () { diff --git a/frontend/app_flowy/lib/user/presentation/sign_in_screen.dart b/frontend/app_flowy/lib/user/presentation/sign_in_screen.dart index ee3600e782..6f17404474 100644 --- a/frontend/app_flowy/lib/user/presentation/sign_in_screen.dart +++ b/frontend/app_flowy/lib/user/presentation/sign_in_screen.dart @@ -94,6 +94,7 @@ class SignUpPrompt extends StatelessWidget { Widget build(BuildContext context) { final theme = context.watch(); return Row( + mainAxisAlignment: MainAxisAlignment.center, children: [ Text(LocaleKeys.signIn_dontHaveAnAccount.tr(), style: TextStyle(color: theme.shader3, fontSize: 12)), TextButton( @@ -107,7 +108,6 @@ class SignUpPrompt extends StatelessWidget { ), ), ], - mainAxisAlignment: MainAxisAlignment.center, ); } } diff --git a/frontend/app_flowy/lib/user/presentation/sign_up_screen.dart b/frontend/app_flowy/lib/user/presentation/sign_up_screen.dart index d0cb7f8b90..75834f3836 100644 --- a/frontend/app_flowy/lib/user/presentation/sign_up_screen.dart +++ b/frontend/app_flowy/lib/user/presentation/sign_up_screen.dart @@ -86,6 +86,7 @@ class SignUpPrompt extends StatelessWidget { Widget build(BuildContext context) { final theme = context.watch(); return Row( + mainAxisAlignment: MainAxisAlignment.center, children: [ Text( LocaleKeys.signUp_alreadyHaveAnAccount.tr(), @@ -97,7 +98,6 @@ class SignUpPrompt extends StatelessWidget { child: Text(LocaleKeys.signIn_buttonText.tr(), style: TextStyle(color: theme.main1)), ), ], - mainAxisAlignment: MainAxisAlignment.center, ); } } diff --git a/frontend/app_flowy/lib/workspace/presentation/home/hotkeys.dart b/frontend/app_flowy/lib/workspace/presentation/home/hotkeys.dart index 32c2bae7fe..0ac9cbc704 100644 --- a/frontend/app_flowy/lib/workspace/presentation/home/hotkeys.dart +++ b/frontend/app_flowy/lib/workspace/presentation/home/hotkeys.dart @@ -13,14 +13,14 @@ class HomeHotKeys extends StatelessWidget { @override Widget build(BuildContext context) { - HotKey _hotKey = HotKey( + HotKey hotKey = HotKey( KeyCode.backslash, modifiers: [Platform.isMacOS ? KeyModifier.meta : KeyModifier.control], // Set hotkey scope (default is HotKeyScope.system) scope: HotKeyScope.inapp, // Set as inapp-wide hotkey. ); hotKeyManager.register( - _hotKey, + hotKey, keyDownHandler: (hotKey) { context.read().add(const HomeEvent.collapseMenu()); getIt().collapsedNotifier.value = diff --git a/frontend/app_flowy/lib/workspace/presentation/home/menu/menu.dart b/frontend/app_flowy/lib/workspace/presentation/home/menu/menu.dart index f9704a695f..1796640d00 100644 --- a/frontend/app_flowy/lib/workspace/presentation/home/menu/menu.dart +++ b/frontend/app_flowy/lib/workspace/presentation/home/menu/menu.dart @@ -222,7 +222,7 @@ class MenuTopBar extends StatelessWidget { Tooltip( richMessage: TextSpan(children: [ TextSpan( - text: LocaleKeys.sideBar_closeSidebar.tr() + "\n"), + text: "${LocaleKeys.sideBar_closeSidebar.tr()}\n"), TextSpan( text: Platform.isMacOS ? "⌘+\\" : "Ctrl+\\", style: const TextStyle(color: Colors.white60), diff --git a/frontend/app_flowy/lib/workspace/presentation/home/menu/menu_user.dart b/frontend/app_flowy/lib/workspace/presentation/home/menu/menu_user.dart index 3d9d76fe29..399bbd1f89 100644 --- a/frontend/app_flowy/lib/workspace/presentation/home/menu/menu_user.dart +++ b/frontend/app_flowy/lib/workspace/presentation/home/menu/menu_user.dart @@ -24,6 +24,7 @@ class MenuUser extends StatelessWidget { getIt(param1: user)..add(const MenuUserEvent.initial()), child: BlocBuilder( builder: (context, state) => Row( + crossAxisAlignment: CrossAxisAlignment.center, children: [ _renderAvatar(context), const HSpace(10), @@ -34,7 +35,6 @@ class MenuUser extends StatelessWidget { //we get the below block back //_renderDropButton(context), ], - crossAxisAlignment: CrossAxisAlignment.center, ), ), ); diff --git a/frontend/app_flowy/lib/workspace/presentation/home/navigation.dart b/frontend/app_flowy/lib/workspace/presentation/home/navigation.dart index 019a567932..bfdb708013 100644 --- a/frontend/app_flowy/lib/workspace/presentation/home/navigation.dart +++ b/frontend/app_flowy/lib/workspace/presentation/home/navigation.dart @@ -101,7 +101,7 @@ class FlowyNavigation extends StatelessWidget { turns: const AlwaysStoppedAnimation(180 / 360), child: Tooltip( richMessage: TextSpan(children: [ - TextSpan(text: LocaleKeys.sideBar_openSidebar.tr() + "\n"), + TextSpan(text: "${LocaleKeys.sideBar_openSidebar.tr()}\n"), TextSpan( text: Platform.isMacOS ? "⌘+\\" : "Ctrl+\\", style: const TextStyle(color: Colors.white60), diff --git a/frontend/app_flowy/lib/workspace/presentation/home/toast.dart b/frontend/app_flowy/lib/workspace/presentation/home/toast.dart index 28241c0ec4..d3473adaa9 100644 --- a/frontend/app_flowy/lib/workspace/presentation/home/toast.dart +++ b/frontend/app_flowy/lib/workspace/presentation/home/toast.dart @@ -10,14 +10,14 @@ class FlowyMessageToast extends StatelessWidget { @override Widget build(BuildContext context) { return Container( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), - child: FlowyText.medium(message, color: Colors.white), - ), decoration: const BoxDecoration( borderRadius: BorderRadius.all(Radius.circular(4)), color: Colors.black, ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + child: FlowyText.medium(message, color: Colors.white), + ), ); } } diff --git a/frontend/app_flowy/lib/workspace/presentation/settings/widgets/settings_language_view.dart b/frontend/app_flowy/lib/workspace/presentation/settings/widgets/settings_language_view.dart index e95e6e83ab..bb1b419da0 100644 --- a/frontend/app_flowy/lib/workspace/presentation/settings/widgets/settings_language_view.dart +++ b/frontend/app_flowy/lib/workspace/presentation/settings/widgets/settings_language_view.dart @@ -84,8 +84,8 @@ class _LanguageSelectorDropdownState extends State { }); }, icon: const Visibility( - child: (Icon(Icons.arrow_downward)), visible: false, + child: (Icon(Icons.arrow_downward)), ), borderRadius: BorderRadius.circular(8), items: EasyLocalization.of(context)!.supportedLocales.map((locale) { diff --git a/frontend/app_flowy/lib/workspace/presentation/widgets/edit_panel/panel_animation.dart b/frontend/app_flowy/lib/workspace/presentation/widgets/edit_panel/panel_animation.dart index 082cb5dabf..e88c6b09b6 100644 --- a/frontend/app_flowy/lib/workspace/presentation/widgets/edit_panel/panel_animation.dart +++ b/frontend/app_flowy/lib/workspace/presentation/widgets/edit_panel/panel_animation.dart @@ -79,9 +79,9 @@ extension AnimatedPanelExtensions on Widget { return AnimatedPanel( closedX: closePos.dx, closedY: closePos.dy, - child: this, isClosed: isClosed ?? false, duration: duration ?? .35, - curve: curve); + curve: curve, + child: this); } } diff --git a/frontend/app_flowy/lib/workspace/presentation/widgets/emoji_picker/src/default_emoji_picker_view.dart b/frontend/app_flowy/lib/workspace/presentation/widgets/emoji_picker/src/default_emoji_picker_view.dart index afdfa5e6eb..d6b605332f 100644 --- a/frontend/app_flowy/lib/workspace/presentation/widgets/emoji_picker/src/default_emoji_picker_view.dart +++ b/frontend/app_flowy/lib/workspace/presentation/widgets/emoji_picker/src/default_emoji_picker_view.dart @@ -199,8 +199,8 @@ class _DefaultEmojiPickerViewState extends State with Ti if (widget.config.buttonMode == ButtonMode.MATERIAL) { return TextButton( onPressed: onPressed, - child: child, style: ButtonStyle(padding: MaterialStateProperty.all(EdgeInsets.zero)), + child: child, ); } return CupertinoButton(padding: EdgeInsets.zero, onPressed: onPressed, child: child); diff --git a/frontend/app_flowy/lib/workspace/presentation/widgets/float_bubble/question_bubble.dart b/frontend/app_flowy/lib/workspace/presentation/widgets/float_bubble/question_bubble.dart index 6cc150489c..dbcafe2e44 100644 --- a/frontend/app_flowy/lib/workspace/presentation/widgets/float_bubble/question_bubble.dart +++ b/frontend/app_flowy/lib/workspace/presentation/widgets/float_bubble/question_bubble.dart @@ -87,7 +87,7 @@ class _DebugToast { return deviceInfo.then((info) { var debugText = ""; info.toMap().forEach((key, value) { - debugText = debugText + "$key: $value\n"; + debugText = "$debugText$key: $value\n"; }); return debugText; }); diff --git a/frontend/app_flowy/lib/workspace/presentation/widgets/pop_up_window.dart b/frontend/app_flowy/lib/workspace/presentation/widgets/pop_up_window.dart index 1803257672..4f30248ed2 100644 --- a/frontend/app_flowy/lib/workspace/presentation/widgets/pop_up_window.dart +++ b/frontend/app_flowy/lib/workspace/presentation/widgets/pop_up_window.dart @@ -10,8 +10,8 @@ class FlowyPoppuWindow extends StatelessWidget { @override Widget build(BuildContext context) { return Material( - child: child, type: MaterialType.transparency, + child: child, ); } diff --git a/frontend/app_flowy/pubspec.lock b/frontend/app_flowy/pubspec.lock index 199a41baa3..1279bec74b 100644 --- a/frontend/app_flowy/pubspec.lock +++ b/frontend/app_flowy/pubspec.lock @@ -7,14 +7,14 @@ packages: name: _fe_analyzer_shared url: "https://pub.dartlang.org" source: hosted - version: "44.0.0" + version: "46.0.0" analyzer: dependency: "direct overridden" description: name: analyzer url: "https://pub.dartlang.org" source: hosted - version: "4.4.0" + version: "4.6.0" animations: dependency: transitive description: @@ -182,7 +182,7 @@ packages: name: connectivity_plus url: "https://pub.dartlang.org" source: hosted - version: "2.2.0" + version: "2.3.6+1" connectivity_plus_linux: dependency: transitive description: @@ -196,7 +196,7 @@ packages: name: connectivity_plus_macos url: "https://pub.dartlang.org" source: hosted - version: "1.2.2" + version: "1.2.4" connectivity_plus_platform_interface: dependency: transitive description: @@ -210,14 +210,14 @@ packages: name: connectivity_plus_web url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.2.3" connectivity_plus_windows: dependency: transitive description: name: connectivity_plus_windows url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.2.2" convert: dependency: transitive description: diff --git a/frontend/app_flowy/pubspec.yaml b/frontend/app_flowy/pubspec.yaml index c81b96e156..7447ae999d 100644 --- a/frontend/app_flowy/pubspec.yaml +++ b/frontend/app_flowy/pubspec.yaml @@ -81,6 +81,7 @@ dependencies: reorderables: ^0.5.0 linked_scroll_controller: ^0.2.0 hotkey_manager: ^0.1.7 + fixnum: ^1.0.1 dev_dependencies: flutter_lints: ^2.0.1 From 410d150360d4f4df8078a1bc17735cb0512317b8 Mon Sep 17 00:00:00 2001 From: appflowy Date: Wed, 31 Aug 2022 09:28:08 +0800 Subject: [PATCH 086/106] chore: adjust flutter lint 2.0 --- .../app_flowy/lib/core/frameless_window.dart | 7 +- .../doc/presentation/style_widgets.dart | 13 ++-- .../presentation/toolbar/check_button.dart | 4 +- .../presentation/toolbar/color_picker.dart | 64 ++++++++++++------- .../presentation/toolbar/header_button.dart | 19 ++++-- .../doc/presentation/toolbar/link_button.dart | 8 ++- .../presentation/toolbar/toggle_button.dart | 8 ++- .../doc/presentation/toolbar/tool_bar.dart | 17 +++-- .../cell/select_option_editor_bloc.dart | 6 +- .../widgets/header/type_option/number.dart | 4 +- .../lib/user/presentation/router.dart | 19 ++++-- .../markdown/src/inline_parser.dart | 19 +++--- .../presentation/home/home_stack.dart | 4 +- .../widgets/edit_panel/panel_animation.dart | 4 +- .../src/default_emoji_picker_view.dart | 59 +++++++++++------ .../emoji_picker/src/emoji_button.dart | 4 +- .../emoji_picker/src/emoji_picker.dart | 62 ++++++++++++------ .../presentation/widgets/pop_up_window.dart | 6 +- frontend/app_flowy/pubspec.yaml | 7 ++ 19 files changed, 219 insertions(+), 115 deletions(-) diff --git a/frontend/app_flowy/lib/core/frameless_window.dart b/frontend/app_flowy/lib/core/frameless_window.dart index a7d6417cd3..3641aeef4d 100644 --- a/frontend/app_flowy/lib/core/frameless_window.dart +++ b/frontend/app_flowy/lib/core/frameless_window.dart @@ -31,10 +31,10 @@ class MoveWindowDetector extends StatefulWidget { final Widget? child; @override - _MoveWindowDetectorState createState() => _MoveWindowDetectorState(); + MoveWindowDetectorState createState() => MoveWindowDetectorState(); } -class _MoveWindowDetectorState extends State { +class MoveWindowDetectorState extends State { double winX = 0; double winY = 0; @@ -59,7 +59,8 @@ class _MoveWindowDetectorState extends State { final double dy = windowPos[1]; final deltaX = details.globalPosition.dx - winX; final deltaY = details.globalPosition.dy - winY; - await CocoaWindowChannel.instance.setWindowPosition(Offset(dx + deltaX, dy - deltaY)); + await CocoaWindowChannel.instance + .setWindowPosition(Offset(dx + deltaX, dy - deltaY)); }, child: widget.child, ); diff --git a/frontend/app_flowy/lib/plugins/doc/presentation/style_widgets.dart b/frontend/app_flowy/lib/plugins/doc/presentation/style_widgets.dart index 4f09b64053..8bbd5bc0d0 100644 --- a/frontend/app_flowy/lib/plugins/doc/presentation/style_widgets.dart +++ b/frontend/app_flowy/lib/plugins/doc/presentation/style_widgets.dart @@ -16,7 +16,10 @@ class EditorCheckboxBuilder extends QuillCheckboxBuilder { EditorCheckboxBuilder(this.theme); @override - Widget build({required BuildContext context, required bool isChecked, required ValueChanged onChanged}) { + Widget build( + {required BuildContext context, + required bool isChecked, + required ValueChanged onChanged}) { return FlowyEditorCheckbox( theme: theme, isChecked: isChecked, @@ -37,10 +40,10 @@ class FlowyEditorCheckbox extends StatefulWidget { }) : super(key: key); @override - _FlowyEditorCheckboxState createState() => _FlowyEditorCheckboxState(); + FlowyEditorCheckboxState createState() => FlowyEditorCheckboxState(); } -class _FlowyEditorCheckboxState extends State { +class FlowyEditorCheckboxState extends State { late bool isChecked; @override @@ -51,7 +54,9 @@ class _FlowyEditorCheckboxState extends State { @override Widget build(BuildContext context) { - final icon = isChecked ? svgWidget('editor/editor_check') : svgWidget('editor/editor_uncheck'); + final icon = isChecked + ? svgWidget('editor/editor_check') + : svgWidget('editor/editor_uncheck'); return Align( alignment: Alignment.centerLeft, child: FlowyIconButton( diff --git a/frontend/app_flowy/lib/plugins/doc/presentation/toolbar/check_button.dart b/frontend/app_flowy/lib/plugins/doc/presentation/toolbar/check_button.dart index 55f17ac558..280209c64a 100644 --- a/frontend/app_flowy/lib/plugins/doc/presentation/toolbar/check_button.dart +++ b/frontend/app_flowy/lib/plugins/doc/presentation/toolbar/check_button.dart @@ -28,10 +28,10 @@ class FlowyCheckListButton extends StatefulWidget { final String tooltipText; @override - _FlowyCheckListButtonState createState() => _FlowyCheckListButtonState(); + FlowyCheckListButtonState createState() => FlowyCheckListButtonState(); } -class _FlowyCheckListButtonState extends State { +class FlowyCheckListButtonState extends State { bool? _isToggled; Style get _selectionStyle => widget.controller.getSelectionStyle(); diff --git a/frontend/app_flowy/lib/plugins/doc/presentation/toolbar/color_picker.dart b/frontend/app_flowy/lib/plugins/doc/presentation/toolbar/color_picker.dart index 57299bd6cc..1f262483a8 100644 --- a/frontend/app_flowy/lib/plugins/doc/presentation/toolbar/color_picker.dart +++ b/frontend/app_flowy/lib/plugins/doc/presentation/toolbar/color_picker.dart @@ -24,10 +24,10 @@ class FlowyColorButton extends StatefulWidget { final QuillIconTheme? iconTheme; @override - _FlowyColorButtonState createState() => _FlowyColorButtonState(); + FlowyColorButtonState createState() => FlowyColorButtonState(); } -class _FlowyColorButtonState extends State { +class FlowyColorButtonState extends State { late bool _isToggledColor; late bool _isToggledBackground; late bool _isWhite; @@ -37,10 +37,14 @@ class _FlowyColorButtonState extends State { void _didChangeEditingValue() { setState(() { - _isToggledColor = _getIsToggledColor(widget.controller.getSelectionStyle().attributes); - _isToggledBackground = _getIsToggledBackground(widget.controller.getSelectionStyle().attributes); - _isWhite = _isToggledColor && _selectionStyle.attributes['color']!.value == '#ffffff'; - _isWhitebackground = _isToggledBackground && _selectionStyle.attributes['background']!.value == '#ffffff'; + _isToggledColor = + _getIsToggledColor(widget.controller.getSelectionStyle().attributes); + _isToggledBackground = _getIsToggledBackground( + widget.controller.getSelectionStyle().attributes); + _isWhite = _isToggledColor && + _selectionStyle.attributes['color']!.value == '#ffffff'; + _isWhitebackground = _isToggledBackground && + _selectionStyle.attributes['background']!.value == '#ffffff'; }); } @@ -49,8 +53,10 @@ class _FlowyColorButtonState extends State { super.initState(); _isToggledColor = _getIsToggledColor(_selectionStyle.attributes); _isToggledBackground = _getIsToggledBackground(_selectionStyle.attributes); - _isWhite = _isToggledColor && _selectionStyle.attributes['color']!.value == '#ffffff'; - _isWhitebackground = _isToggledBackground && _selectionStyle.attributes['background']!.value == '#ffffff'; + _isWhite = _isToggledColor && + _selectionStyle.attributes['color']!.value == '#ffffff'; + _isWhitebackground = _isToggledBackground && + _selectionStyle.attributes['background']!.value == '#ffffff'; widget.controller.addListener(_didChangeEditingValue); } @@ -69,9 +75,12 @@ class _FlowyColorButtonState extends State { oldWidget.controller.removeListener(_didChangeEditingValue); widget.controller.addListener(_didChangeEditingValue); _isToggledColor = _getIsToggledColor(_selectionStyle.attributes); - _isToggledBackground = _getIsToggledBackground(_selectionStyle.attributes); - _isWhite = _isToggledColor && _selectionStyle.attributes['color']!.value == '#ffffff'; - _isWhitebackground = _isToggledBackground && _selectionStyle.attributes['background']!.value == '#ffffff'; + _isToggledBackground = + _getIsToggledBackground(_selectionStyle.attributes); + _isWhite = _isToggledColor && + _selectionStyle.attributes['color']!.value == '#ffffff'; + _isWhitebackground = _isToggledBackground && + _selectionStyle.attributes['background']!.value == '#ffffff'; } } @@ -88,9 +97,10 @@ class _FlowyColorButtonState extends State { final fillColor = _isToggledColor && !widget.background && _isWhite ? stringToColor('#ffffff') : (widget.iconTheme?.iconUnselectedFillColor ?? theme.canvasColor); - final fillColorBackground = _isToggledBackground && widget.background && _isWhitebackground - ? stringToColor('#ffffff') - : (widget.iconTheme?.iconUnselectedFillColor ?? theme.canvasColor); + final fillColorBackground = + _isToggledBackground && widget.background && _isWhitebackground + ? stringToColor('#ffffff') + : (widget.iconTheme?.iconUnselectedFillColor ?? theme.canvasColor); return Tooltip( message: LocaleKeys.toolbar_highlight.tr(), @@ -99,7 +109,8 @@ class _FlowyColorButtonState extends State { highlightElevation: 0, hoverElevation: 0, size: widget.iconSize * kIconButtonFactor, - icon: Icon(widget.icon, size: widget.iconSize, color: theme.iconTheme.color), + icon: Icon(widget.icon, + size: widget.iconSize, color: theme.iconTheme.color), fillColor: widget.background ? fillColorBackground : fillColor, onPressed: _showColorPicker, ), @@ -112,13 +123,16 @@ class _FlowyColorButtonState extends State { hex = hex.substring(2); } hex = '#$hex'; - widget.controller.formatSelection(widget.background ? BackgroundAttribute(hex) : ColorAttribute(hex)); + widget.controller.formatSelection( + widget.background ? BackgroundAttribute(hex) : ColorAttribute(hex)); Navigator.of(context).pop(); } void _showColorPicker() { final style = widget.controller.getSelectionStyle(); - final values = style.values.where((v) => v.key == Attribute.background.key).map((v) => v.value); + final values = style.values + .where((v) => v.key == Attribute.background.key) + .map((v) => v.value); int initialColor = 0; if (values.isNotEmpty) { assert(values.length == 1); @@ -160,7 +174,9 @@ class FlowyColorPicker extends StatefulWidget { ]; final Function(Color?) onColorChanged; final int initialColor; - FlowyColorPicker({Key? key, required this.onColorChanged, this.initialColor = 0}) : super(key: key); + FlowyColorPicker( + {Key? key, required this.onColorChanged, this.initialColor = 0}) + : super(key: key); @override State createState() => _FlowyColorPickerState(); @@ -178,8 +194,10 @@ class _FlowyColorPickerState extends State { const double crossAxisSpacing = 10; final numberOfRows = (widget.colors.length / crossAxisCount).ceil(); - const perRowHeight = ((width - ((crossAxisCount - 1) * mainAxisSpacing)) / crossAxisCount); - final totalHeight = numberOfRows * perRowHeight + numberOfRows * crossAxisSpacing; + const perRowHeight = + ((width - ((crossAxisCount - 1) * mainAxisSpacing)) / crossAxisCount); + final totalHeight = + numberOfRows * perRowHeight + numberOfRows * crossAxisSpacing; return Container( constraints: BoxConstraints.tightFor(width: width, height: totalHeight), @@ -198,7 +216,8 @@ class _FlowyColorPickerState extends State { delegate: SliverChildBuilderDelegate( (BuildContext context, int index) { if (widget.colors.length > index) { - final isSelected = widget.colors[index] == widget.initialColor; + final isSelected = + widget.colors[index] == widget.initialColor; return ColorItem( color: Color(widget.colors[index]), onPressed: widget.onColorChanged, @@ -242,7 +261,8 @@ class ColorItem extends StatelessWidget { ); } else { return RawMaterialButton( - shape: const CircleBorder(side: BorderSide(color: Colors.white, width: 8)) + + shape: const CircleBorder( + side: BorderSide(color: Colors.white, width: 8)) + CircleBorder(side: BorderSide(color: color, width: 4)), onPressed: () { if (isSelected) { diff --git a/frontend/app_flowy/lib/plugins/doc/presentation/toolbar/header_button.dart b/frontend/app_flowy/lib/plugins/doc/presentation/toolbar/header_button.dart index 2da700df47..2db98b5af0 100644 --- a/frontend/app_flowy/lib/plugins/doc/presentation/toolbar/header_button.dart +++ b/frontend/app_flowy/lib/plugins/doc/presentation/toolbar/header_button.dart @@ -16,10 +16,10 @@ class FlowyHeaderStyleButton extends StatefulWidget { final double iconSize; @override - _FlowyHeaderStyleButtonState createState() => _FlowyHeaderStyleButtonState(); + FlowyHeaderStyleButtonState createState() => FlowyHeaderStyleButtonState(); } -class _FlowyHeaderStyleButtonState extends State { +class FlowyHeaderStyleButtonState extends State { Attribute? _value; Style get _selectionStyle => widget.controller.getSelectionStyle(); @@ -28,7 +28,8 @@ class _FlowyHeaderStyleButtonState extends State { void initState() { super.initState(); setState(() { - _value = _selectionStyle.attributes[Attribute.header.key] ?? Attribute.header; + _value = + _selectionStyle.attributes[Attribute.header.key] ?? Attribute.header; }); widget.controller.addListener(_didChangeEditingValue); } @@ -41,7 +42,11 @@ class _FlowyHeaderStyleButtonState extends State { Attribute.h3: 'H3', }; - final valueAttribute = [Attribute.h1, Attribute.h2, Attribute.h3]; + final valueAttribute = [ + Attribute.h1, + Attribute.h2, + Attribute.h3 + ]; final valueString = ['H1', 'H2', 'H3']; final attributeImageName = ['editor/H1', 'editor/H2', 'editor/H3']; @@ -72,7 +77,8 @@ class _FlowyHeaderStyleButtonState extends State { void _didChangeEditingValue() { setState(() { - _value = _selectionStyle.attributes[Attribute.header.key] ?? Attribute.header; + _value = + _selectionStyle.attributes[Attribute.header.key] ?? Attribute.header; }); } @@ -82,7 +88,8 @@ class _FlowyHeaderStyleButtonState extends State { if (oldWidget.controller != widget.controller) { oldWidget.controller.removeListener(_didChangeEditingValue); widget.controller.addListener(_didChangeEditingValue); - _value = _selectionStyle.attributes[Attribute.header.key] ?? Attribute.header; + _value = + _selectionStyle.attributes[Attribute.header.key] ?? Attribute.header; } } diff --git a/frontend/app_flowy/lib/plugins/doc/presentation/toolbar/link_button.dart b/frontend/app_flowy/lib/plugins/doc/presentation/toolbar/link_button.dart index 60b654302f..428c45e400 100644 --- a/frontend/app_flowy/lib/plugins/doc/presentation/toolbar/link_button.dart +++ b/frontend/app_flowy/lib/plugins/doc/presentation/toolbar/link_button.dart @@ -19,10 +19,10 @@ class FlowyLinkStyleButton extends StatefulWidget { final double iconSize; @override - _FlowyLinkStyleButtonState createState() => _FlowyLinkStyleButtonState(); + FlowyLinkStyleButtonState createState() => FlowyLinkStyleButtonState(); } -class _FlowyLinkStyleButtonState extends State { +class FlowyLinkStyleButtonState extends State { void _didChangeSelection() { setState(() {}); } @@ -75,7 +75,9 @@ class _FlowyLinkStyleButtonState extends State { void _openLinkDialog(BuildContext context) { final style = widget.controller.getSelectionStyle(); - final values = style.values.where((v) => v.key == Attribute.link.key).map((v) => v.value); + final values = style.values + .where((v) => v.key == Attribute.link.key) + .map((v) => v.value); String value = ""; if (values.isNotEmpty) { assert(values.length == 1); diff --git a/frontend/app_flowy/lib/plugins/doc/presentation/toolbar/toggle_button.dart b/frontend/app_flowy/lib/plugins/doc/presentation/toolbar/toggle_button.dart index 89fd72981d..2ecb98c3ea 100644 --- a/frontend/app_flowy/lib/plugins/doc/presentation/toolbar/toggle_button.dart +++ b/frontend/app_flowy/lib/plugins/doc/presentation/toolbar/toggle_button.dart @@ -21,10 +21,10 @@ class FlowyToggleStyleButton extends StatefulWidget { }) : super(key: key); @override - _ToggleStyleButtonState createState() => _ToggleStyleButtonState(); + ToggleStyleButtonState createState() => ToggleStyleButtonState(); } -class _ToggleStyleButtonState extends State { +class ToggleStyleButtonState extends State { bool? _isToggled; Style get _selectionStyle => widget.controller.getSelectionStyle(); @override @@ -77,6 +77,8 @@ class _ToggleStyleButtonState extends State { } void _toggleAttribute() { - widget.controller.formatSelection(_isToggled! ? Attribute.clone(widget.attribute, null) : widget.attribute); + widget.controller.formatSelection(_isToggled! + ? Attribute.clone(widget.attribute, null) + : widget.attribute); } } diff --git a/frontend/app_flowy/lib/plugins/doc/presentation/toolbar/tool_bar.dart b/frontend/app_flowy/lib/plugins/doc/presentation/toolbar/tool_bar.dart index 8dae41f986..d649340066 100644 --- a/frontend/app_flowy/lib/plugins/doc/presentation/toolbar/tool_bar.dart +++ b/frontend/app_flowy/lib/plugins/doc/presentation/toolbar/tool_bar.dart @@ -32,7 +32,8 @@ class EditorToolbar extends StatelessWidget implements PreferredSizeWidget { return Container( color: Theme.of(context).canvasColor, constraints: BoxConstraints.tightFor(height: preferredSize.height), - child: ToolbarButtonList(buttons: children).padding(horizontal: 4, vertical: 4), + child: ToolbarButtonList(buttons: children) + .padding(horizontal: 4, vertical: 4), ); } @@ -168,10 +169,11 @@ class ToolbarButtonList extends StatefulWidget { final List buttons; @override - _ToolbarButtonListState createState() => _ToolbarButtonListState(); + ToolbarButtonListState createState() => ToolbarButtonListState(); } -class _ToolbarButtonListState extends State with WidgetsBindingObserver { +class ToolbarButtonListState extends State + with WidgetsBindingObserver { final ScrollController _controller = ScrollController(); bool _showLeftArrow = false; bool _showRightArrow = false; @@ -196,7 +198,8 @@ class _ToolbarButtonListState extends State with WidgetsBindi return LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { List children = []; - double width = (widget.buttons.length + 2) * defaultIconSize * kIconButtonFactor; + double width = + (widget.buttons.length + 2) * defaultIconSize * kIconButtonFactor; final isFit = constraints.maxWidth > width; if (!isFit) { children.add(_buildLeftArrow()); @@ -233,8 +236,10 @@ class _ToolbarButtonListState extends State with WidgetsBindi void _handleScroll() { if (!mounted) return; setState(() { - _showLeftArrow = _controller.position.minScrollExtent != _controller.position.pixels; - _showRightArrow = _controller.position.maxScrollExtent != _controller.position.pixels; + _showLeftArrow = + _controller.position.minScrollExtent != _controller.position.pixels; + _showRightArrow = + _controller.position.maxScrollExtent != _controller.position.pixels; }); } diff --git a/frontend/app_flowy/lib/plugins/grid/application/cell/select_option_editor_bloc.dart b/frontend/app_flowy/lib/plugins/grid/application/cell/select_option_editor_bloc.dart index 8d52252e2a..349d95d13f 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/cell/select_option_editor_bloc.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/cell/select_option_editor_bloc.dart @@ -1,12 +1,14 @@ import 'dart:async'; + +import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart'; +import 'package:collection/collection.dart'; import 'package:dartz/dartz.dart'; import 'package:flowy_sdk/log.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/select_option.pb.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart'; + import 'select_option_service.dart'; -import 'package:collection/collection.dart'; part 'select_option_editor_bloc.freezed.dart'; diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/number.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/number.dart index d15be4a6a0..590a025a41 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/number.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/number.dart @@ -90,10 +90,10 @@ class NumberTypeOptionWidget extends TypeOptionWidget { } } -typedef _SelectNumberFormatCallback = Function(NumberFormat format); +typedef SelectNumberFormatCallback = Function(NumberFormat format); class NumberFormatList extends StatelessWidget { - final _SelectNumberFormatCallback onSelected; + final SelectNumberFormatCallback onSelected; final NumberFormat selectedFormat; const NumberFormatList( {required this.selectedFormat, required this.onSelected, Key? key}) diff --git a/frontend/app_flowy/lib/user/presentation/router.dart b/frontend/app_flowy/lib/user/presentation/router.dart index 2928154ebe..82ff46ada9 100644 --- a/frontend/app_flowy/lib/user/presentation/router.dart +++ b/frontend/app_flowy/lib/user/presentation/router.dart @@ -28,16 +28,19 @@ class AuthRouter { ); } - void pushHomeScreen(BuildContext context, UserProfilePB profile, CurrentWorkspaceSettingPB workspaceSetting) { + void pushHomeScreen(BuildContext context, UserProfilePB profile, + CurrentWorkspaceSettingPB workspaceSetting) { Navigator.push( context, - PageRoutes.fade(() => HomeScreen(profile, workspaceSetting), RouteDurations.slow.inMilliseconds * .001), + PageRoutes.fade(() => HomeScreen(profile, workspaceSetting), + RouteDurations.slow.inMilliseconds * .001), ); } } class SplashRoute { - Future pushWelcomeScreen(BuildContext context, UserProfilePB userProfile) async { + Future pushWelcomeScreen( + BuildContext context, UserProfilePB userProfile) async { final screen = WelcomeScreen(userProfile: userProfile); final workspaceId = await Navigator.of(context).push( PageRoutes.fade( @@ -46,20 +49,24 @@ class SplashRoute { ), ); + // ignore: use_build_context_synchronously pushHomeScreen(context, userProfile, workspaceId); } - void pushHomeScreen(BuildContext context, UserProfilePB userProfile, CurrentWorkspaceSettingPB workspaceSetting) { + void pushHomeScreen(BuildContext context, UserProfilePB userProfile, + CurrentWorkspaceSettingPB workspaceSetting) { Navigator.push( context, - PageRoutes.fade(() => HomeScreen(userProfile, workspaceSetting), RouteDurations.slow.inMilliseconds * .001), + PageRoutes.fade(() => HomeScreen(userProfile, workspaceSetting), + RouteDurations.slow.inMilliseconds * .001), ); } void pushSignInScreen(BuildContext context) { Navigator.push( context, - PageRoutes.fade(() => SignInScreen(router: getIt()), RouteDurations.slow.inMilliseconds * .001), + PageRoutes.fade(() => SignInScreen(router: getIt()), + RouteDurations.slow.inMilliseconds * .001), ); } diff --git a/frontend/app_flowy/lib/workspace/application/markdown/src/inline_parser.dart b/frontend/app_flowy/lib/workspace/application/markdown/src/inline_parser.dart index 8b47a97433..ce0f11302e 100644 --- a/frontend/app_flowy/lib/workspace/application/markdown/src/inline_parser.dart +++ b/frontend/app_flowy/lib/workspace/application/markdown/src/inline_parser.dart @@ -399,8 +399,8 @@ class AutolinkExtensionSyntax extends InlineSyntax { } } -class _DelimiterRun { - _DelimiterRun._( +class DelimiterRun { + DelimiterRun._( {this.char, this.length, this.isLeftFlanking, @@ -420,8 +420,7 @@ class _DelimiterRun { final bool? isFollowedByPunctuation; // ignore: prefer_constructors_over_static_methods - static _DelimiterRun? tryParse( - InlineParser parser, int runStart, int runEnd) { + static DelimiterRun? tryParse(InlineParser parser, int runStart, int runEnd) { bool leftFlanking, rightFlanking, precededByPunctuation, @@ -466,7 +465,7 @@ class _DelimiterRun { return null; } - return _DelimiterRun._( + return DelimiterRun._( char: parser.charAt(runStart), length: runEnd - runStart + 1, isLeftFlanking: leftFlanking, @@ -516,7 +515,7 @@ class TagSyntax extends InlineSyntax { return true; } - final delimiterRun = _DelimiterRun.tryParse(parser, matchStart, matchEnd); + final delimiterRun = DelimiterRun.tryParse(parser, matchStart, matchEnd); if (delimiterRun != null && delimiterRun.canOpen) { parser.openTag(TagState(parser.pos, matchEnd + 1, this, delimiterRun)); return true; @@ -531,7 +530,7 @@ class TagSyntax extends InlineSyntax { final matchStart = parser.pos; final matchEnd = parser.pos + runLength - 1; final openingRunLength = state.endPos - state.startPos; - final delimiterRun = _DelimiterRun.tryParse(parser, matchStart, matchEnd); + final delimiterRun = DelimiterRun.tryParse(parser, matchStart, matchEnd); if (openingRunLength == 1 && runLength == 1) { parser.addNode(Element('em', state.children)); @@ -579,7 +578,7 @@ class StrikethroughSyntax extends TagSyntax { final runLength = match.group(0)!.length; final matchStart = parser.pos; final matchEnd = parser.pos + runLength - 1; - final delimiterRun = _DelimiterRun.tryParse(parser, matchStart, matchEnd)!; + final delimiterRun = DelimiterRun.tryParse(parser, matchStart, matchEnd)!; if (!delimiterRun.isRightFlanking!) { return false; } @@ -1170,7 +1169,7 @@ class TagState { /// The children of this node. Will be `null` for text nodes. final List children; - final _DelimiterRun? openingDelimiterRun; + final DelimiterRun? openingDelimiterRun; /// Attempts to close this tag by matching the current text against its end /// pattern. @@ -1193,7 +1192,7 @@ class TagState { final closingMatchStart = parser.pos; final closingMatchEnd = parser.pos + runLength - 1; final closingDelimiterRun = - _DelimiterRun.tryParse(parser, closingMatchStart, closingMatchEnd); + DelimiterRun.tryParse(parser, closingMatchStart, closingMatchEnd); if (closingDelimiterRun != null && closingDelimiterRun.canClose) { // Emphasis rules #9 and #10: final oneRunOpensAndCloses = diff --git a/frontend/app_flowy/lib/workspace/presentation/home/home_stack.dart b/frontend/app_flowy/lib/workspace/presentation/home/home_stack.dart index 5099dcddfc..eed9eb6f4a 100644 --- a/frontend/app_flowy/lib/workspace/presentation/home/home_stack.dart +++ b/frontend/app_flowy/lib/workspace/presentation/home/home_stack.dart @@ -58,10 +58,10 @@ class FadingIndexedStack extends StatefulWidget { }) : super(key: key); @override - _FadingIndexedStackState createState() => _FadingIndexedStackState(); + FadingIndexedStackState createState() => FadingIndexedStackState(); } -class _FadingIndexedStackState extends State { +class FadingIndexedStackState extends State { double _targetOpacity = 1; @override diff --git a/frontend/app_flowy/lib/workspace/presentation/widgets/edit_panel/panel_animation.dart b/frontend/app_flowy/lib/workspace/presentation/widgets/edit_panel/panel_animation.dart index e88c6b09b6..614ff356cb 100644 --- a/frontend/app_flowy/lib/workspace/presentation/widgets/edit_panel/panel_animation.dart +++ b/frontend/app_flowy/lib/workspace/presentation/widgets/edit_panel/panel_animation.dart @@ -19,10 +19,10 @@ class AnimatedPanel extends StatefulWidget { : super(key: key); @override - _AnimatedPanelState createState() => _AnimatedPanelState(); + AnimatedPanelState createState() => AnimatedPanelState(); } -class _AnimatedPanelState extends State { +class AnimatedPanelState extends State { bool _isHidden = true; @override diff --git a/frontend/app_flowy/lib/workspace/presentation/widgets/emoji_picker/src/default_emoji_picker_view.dart b/frontend/app_flowy/lib/workspace/presentation/widgets/emoji_picker/src/default_emoji_picker_view.dart index d6b605332f..271a3b6dd2 100644 --- a/frontend/app_flowy/lib/workspace/presentation/widgets/emoji_picker/src/default_emoji_picker_view.dart +++ b/frontend/app_flowy/lib/workspace/presentation/widgets/emoji_picker/src/default_emoji_picker_view.dart @@ -10,28 +10,34 @@ import 'emoji_picker_builder.dart'; import 'emoji_view_state.dart'; class DefaultEmojiPickerView extends EmojiPickerBuilder { - const DefaultEmojiPickerView(Config config, EmojiViewState state, {Key? key}) : super(config, state, key: key); + const DefaultEmojiPickerView(Config config, EmojiViewState state, {Key? key}) + : super(config, state, key: key); @override - _DefaultEmojiPickerViewState createState() => _DefaultEmojiPickerViewState(); + DefaultEmojiPickerViewState createState() => DefaultEmojiPickerViewState(); } -class _DefaultEmojiPickerViewState extends State with TickerProviderStateMixin { +class DefaultEmojiPickerViewState extends State + with TickerProviderStateMixin { PageController? _pageController; TabController? _tabController; final TextEditingController _emojiController = TextEditingController(); final FocusNode _emojiFocusNode = FocusNode(); - final CategoryEmoji _categoryEmoji = CategoryEmoji(Category.SEARCH, List.empty(growable: true)); + final CategoryEmoji _categoryEmoji = + CategoryEmoji(Category.SEARCH, List.empty(growable: true)); CategoryEmoji searchEmojiList = CategoryEmoji(Category.SEARCH, []); @override void initState() { - var initCategory = - widget.state.categoryEmoji.indexWhere((element) => element.category == widget.config.initCategory); + var initCategory = widget.state.categoryEmoji.indexWhere( + (element) => element.category == widget.config.initCategory); if (initCategory == -1) { initCategory = 0; } - _tabController = TabController(initialIndex: initCategory, length: widget.state.categoryEmoji.length, vsync: this); + _tabController = TabController( + initialIndex: initCategory, + length: widget.state.categoryEmoji.length, + vsync: this); _pageController = PageController(initialPage: initCategory); _emojiFocusNode.requestFocus(); @@ -83,7 +89,8 @@ class _DefaultEmojiPickerViewState extends State with Ti } bool isEmojiSearching() { - bool result = searchEmojiList.emoji.isNotEmpty || _emojiController.text.isNotEmpty; + bool result = + searchEmojiList.emoji.isNotEmpty || _emojiController.text.isNotEmpty; return result; } @@ -133,7 +140,9 @@ class _DefaultEmojiPickerViewState extends State with Ti child: TabBar( labelColor: widget.config.iconColorSelected, unselectedLabelColor: widget.config.iconColor, - controller: isEmojiSearching() ? TabController(length: 1, vsync: this) : _tabController, + controller: isEmojiSearching() + ? TabController(length: 1, vsync: this) + : _tabController, labelPadding: EdgeInsets.zero, indicatorColor: widget.config.indicatorColor, padding: const EdgeInsets.symmetric(vertical: 5.0), @@ -154,7 +163,8 @@ class _DefaultEmojiPickerViewState extends State with Ti : widget.state.categoryEmoji .asMap() .entries - .map((item) => _buildCategory(item.value.category, emojiSize)) + .map((item) => _buildCategory( + item.value.category, emojiSize)) .toList(), ), ), @@ -163,7 +173,9 @@ class _DefaultEmojiPickerViewState extends State with Ti ), Flexible( child: PageView.builder( - itemCount: searchEmojiList.emoji.isNotEmpty ? 1 : widget.state.categoryEmoji.length, + itemCount: searchEmojiList.emoji.isNotEmpty + ? 1 + : widget.state.categoryEmoji.length, controller: _pageController, physics: const NeverScrollableScrollPhysics(), // onPageChanged: (index) { @@ -173,7 +185,9 @@ class _DefaultEmojiPickerViewState extends State with Ti // ); // }, itemBuilder: (context, index) { - CategoryEmoji catEmoji = isEmojiSearching() ? searchEmojiList : widget.state.categoryEmoji[index]; + CategoryEmoji catEmoji = isEmojiSearching() + ? searchEmojiList + : widget.state.categoryEmoji[index]; return _buildPage(emojiSize, catEmoji); }, ), @@ -195,7 +209,8 @@ class _DefaultEmojiPickerViewState extends State with Ti ); } - Widget _buildButtonWidget({required VoidCallback onPressed, required Widget child}) { + Widget _buildButtonWidget( + {required VoidCallback onPressed, required Widget child}) { if (widget.config.buttonMode == ButtonMode.MATERIAL) { return TextButton( onPressed: onPressed, @@ -203,16 +218,19 @@ class _DefaultEmojiPickerViewState extends State with Ti child: child, ); } - return CupertinoButton(padding: EdgeInsets.zero, onPressed: onPressed, child: child); + return CupertinoButton( + padding: EdgeInsets.zero, onPressed: onPressed, child: child); } Widget _buildPage(double emojiSize, CategoryEmoji categoryEmoji) { // Display notice if recent has no entries yet final scrollController = ScrollController(); - if (categoryEmoji.category == Category.RECENT && categoryEmoji.emoji.isEmpty) { + if (categoryEmoji.category == Category.RECENT && + categoryEmoji.emoji.isEmpty) { return _buildNoRecent(); - } else if (categoryEmoji.category == Category.SEARCH && categoryEmoji.emoji.isEmpty) { + } else if (categoryEmoji.category == Category.SEARCH && + categoryEmoji.emoji.isEmpty) { return const Center(child: Text("No Emoji Found")); } // Build page normally @@ -236,8 +254,13 @@ class _DefaultEmojiPickerViewState extends State with Ti mainAxisSpacing: widget.config.verticalSpacing, crossAxisSpacing: widget.config.horizontalSpacing, children: _categoryEmoji.emoji.isNotEmpty - ? _categoryEmoji.emoji.map((e) => _buildEmoji(emojiSize, categoryEmoji, e)).toList() - : categoryEmoji.emoji.map((item) => _buildEmoji(emojiSize, categoryEmoji, item)).toList(), + ? _categoryEmoji.emoji + .map((e) => _buildEmoji(emojiSize, categoryEmoji, e)) + .toList() + : categoryEmoji.emoji + .map( + (item) => _buildEmoji(emojiSize, categoryEmoji, item)) + .toList(), ), ), ); diff --git a/frontend/app_flowy/lib/workspace/presentation/widgets/emoji_picker/src/emoji_button.dart b/frontend/app_flowy/lib/workspace/presentation/widgets/emoji_picker/src/emoji_button.dart index 37ce933618..28cf268a46 100644 --- a/frontend/app_flowy/lib/workspace/presentation/widgets/emoji_picker/src/emoji_button.dart +++ b/frontend/app_flowy/lib/workspace/presentation/widgets/emoji_picker/src/emoji_button.dart @@ -20,10 +20,10 @@ class FlowyEmojiStyleButton extends StatefulWidget { }) : super(key: key); @override - _EmojiStyleButtonState createState() => _EmojiStyleButtonState(); + EmojiStyleButtonState createState() => EmojiStyleButtonState(); } -class _EmojiStyleButtonState extends State { +class EmojiStyleButtonState extends State { bool _isToggled = false; // Style get _selectionStyle => widget.controller.getSelectionStyle(); final GlobalKey emojiButtonKey = GlobalKey(); diff --git a/frontend/app_flowy/lib/workspace/presentation/widgets/emoji_picker/src/emoji_picker.dart b/frontend/app_flowy/lib/workspace/presentation/widgets/emoji_picker/src/emoji_picker.dart index 650cd185f1..8852b12799 100644 --- a/frontend/app_flowy/lib/workspace/presentation/widgets/emoji_picker/src/emoji_picker.dart +++ b/frontend/app_flowy/lib/workspace/presentation/widgets/emoji_picker/src/emoji_picker.dart @@ -101,10 +101,10 @@ class EmojiPicker extends StatefulWidget { final Config config; @override - _EmojiPickerState createState() => _EmojiPickerState(); + EmojiPickerState createState() => EmojiPickerState(); } -class _EmojiPickerState extends State { +class EmojiPickerState extends State { static const platform = MethodChannel('emoji_picker_flutter'); List categoryEmoji = List.empty(growable: true); @@ -147,7 +147,8 @@ class _EmojiPickerState extends State { return const Center(child: CircularProgressIndicator()); } if (widget.config.showRecentsTab) { - categoryEmoji[0].emoji = recentEmoji.map((e) => e.emoji).toList().cast(); + categoryEmoji[0].emoji = + recentEmoji.map((e) => e.emoji).toList().cast(); } var state = EmojiViewState( @@ -184,23 +185,35 @@ class _EmojiPickerState extends State { categoryEmoji.clear(); if (widget.config.showRecentsTab) { recentEmoji = await _getRecentEmojis(); - final List recentEmojiMap = recentEmoji.map((e) => e.emoji).toList().cast(); + final List recentEmojiMap = + recentEmoji.map((e) => e.emoji).toList().cast(); categoryEmoji.add(CategoryEmoji(Category.RECENT, recentEmojiMap)); } categoryEmoji.addAll([ - CategoryEmoji(Category.SMILEYS, await _getAvailableEmojis(emoji_list.smileys, title: 'smileys')), - CategoryEmoji(Category.ANIMALS, await _getAvailableEmojis(emoji_list.animals, title: 'animals')), - CategoryEmoji(Category.FOODS, await _getAvailableEmojis(emoji_list.foods, title: 'foods')), - CategoryEmoji(Category.ACTIVITIES, await _getAvailableEmojis(emoji_list.activities, title: 'activities')), - CategoryEmoji(Category.TRAVEL, await _getAvailableEmojis(emoji_list.travel, title: 'travel')), - CategoryEmoji(Category.OBJECTS, await _getAvailableEmojis(emoji_list.objects, title: 'objects')), - CategoryEmoji(Category.SYMBOLS, await _getAvailableEmojis(emoji_list.symbols, title: 'symbols')), - CategoryEmoji(Category.FLAGS, await _getAvailableEmojis(emoji_list.flags, title: 'flags')) + CategoryEmoji(Category.SMILEYS, + await _getAvailableEmojis(emoji_list.smileys, title: 'smileys')), + CategoryEmoji(Category.ANIMALS, + await _getAvailableEmojis(emoji_list.animals, title: 'animals')), + CategoryEmoji(Category.FOODS, + await _getAvailableEmojis(emoji_list.foods, title: 'foods')), + CategoryEmoji( + Category.ACTIVITIES, + await _getAvailableEmojis(emoji_list.activities, + title: 'activities')), + CategoryEmoji(Category.TRAVEL, + await _getAvailableEmojis(emoji_list.travel, title: 'travel')), + CategoryEmoji(Category.OBJECTS, + await _getAvailableEmojis(emoji_list.objects, title: 'objects')), + CategoryEmoji(Category.SYMBOLS, + await _getAvailableEmojis(emoji_list.symbols, title: 'symbols')), + CategoryEmoji(Category.FLAGS, + await _getAvailableEmojis(emoji_list.flags, title: 'flags')) ]); } // Get available emoji for given category title - Future> _getAvailableEmojis(Map map, {required String title}) async { + Future> _getAvailableEmojis(Map map, + {required String title}) async { Map? newMap; // Get Emojis cached locally if available @@ -216,19 +229,22 @@ class _EmojiPickerState extends State { } // Map to Emoji Object - return newMap!.entries.map((entry) => Emoji(entry.key, entry.value)).toList(); + return newMap!.entries + .map((entry) => Emoji(entry.key, entry.value)) + .toList(); } // Check if emoji is available on current platform - Future?> _getPlatformAvailableEmoji(Map emoji) async { + Future?> _getPlatformAvailableEmoji( + Map emoji) async { if (Platform.isAndroid) { Map? filtered = {}; var delimiter = '|'; try { var entries = emoji.values.join(delimiter); var keys = emoji.keys.join(delimiter); - var result = (await platform - .invokeMethod('checkAvailability', {'emojiKeys': keys, 'emojiEntries': entries})) as String; + var result = (await platform.invokeMethod('checkAvailability', + {'emojiKeys': keys, 'emojiEntries': entries})) as String; var resultKeys = result.split(delimiter); for (var i = 0; i < resultKeys.length; i++) { filtered[resultKeys[i]] = emoji[resultKeys[i]]!; @@ -249,12 +265,14 @@ class _EmojiPickerState extends State { if (emojiJson == null) { return null; } - var emojis = Map.from(jsonDecode(emojiJson) as Map); + var emojis = + Map.from(jsonDecode(emojiJson) as Map); return emojis; } // Stores filtered emoji locally for faster access next time - Future _cacheFilteredEmojis(String title, Map emojis) async { + Future _cacheFilteredEmojis( + String title, Map emojis) async { final prefs = await SharedPreferences.getInstance(); var emojiJson = jsonEncode(emojis); prefs.setString(title, emojiJson); @@ -274,7 +292,8 @@ class _EmojiPickerState extends State { // Add an emoji to recently used list or increase its counter Future _addEmojiToRecentlyUsed(Emoji emoji) async { final prefs = await SharedPreferences.getInstance(); - var recentEmojiIndex = recentEmoji.indexWhere((element) => element.emoji.emoji == emoji.emoji); + var recentEmojiIndex = + recentEmoji.indexWhere((element) => element.emoji.emoji == emoji.emoji); if (recentEmojiIndex != -1) { // Already exist in recent list // Just update counter @@ -285,7 +304,8 @@ class _EmojiPickerState extends State { // Sort by counter desc recentEmoji.sort((a, b) => b.counter - a.counter); // Limit entries to recentsLimit - recentEmoji = recentEmoji.sublist(0, min(widget.config.recentsLimit, recentEmoji.length)); + recentEmoji = recentEmoji.sublist( + 0, min(widget.config.recentsLimit, recentEmoji.length)); // save locally prefs.setString('recent', jsonEncode(recentEmoji)); } diff --git a/frontend/app_flowy/lib/workspace/presentation/widgets/pop_up_window.dart b/frontend/app_flowy/lib/workspace/presentation/widgets/pop_up_window.dart index 4f30248ed2..9ad25dc70c 100644 --- a/frontend/app_flowy/lib/workspace/presentation/widgets/pop_up_window.dart +++ b/frontend/app_flowy/lib/workspace/presentation/widgets/pop_up_window.dart @@ -21,6 +21,7 @@ class FlowyPoppuWindow extends StatelessWidget { required Size size, }) async { final window = await getWindowInfo(); + // ignore: use_build_context_synchronously FlowyOverlay.of(context).insertWithRect( widget: FlowyPoppuWindow(child: child), identifier: 'FlowyPoppuWindow', @@ -49,7 +50,10 @@ class PopupTextField extends StatelessWidget { ); } - static void show({required BuildContext context, required Size size, required void Function(String) textDidChange}) { + static void show( + {required BuildContext context, + required Size size, + required void Function(String) textDidChange}) { FlowyPoppuWindow.show( context, size: size, diff --git a/frontend/app_flowy/pubspec.yaml b/frontend/app_flowy/pubspec.yaml index 7447ae999d..5ebf486947 100644 --- a/frontend/app_flowy/pubspec.yaml +++ b/frontend/app_flowy/pubspec.yaml @@ -82,6 +82,13 @@ dependencies: linked_scroll_controller: ^0.2.0 hotkey_manager: ^0.1.7 fixnum: ^1.0.1 + tuple: ^2.0.0 + protobuf: "2.0.0" + charcode: ^1.3.1 + collection: ^1.16.0 + bloc: ^8.1.0 + textstyle_extensions: ^1.1.0 + shared_preferences: ^2.0.15 dev_dependencies: flutter_lints: ^2.0.1 From fb9ddff574b542f8dd56c3e4d7d8c32f1211b126 Mon Sep 17 00:00:00 2001 From: appflowy Date: Wed, 31 Aug 2022 09:41:45 +0800 Subject: [PATCH 087/106] chore: install keybinder --- frontend/scripts/docker-buildfiles/Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/scripts/docker-buildfiles/Dockerfile b/frontend/scripts/docker-buildfiles/Dockerfile index f050087977..df16c45b3f 100644 --- a/frontend/scripts/docker-buildfiles/Dockerfile +++ b/frontend/scripts/docker-buildfiles/Dockerfile @@ -26,6 +26,7 @@ RUN flutter channel stable RUN flutter config --enable-linux-desktop RUN flutter doctor RUN dart pub global activate protoc_plugin +RUN apt-get install keybinder-3.0 RUN git clone https://github.com/AppFlowy-IO/appflowy.git && \ cd appflowy/frontend && \ From 3b6117fa9f1af5df7714522c0178dd38b654ac19 Mon Sep 17 00:00:00 2001 From: appflowy Date: Wed, 31 Aug 2022 09:44:46 +0800 Subject: [PATCH 088/106] chore: udpate textstyle_extensions version --- frontend/app_flowy/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/app_flowy/pubspec.yaml b/frontend/app_flowy/pubspec.yaml index 5ebf486947..18f7d2f2a7 100644 --- a/frontend/app_flowy/pubspec.yaml +++ b/frontend/app_flowy/pubspec.yaml @@ -87,7 +87,7 @@ dependencies: charcode: ^1.3.1 collection: ^1.16.0 bloc: ^8.1.0 - textstyle_extensions: ^1.1.0 + textstyle_extensions: "2.0.0-nullsafety" shared_preferences: ^2.0.15 dev_dependencies: From 25a43c288c2233666838b0b10cc628c4d8146f19 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Tue, 30 Aug 2022 16:25:32 +0800 Subject: [PATCH 089/106] fix: in the case of multiple selections, the highlighted state does not meet expectations. #948 --- .../src/extensions/text_node_extensions.dart | 29 ++++++--- .../test/service/toolbar_service_test.dart | 61 +++++++++++++++++++ 2 files changed, 81 insertions(+), 9 deletions(-) diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/text_node_extensions.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/text_node_extensions.dart index 9fd85fdb76..a07529cfdf 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/text_node_extensions.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/text_node_extensions.dart @@ -162,23 +162,34 @@ extension TextNodesExtension on List { final node = this[i]; final Selection newSelection; if (i == 0 && pathEquals(node.path, selection.start.path)) { - newSelection = selection.copyWith( - end: Position(path: node.path, offset: node.toRawString().length), - ); + if (selection.isBackward) { + newSelection = selection.copyWith( + end: Position(path: node.path, offset: node.toRawString().length), + ); + } else { + newSelection = selection.copyWith( + end: Position(path: node.path, offset: 0), + ); + } } else if (i == length - 1 && pathEquals(node.path, selection.end.path)) { - newSelection = selection.copyWith( - start: Position(path: node.path, offset: 0), - ); + if (selection.isBackward) { + newSelection = selection.copyWith( + start: Position(path: node.path, offset: 0), + ); + } else { + newSelection = selection.copyWith( + start: + Position(path: node.path, offset: node.toRawString().length), + ); + } } else { newSelection = Selection( start: Position(path: node.path, offset: 0), end: Position(path: node.path, offset: node.toRawString().length), ); } - if (!node.allSatisfyInSelection(newSelection, styleKey, (value) { - return test(value); - })) { + if (!node.allSatisfyInSelection(newSelection, styleKey, test)) { return false; } } diff --git a/frontend/app_flowy/packages/appflowy_editor/test/service/toolbar_service_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/service/toolbar_service_test.dart index 418a333189..23759e449c 100644 --- a/frontend/app_flowy/packages/appflowy_editor/test/service/toolbar_service_test.dart +++ b/frontend/app_flowy/packages/appflowy_editor/test/service/toolbar_service_test.dart @@ -154,6 +154,67 @@ void main() async { itemWidget = _itemWidgetForId(tester, 'appflowy.toolbar.bulleted_list'); expect(itemWidget.isHighlight, true); }); + + testWidgets('Test toolbar service in multi text selection', (tester) async { + const text = 'Welcome to Appflowy 😁'; + + /// [h1][bold] Welcome to Appflowy 😁 + /// [EmptyLine] + /// Welcome to Appflowy 😁 + final editor = tester.editor + ..insertTextNode( + null, + attributes: { + StyleKey.subtype: StyleKey.heading, + StyleKey.heading: StyleKey.h1, + }, + delta: Delta([ + TextInsert(text, { + StyleKey.bold: true, + }) + ]), + ) + ..insertTextNode(null) + ..insertTextNode(text); + await editor.startTesting(); + + await editor.updateSelection( + Selection.single(path: [2], startOffset: text.length, endOffset: 0), + ); + expect(find.byType(ToolbarWidget), findsOneWidget); + expect( + _itemWidgetForId(tester, 'appflowy.toolbar.h1').isHighlight, + false, + ); + expect( + _itemWidgetForId(tester, 'appflowy.toolbar.bold').isHighlight, + false, + ); + + await editor.updateSelection( + Selection( + start: Position(path: [2], offset: text.length), + end: Position(path: [1], offset: 0), + ), + ); + expect(find.byType(ToolbarWidget), findsOneWidget); + expect( + _itemWidgetForId(tester, 'appflowy.toolbar.bold').isHighlight, + false, + ); + + await editor.updateSelection( + Selection( + start: Position(path: [2], offset: text.length), + end: Position(path: [0], offset: 0), + ), + ); + expect(find.byType(ToolbarWidget), findsOneWidget); + expect( + _itemWidgetForId(tester, 'appflowy.toolbar.bold').isHighlight, + false, + ); + }); }); } From c19b7cf8564c5c036cb64f3d9d354c374aba7b96 Mon Sep 17 00:00:00 2001 From: appflowy Date: Wed, 31 Aug 2022 11:15:35 +0800 Subject: [PATCH 090/106] chore: update packages's flutter_lint version --- .../lib/startup/tasks/app_widget.dart | 56 +++++++++---------- .../appflowy_board/example/pubspec.yaml | 2 +- .../packages/appflowy_board/pubspec.yaml | 2 +- .../appflowy_editor/example/pubspec.yaml | 2 +- .../packages/appflowy_editor/pubspec.yaml | 2 +- .../packages/flowy_infra/lib/notifier.dart | 4 +- .../packages/flowy_infra/pubspec.lock | 18 +++--- .../packages/flowy_infra/pubspec.yaml | 2 +- .../flowy_infra_ui/example/pubspec.lock | 18 +++--- .../flowy_infra_ui/example/pubspec.yaml | 2 +- .../pubspec.yaml | 2 +- .../flowy_infra_ui_web/pubspec.yaml | 2 +- .../packages/flowy_infra_ui/pubspec.lock | 18 +++--- .../packages/flowy_infra_ui/pubspec.yaml | 2 +- .../packages/flowy_sdk/example/pubspec.yaml | 2 +- .../app_flowy/packages/flowy_sdk/pubspec.yaml | 2 +- frontend/app_flowy/pubspec.lock | 18 +++--- frontend/app_flowy/pubspec.yaml | 1 + 18 files changed, 76 insertions(+), 79 deletions(-) diff --git a/frontend/app_flowy/lib/startup/tasks/app_widget.dart b/frontend/app_flowy/lib/startup/tasks/app_widget.dart index fede2e0fdb..dd9e1f0850 100644 --- a/frontend/app_flowy/lib/startup/tasks/app_widget.dart +++ b/frontend/app_flowy/lib/startup/tasks/app_widget.dart @@ -23,36 +23,32 @@ class InitAppWidgetTask extends LaunchTask { settingModel: settingModel, child: widget, ); - BlocOverrides.runZoned( - () { - runApp( - EasyLocalization( - supportedLocales: const [ - // In alphabetical order - Locale('ca', 'ES'), - Locale('de', 'DE'), - Locale('en'), - Locale('es', 'VE'), - Locale('fr', 'FR'), - Locale('fr', 'CA'), - Locale('hu', 'HU'), - Locale('id', 'ID'), - Locale('it', 'IT'), - Locale('ja', 'JP'), - Locale('pl', 'PL'), - Locale('pt', 'BR'), - Locale('ru', 'RU'), - Locale('tr', 'TR'), - Locale('zh', 'CN'), - ], - path: 'assets/translations', - fallbackLocale: const Locale('en'), - saveLocale: false, - child: app, - ), - ); - }, - blocObserver: ApplicationBlocObserver(), + Bloc.observer = ApplicationBlocObserver(); + runApp( + EasyLocalization( + supportedLocales: const [ + // In alphabetical order + Locale('ca', 'ES'), + Locale('de', 'DE'), + Locale('en'), + Locale('es', 'VE'), + Locale('fr', 'FR'), + Locale('fr', 'CA'), + Locale('hu', 'HU'), + Locale('id', 'ID'), + Locale('it', 'IT'), + Locale('ja', 'JP'), + Locale('pl', 'PL'), + Locale('pt', 'BR'), + Locale('ru', 'RU'), + Locale('tr', 'TR'), + Locale('zh', 'CN'), + ], + path: 'assets/translations', + fallbackLocale: const Locale('en'), + saveLocale: false, + child: app, + ), ); return Future(() => {}); diff --git a/frontend/app_flowy/packages/appflowy_board/example/pubspec.yaml b/frontend/app_flowy/packages/appflowy_board/example/pubspec.yaml index 1a90f3b84a..c9bd9ef0f9 100644 --- a/frontend/app_flowy/packages/appflowy_board/example/pubspec.yaml +++ b/frontend/app_flowy/packages/appflowy_board/example/pubspec.yaml @@ -46,7 +46,7 @@ dev_dependencies: # activated in the `analysis_options.yaml` file located at the root of your # package. See that file for information about deactivating specific lint # rules and activating additional ones. - flutter_lints: ^2.0.0 + flutter_lints: ^2.0.1 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/frontend/app_flowy/packages/appflowy_board/pubspec.yaml b/frontend/app_flowy/packages/appflowy_board/pubspec.yaml index a9adf5007a..14dc501dcc 100644 --- a/frontend/app_flowy/packages/appflowy_board/pubspec.yaml +++ b/frontend/app_flowy/packages/appflowy_board/pubspec.yaml @@ -17,7 +17,7 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - flutter_lints: ^2.0.0 + flutter_lints: ^2.0.1 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/frontend/app_flowy/packages/appflowy_editor/example/pubspec.yaml b/frontend/app_flowy/packages/appflowy_editor/example/pubspec.yaml index 5ba51433d6..482cad0875 100644 --- a/frontend/app_flowy/packages/appflowy_editor/example/pubspec.yaml +++ b/frontend/app_flowy/packages/appflowy_editor/example/pubspec.yaml @@ -51,7 +51,7 @@ dev_dependencies: # activated in the `analysis_options.yaml` file located at the root of your # package. See that file for information about deactivating specific lint # rules and activating additional ones. - flutter_lints: ^2.0.0 + flutter_lints: ^2.0.1 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/frontend/app_flowy/packages/appflowy_editor/pubspec.yaml b/frontend/app_flowy/packages/appflowy_editor/pubspec.yaml index 6816037b9b..295a45ad8c 100644 --- a/frontend/app_flowy/packages/appflowy_editor/pubspec.yaml +++ b/frontend/app_flowy/packages/appflowy_editor/pubspec.yaml @@ -21,7 +21,7 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - flutter_lints: ^2.0.0 + flutter_lints: ^2.0.1 network_image_mock: ^2.1.1 # For information on the generic Dart part of this file, see the diff --git a/frontend/app_flowy/packages/flowy_infra/lib/notifier.dart b/frontend/app_flowy/packages/flowy_infra/lib/notifier.dart index ac225775d7..bbb55bf885 100644 --- a/frontend/app_flowy/packages/flowy_infra/lib/notifier.dart +++ b/frontend/app_flowy/packages/flowy_infra/lib/notifier.dart @@ -37,13 +37,13 @@ class PublishNotifier extends ChangeNotifier { () { if (_value == null) { return; - } + } else {} if (listenWhen != null && listenWhen() == false) { return; } - callback(_value!); + callback(_value as T); }, ); } diff --git a/frontend/app_flowy/packages/flowy_infra/pubspec.lock b/frontend/app_flowy/packages/flowy_infra/pubspec.lock index fc492e7344..23fd739d68 100644 --- a/frontend/app_flowy/packages/flowy_infra/pubspec.lock +++ b/frontend/app_flowy/packages/flowy_infra/pubspec.lock @@ -68,14 +68,14 @@ packages: name: flutter_lints url: "https://pub.dartlang.org" source: hosted - version: "1.0.4" + version: "2.0.1" flutter_svg: dependency: "direct main" description: name: flutter_svg url: "https://pub.dartlang.org" source: hosted - version: "0.22.0" + version: "1.1.4" flutter_test: dependency: "direct dev" description: flutter @@ -87,7 +87,7 @@ packages: name: lints url: "https://pub.dartlang.org" source: hosted - version: "1.0.1" + version: "2.0.0" matcher: dependency: transitive description: @@ -122,21 +122,21 @@ packages: name: path_drawing url: "https://pub.dartlang.org" source: hosted - version: "0.5.1" + version: "1.0.1" path_parsing: dependency: transitive description: name: path_parsing url: "https://pub.dartlang.org" source: hosted - version: "0.2.1" + version: "1.0.1" petitparser: dependency: transitive description: name: petitparser url: "https://pub.dartlang.org" source: hosted - version: "4.2.0" + version: "5.0.0" sky_engine: dependency: transitive description: flutter @@ -225,7 +225,7 @@ packages: name: xml url: "https://pub.dartlang.org" source: hosted - version: "5.2.0" + version: "6.1.0" sdks: - dart: ">=2.17.0-0 <3.0.0" - flutter: ">=1.24.0-7.0" + dart: ">=2.17.0 <3.0.0" + flutter: ">=2.11.0-0.1.pre" diff --git a/frontend/app_flowy/packages/flowy_infra/pubspec.yaml b/frontend/app_flowy/packages/flowy_infra/pubspec.yaml index ebd7656af9..63f1b66b4e 100644 --- a/frontend/app_flowy/packages/flowy_infra/pubspec.yaml +++ b/frontend/app_flowy/packages/flowy_infra/pubspec.yaml @@ -18,7 +18,7 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - flutter_lints: ^1.0.0 + flutter_lints: ^2.0.1 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/frontend/app_flowy/packages/flowy_infra_ui/example/pubspec.lock b/frontend/app_flowy/packages/flowy_infra_ui/example/pubspec.lock index eaaa5db680..8ddcc99719 100644 --- a/frontend/app_flowy/packages/flowy_infra_ui/example/pubspec.lock +++ b/frontend/app_flowy/packages/flowy_infra_ui/example/pubspec.lock @@ -124,14 +124,14 @@ packages: name: flutter_lints url: "https://pub.dartlang.org" source: hosted - version: "1.0.4" + version: "2.0.1" flutter_svg: dependency: transitive description: name: flutter_svg url: "https://pub.dartlang.org" source: hosted - version: "0.22.0" + version: "1.1.4" flutter_test: dependency: "direct dev" description: flutter @@ -162,7 +162,7 @@ packages: name: lints url: "https://pub.dartlang.org" source: hosted - version: "1.0.1" + version: "2.0.0" loading_indicator: dependency: transitive description: @@ -211,21 +211,21 @@ packages: name: path_drawing url: "https://pub.dartlang.org" source: hosted - version: "0.5.1+1" + version: "1.0.1" path_parsing: dependency: transitive description: name: path_parsing url: "https://pub.dartlang.org" source: hosted - version: "0.2.1" + version: "1.0.1" petitparser: dependency: transitive description: name: petitparser url: "https://pub.dartlang.org" source: hosted - version: "4.4.0" + version: "5.0.0" plugin_platform_interface: dependency: transitive description: @@ -335,7 +335,7 @@ packages: name: xml url: "https://pub.dartlang.org" source: hosted - version: "5.3.1" + version: "6.1.0" sdks: - dart: ">=2.17.0-0 <3.0.0" - flutter: ">=2.0.0" + dart: ">=2.17.0 <3.0.0" + flutter: ">=2.11.0-0.1.pre" diff --git a/frontend/app_flowy/packages/flowy_infra_ui/example/pubspec.yaml b/frontend/app_flowy/packages/flowy_infra_ui/example/pubspec.yaml index a747d1dc18..1f7a31fa6a 100644 --- a/frontend/app_flowy/packages/flowy_infra_ui/example/pubspec.yaml +++ b/frontend/app_flowy/packages/flowy_infra_ui/example/pubspec.yaml @@ -19,7 +19,7 @@ dev_dependencies: flutter_test: sdk: flutter - flutter_lints: ^1.0.0 + flutter_lints: ^2.0.1 flutter: uses-material-design: true diff --git a/frontend/app_flowy/packages/flowy_infra_ui/flowy_infra_ui_platform_interface/pubspec.yaml b/frontend/app_flowy/packages/flowy_infra_ui/flowy_infra_ui_platform_interface/pubspec.yaml index 0b5f8eb74b..2f375be367 100644 --- a/frontend/app_flowy/packages/flowy_infra_ui/flowy_infra_ui_platform_interface/pubspec.yaml +++ b/frontend/app_flowy/packages/flowy_infra_ui/flowy_infra_ui_platform_interface/pubspec.yaml @@ -16,6 +16,6 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - flutter_lints: ^1.0.0 + flutter_lints: ^2.0.1 flutter: \ No newline at end of file diff --git a/frontend/app_flowy/packages/flowy_infra_ui/flowy_infra_ui_web/pubspec.yaml b/frontend/app_flowy/packages/flowy_infra_ui/flowy_infra_ui_web/pubspec.yaml index 03600f2e5a..224d7bf47f 100644 --- a/frontend/app_flowy/packages/flowy_infra_ui/flowy_infra_ui_web/pubspec.yaml +++ b/frontend/app_flowy/packages/flowy_infra_ui/flowy_infra_ui_web/pubspec.yaml @@ -18,7 +18,7 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - flutter_lints: ^1.0.0 + flutter_lints: ^2.0.1 flutter: plugin: diff --git a/frontend/app_flowy/packages/flowy_infra_ui/pubspec.lock b/frontend/app_flowy/packages/flowy_infra_ui/pubspec.lock index c24e86d22d..143279950d 100644 --- a/frontend/app_flowy/packages/flowy_infra_ui/pubspec.lock +++ b/frontend/app_flowy/packages/flowy_infra_ui/pubspec.lock @@ -110,14 +110,14 @@ packages: name: flutter_lints url: "https://pub.dartlang.org" source: hosted - version: "1.0.4" + version: "2.0.1" flutter_svg: dependency: transitive description: name: flutter_svg url: "https://pub.dartlang.org" source: hosted - version: "0.22.0" + version: "1.1.4" flutter_test: dependency: "direct dev" description: flutter @@ -148,7 +148,7 @@ packages: name: lints url: "https://pub.dartlang.org" source: hosted - version: "1.0.1" + version: "2.0.0" loading_indicator: dependency: "direct main" description: @@ -197,21 +197,21 @@ packages: name: path_drawing url: "https://pub.dartlang.org" source: hosted - version: "0.5.1+1" + version: "1.0.1" path_parsing: dependency: transitive description: name: path_parsing url: "https://pub.dartlang.org" source: hosted - version: "0.2.1" + version: "1.0.1" petitparser: dependency: transitive description: name: petitparser url: "https://pub.dartlang.org" source: hosted - version: "4.4.0" + version: "5.0.0" plugin_platform_interface: dependency: transitive description: @@ -321,7 +321,7 @@ packages: name: xml url: "https://pub.dartlang.org" source: hosted - version: "5.3.1" + version: "6.1.0" sdks: - dart: ">=2.17.0-0 <3.0.0" - flutter: ">=2.0.0" + dart: ">=2.17.0 <3.0.0" + flutter: ">=2.11.0-0.1.pre" diff --git a/frontend/app_flowy/packages/flowy_infra_ui/pubspec.yaml b/frontend/app_flowy/packages/flowy_infra_ui/pubspec.yaml index 36a247411b..a7c01e78ba 100644 --- a/frontend/app_flowy/packages/flowy_infra_ui/pubspec.yaml +++ b/frontend/app_flowy/packages/flowy_infra_ui/pubspec.yaml @@ -34,7 +34,7 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - flutter_lints: ^1.0.0 + flutter_lints: ^2.0.1 flutter: plugin: diff --git a/frontend/app_flowy/packages/flowy_sdk/example/pubspec.yaml b/frontend/app_flowy/packages/flowy_sdk/example/pubspec.yaml index dbee825038..954a50e831 100644 --- a/frontend/app_flowy/packages/flowy_sdk/example/pubspec.yaml +++ b/frontend/app_flowy/packages/flowy_sdk/example/pubspec.yaml @@ -29,7 +29,7 @@ dev_dependencies: sdk: flutter integration_test: sdk: flutter - flutter_lints: ^1.0.0 + flutter_lints: ^2.0.1 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/frontend/app_flowy/packages/flowy_sdk/pubspec.yaml b/frontend/app_flowy/packages/flowy_sdk/pubspec.yaml index ced9b97167..186381d848 100644 --- a/frontend/app_flowy/packages/flowy_sdk/pubspec.yaml +++ b/frontend/app_flowy/packages/flowy_sdk/pubspec.yaml @@ -23,7 +23,7 @@ dev_dependencies: sdk: flutter build_runner: freezed: - flutter_lints: ^1.0.0 + flutter_lints: ^2.0.1 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/frontend/app_flowy/pubspec.lock b/frontend/app_flowy/pubspec.lock index 1279bec74b..e2fef15056 100644 --- a/frontend/app_flowy/pubspec.lock +++ b/frontend/app_flowy/pubspec.lock @@ -51,12 +51,12 @@ packages: source: hosted version: "2.8.2" bloc: - dependency: transitive + dependency: "direct main" description: name: bloc url: "https://pub.dartlang.org" source: hosted - version: "8.0.3" + version: "8.1.0" bloc_test: dependency: "direct dev" description: @@ -135,7 +135,7 @@ packages: source: hosted version: "1.2.0" charcode: - dependency: transitive + dependency: "direct main" description: name: charcode url: "https://pub.dartlang.org" @@ -170,7 +170,7 @@ packages: source: hosted version: "4.1.0" collection: - dependency: transitive + dependency: "direct main" description: name: collection url: "https://pub.dartlang.org" @@ -380,7 +380,7 @@ packages: source: hosted version: "6.1.2" fixnum: - dependency: transitive + dependency: "direct main" description: name: fixnum url: "https://pub.dartlang.org" @@ -948,7 +948,7 @@ packages: source: hosted version: "4.2.4" protobuf: - dependency: transitive + dependency: "direct main" description: name: protobuf url: "https://pub.dartlang.org" @@ -1046,7 +1046,7 @@ packages: source: hosted version: "1.0.0" shared_preferences: - dependency: transitive + dependency: "direct main" description: name: shared_preferences url: "https://pub.dartlang.org" @@ -1268,7 +1268,7 @@ packages: source: hosted version: "2.0.0+1" textstyle_extensions: - dependency: transitive + dependency: "direct main" description: name: textstyle_extensions url: "https://pub.dartlang.org" @@ -1289,7 +1289,7 @@ packages: source: hosted version: "1.0.0" tuple: - dependency: transitive + dependency: "direct main" description: name: tuple url: "https://pub.dartlang.org" diff --git a/frontend/app_flowy/pubspec.yaml b/frontend/app_flowy/pubspec.yaml index 18f7d2f2a7..41fa98d7ea 100644 --- a/frontend/app_flowy/pubspec.yaml +++ b/frontend/app_flowy/pubspec.yaml @@ -92,6 +92,7 @@ dependencies: dev_dependencies: flutter_lints: ^2.0.1 + flutter_test: sdk: flutter build_runner: ^2.2.0 From 28f39488f4efa9c722a303f897b066624cfe2692 Mon Sep 17 00:00:00 2001 From: appflowy Date: Wed, 31 Aug 2022 11:21:15 +0800 Subject: [PATCH 091/106] chore: fix packages warnings --- .../example/lib/keyboard/keyboard_screen.dart | 5 +-- .../flowy_infra_ui/example/pubspec.yaml | 1 + .../lib/src/flowy_overlay/flowy_overlay.dart | 2 +- .../keyboard_visibility_detector.dart | 12 ++++--- .../lib/style_widget/container.dart | 4 +-- .../lib/style_widget/hover.dart | 2 +- .../lib/style_widget/icon_button.dart | 2 +- .../scrolling/styled_scrollview.dart | 10 +++--- .../widget/buttons/base_styled_button.dart | 32 ++++++++++++------- .../lib/widget/buttons/primary_button.dart | 2 +- .../lib/widget/buttons/secondary_button.dart | 2 +- .../lib/widget/dialog/styled_dialogs.dart | 2 +- .../lib/widget/mouse_hover_builder.dart | 10 ++++-- .../lib/widget/rounded_button.dart | 2 +- .../lib/widget/route/animation.dart | 2 +- .../lib/widget/seperated_column.dart | 2 +- .../packages/flowy_infra_ui/pubspec.yaml | 1 + 17 files changed, 57 insertions(+), 36 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_infra_ui/example/lib/keyboard/keyboard_screen.dart b/frontend/app_flowy/packages/flowy_infra_ui/example/lib/keyboard/keyboard_screen.dart index 28aa027b49..fde544365b 100644 --- a/frontend/app_flowy/packages/flowy_infra_ui/example/lib/keyboard/keyboard_screen.dart +++ b/frontend/app_flowy/packages/flowy_infra_ui/example/lib/keyboard/keyboard_screen.dart @@ -22,12 +22,13 @@ class KeyboardScreen extends StatefulWidget { const KeyboardScreen({Key? key}) : super(key: key); @override - _KeyboardScreenState createState() => _KeyboardScreenState(); + State createState() => _KeyboardScreenState(); } class _KeyboardScreenState extends State { bool _isKeyboardVisible = false; - final TextEditingController _controller = TextEditingController(text: 'Hello Flowy'); + final TextEditingController _controller = + TextEditingController(text: 'Hello Flowy'); @override Widget build(BuildContext context) { diff --git a/frontend/app_flowy/packages/flowy_infra_ui/example/pubspec.yaml b/frontend/app_flowy/packages/flowy_infra_ui/example/pubspec.yaml index 1f7a31fa6a..b9c8ec058e 100644 --- a/frontend/app_flowy/packages/flowy_infra_ui/example/pubspec.yaml +++ b/frontend/app_flowy/packages/flowy_infra_ui/example/pubspec.yaml @@ -14,6 +14,7 @@ dependencies: path: ../ cupertino_icons: ^1.0.2 + provider: dev_dependencies: flutter_test: diff --git a/frontend/app_flowy/packages/flowy_infra_ui/lib/src/flowy_overlay/flowy_overlay.dart b/frontend/app_flowy/packages/flowy_infra_ui/lib/src/flowy_overlay/flowy_overlay.dart index 25507b8f6d..54f8d3168f 100644 --- a/frontend/app_flowy/packages/flowy_infra_ui/lib/src/flowy_overlay/flowy_overlay.dart +++ b/frontend/app_flowy/packages/flowy_infra_ui/lib/src/flowy_overlay/flowy_overlay.dart @@ -398,8 +398,8 @@ class FlowyOverlayState extends State { if (style.blur) { child = BackdropFilter( - child: child, filter: ImageFilter.blur(sigmaX: 4, sigmaY: 4), + child: child, ); } } diff --git a/frontend/app_flowy/packages/flowy_infra_ui/lib/src/keyboard/keyboard_visibility_detector.dart b/frontend/app_flowy/packages/flowy_infra_ui/lib/src/keyboard/keyboard_visibility_detector.dart index b4d084acc0..643ddd94b1 100644 --- a/frontend/app_flowy/packages/flowy_infra_ui/lib/src/keyboard/keyboard_visibility_detector.dart +++ b/frontend/app_flowy/packages/flowy_infra_ui/lib/src/keyboard/keyboard_visibility_detector.dart @@ -14,10 +14,12 @@ class KeyboardVisibilityDetector extends StatefulWidget { final void Function(bool)? onKeyboardVisibilityChange; @override - _KeyboardVisibilityDetectorState createState() => _KeyboardVisibilityDetectorState(); + State createState() => + _KeyboardVisibilityDetectorState(); } -class _KeyboardVisibilityDetectorState extends State { +class _KeyboardVisibilityDetectorState + extends State { FlowyInfraUIPlatform get _platform => FlowyInfraUIPlatform.instance; bool isObserving = false; @@ -27,7 +29,8 @@ class _KeyboardVisibilityDetectorState extends State @override void initState() { super.initState(); - _keyboardSubscription = _platform.onKeyboardVisibilityChange.listen((newValue) { + _keyboardSubscription = + _platform.onKeyboardVisibilityChange.listen((newValue) { setState(() { isKeyboardVisible = newValue; if (widget.onKeyboardVisibilityChange != null) { @@ -62,7 +65,8 @@ class _KeyboardVisibilityDetectorInheritedWidget extends InheritedWidget { final bool isKeyboardVisible; @override - bool updateShouldNotify(_KeyboardVisibilityDetectorInheritedWidget oldWidget) { + bool updateShouldNotify( + _KeyboardVisibilityDetectorInheritedWidget oldWidget) { return isKeyboardVisible != oldWidget.isKeyboardVisible; } } diff --git a/frontend/app_flowy/packages/flowy_infra_ui/lib/style_widget/container.dart b/frontend/app_flowy/packages/flowy_infra_ui/lib/style_widget/container.dart index 4e26d9bd2d..fc91998c1f 100644 --- a/frontend/app_flowy/packages/flowy_infra_ui/lib/style_widget/container.dart +++ b/frontend/app_flowy/packages/flowy_infra_ui/lib/style_widget/container.dart @@ -31,7 +31,6 @@ class FlowyContainer extends StatelessWidget { return AnimatedContainer( width: width, height: height, - child: child, margin: margin, alignment: align, duration: duration ?? Durations.medium, @@ -39,6 +38,7 @@ class FlowyContainer extends StatelessWidget { color: color, borderRadius: borderRadius, boxShadow: shadows, - border: border)); + border: border), + child: child); } } diff --git a/frontend/app_flowy/packages/flowy_infra_ui/lib/style_widget/hover.dart b/frontend/app_flowy/packages/flowy_infra_ui/lib/style_widget/hover.dart index b9440bf1f1..5a3b305e1e 100644 --- a/frontend/app_flowy/packages/flowy_infra_ui/lib/style_widget/hover.dart +++ b/frontend/app_flowy/packages/flowy_infra_ui/lib/style_widget/hover.dart @@ -51,7 +51,7 @@ class _FlowyHoverState extends State { child: child, ); } else { - return Container(child: child, color: widget.style.backgroundColor); + return Container(color: widget.style.backgroundColor, child: child); } } } diff --git a/frontend/app_flowy/packages/flowy_infra_ui/lib/style_widget/icon_button.dart b/frontend/app_flowy/packages/flowy_infra_ui/lib/style_widget/icon_button.dart index f3ecd23005..0f56541516 100644 --- a/frontend/app_flowy/packages/flowy_infra_ui/lib/style_widget/icon_button.dart +++ b/frontend/app_flowy/packages/flowy_infra_ui/lib/style_widget/icon_button.dart @@ -57,7 +57,7 @@ class FlowyIconButton extends StatelessWidget { onPressed: onPressed, child: Padding( padding: iconPadding, - child: SizedBox.fromSize(child: child, size: childSize), + child: SizedBox.fromSize(size: childSize, child: child), ), ), ), diff --git a/frontend/app_flowy/packages/flowy_infra_ui/lib/style_widget/scrolling/styled_scrollview.dart b/frontend/app_flowy/packages/flowy_infra_ui/lib/style_widget/scrolling/styled_scrollview.dart index bf086f756e..b57b4059cf 100644 --- a/frontend/app_flowy/packages/flowy_infra_ui/lib/style_widget/scrolling/styled_scrollview.dart +++ b/frontend/app_flowy/packages/flowy_infra_ui/lib/style_widget/scrolling/styled_scrollview.dart @@ -27,10 +27,12 @@ class StyledSingleChildScrollView extends StatefulWidget { }) : super(key: key); @override - _StyledSingleChildScrollViewState createState() => _StyledSingleChildScrollViewState(); + State createState() => + StyledSingleChildScrollViewState(); } -class _StyledSingleChildScrollViewState extends State { +class StyledSingleChildScrollViewState + extends State { late ScrollController scrollController; @override @@ -92,10 +94,10 @@ class StyledCustomScrollView extends StatefulWidget { }) : super(key: key); @override - _StyledCustomScrollViewState createState() => _StyledCustomScrollViewState(); + StyledCustomScrollViewState createState() => StyledCustomScrollViewState(); } -class _StyledCustomScrollViewState extends State { +class StyledCustomScrollViewState extends State { late ScrollController controller; @override diff --git a/frontend/app_flowy/packages/flowy_infra_ui/lib/widget/buttons/base_styled_button.dart b/frontend/app_flowy/packages/flowy_infra_ui/lib/widget/buttons/base_styled_button.dart index 0c257f3d3a..d251f993fd 100644 --- a/frontend/app_flowy/packages/flowy_infra_ui/lib/widget/buttons/base_styled_button.dart +++ b/frontend/app_flowy/packages/flowy_infra_ui/lib/widget/buttons/base_styled_button.dart @@ -45,10 +45,10 @@ class BaseStyledButton extends StatefulWidget { }) : super(key: key); @override - _BaseStyledBtnState createState() => _BaseStyledBtnState(); + State createState() => BaseStyledBtnState(); } -class _BaseStyledBtnState extends State { +class BaseStyledBtnState extends State { late FocusNode _focusNode; bool _isFocused = false; @@ -79,9 +79,16 @@ class _BaseStyledBtnState extends State { borderRadius: widget.borderRadius ?? Corners.s10Border, boxShadow: _isFocused ? [ - BoxShadow(color: theme.shader6, offset: Offset.zero, blurRadius: 8.0, spreadRadius: 0.0), BoxShadow( - color: widget.bgColor ?? theme.surface, offset: Offset.zero, blurRadius: 8.0, spreadRadius: -4.0), + color: theme.shader6, + offset: Offset.zero, + blurRadius: 8.0, + spreadRadius: 0.0), + BoxShadow( + color: widget.bgColor ?? theme.surface, + offset: Offset.zero, + blurRadius: 8.0, + spreadRadius: -4.0), ] : [], ), @@ -112,20 +119,21 @@ class _BaseStyledBtnState extends State { hoverColor: widget.hoverColor ?? theme.hover, highlightColor: widget.downColor ?? theme.main1, focusColor: widget.focusColor ?? Colors.grey.withOpacity(0.35), - child: Opacity( - child: Padding( - padding: widget.contentPadding ?? EdgeInsets.all(Insets.m), - child: widget.child, - ), - opacity: widget.onPressed != null ? 1 : .7, - ), - constraints: BoxConstraints(minHeight: widget.minHeight ?? 0, minWidth: widget.minWidth ?? 0), + constraints: BoxConstraints( + minHeight: widget.minHeight ?? 0, minWidth: widget.minWidth ?? 0), onPressed: widget.onPressed, shape: widget.shape ?? RoundedRectangleBorder( side: BorderSide(color: widget.outlineColor, width: 1.5), borderRadius: widget.borderRadius ?? Corners.s10Border, ), + child: Opacity( + opacity: widget.onPressed != null ? 1 : .7, + child: Padding( + padding: widget.contentPadding ?? EdgeInsets.all(Insets.m), + child: widget.child, + ), + ), ), ); } diff --git a/frontend/app_flowy/packages/flowy_infra_ui/lib/widget/buttons/primary_button.dart b/frontend/app_flowy/packages/flowy_infra_ui/lib/widget/buttons/primary_button.dart index 3cd1cdf81e..2c0725288c 100644 --- a/frontend/app_flowy/packages/flowy_infra_ui/lib/widget/buttons/primary_button.dart +++ b/frontend/app_flowy/packages/flowy_infra_ui/lib/widget/buttons/primary_button.dart @@ -38,8 +38,8 @@ class PrimaryButton extends StatelessWidget { hoverColor: theme.main1, downColor: theme.main1, borderRadius: bigMode ? Corners.s12Border : Corners.s8Border, - child: child, onPressed: onPressed, + child: child, ); } } diff --git a/frontend/app_flowy/packages/flowy_infra_ui/lib/widget/buttons/secondary_button.dart b/frontend/app_flowy/packages/flowy_infra_ui/lib/widget/buttons/secondary_button.dart index dedef61295..9e6f7d331d 100644 --- a/frontend/app_flowy/packages/flowy_infra_ui/lib/widget/buttons/secondary_button.dart +++ b/frontend/app_flowy/packages/flowy_infra_ui/lib/widget/buttons/secondary_button.dart @@ -41,8 +41,8 @@ class SecondaryButton extends StatelessWidget { downColor: theme.main1, outlineColor: theme.main1, borderRadius: bigMode ? Corners.s12Border : Corners.s8Border, - child: child, onPressed: onPressed, + child: child, ); } } diff --git a/frontend/app_flowy/packages/flowy_infra_ui/lib/widget/dialog/styled_dialogs.dart b/frontend/app_flowy/packages/flowy_infra_ui/lib/widget/dialog/styled_dialogs.dart index 5db6afacef..ecd8cfb4ae 100644 --- a/frontend/app_flowy/packages/flowy_infra_ui/lib/widget/dialog/styled_dialogs.dart +++ b/frontend/app_flowy/packages/flowy_infra_ui/lib/widget/dialog/styled_dialogs.dart @@ -151,9 +151,9 @@ class StyledDialogRoute extends PopupRoute { Widget buildPage(BuildContext context, Animation animation, Animation secondaryAnimation) { return Semantics( - child: _pageBuilder(context, animation, secondaryAnimation), scopesRoute: true, explicitChildNodes: true, + child: _pageBuilder(context, animation, secondaryAnimation), ); } diff --git a/frontend/app_flowy/packages/flowy_infra_ui/lib/widget/mouse_hover_builder.dart b/frontend/app_flowy/packages/flowy_infra_ui/lib/widget/mouse_hover_builder.dart index 207775a275..81529ba16c 100644 --- a/frontend/app_flowy/packages/flowy_infra_ui/lib/widget/mouse_hover_builder.dart +++ b/frontend/app_flowy/packages/flowy_infra_ui/lib/widget/mouse_hover_builder.dart @@ -5,12 +5,14 @@ typedef HoverBuilder = Widget Function(BuildContext context, bool onHover); class MouseHoverBuilder extends StatefulWidget { final bool isClickable; - const MouseHoverBuilder({Key? key, required this.builder, this.isClickable = false}) : super(key: key); + const MouseHoverBuilder( + {Key? key, required this.builder, this.isClickable = false}) + : super(key: key); final HoverBuilder builder; @override - _MouseHoverBuilderState createState() => _MouseHoverBuilderState(); + State createState() => _MouseHoverBuilderState(); } class _MouseHoverBuilderState extends State { @@ -19,7 +21,9 @@ class _MouseHoverBuilderState extends State { @override Widget build(BuildContext context) { return MouseRegion( - cursor: widget.isClickable ? SystemMouseCursors.click : SystemMouseCursors.basic, + cursor: widget.isClickable + ? SystemMouseCursors.click + : SystemMouseCursors.basic, onEnter: (p) => setOnHover(true), onExit: (p) => setOnHover(false), child: widget.builder(context, _onHover), diff --git a/frontend/app_flowy/packages/flowy_infra_ui/lib/widget/rounded_button.dart b/frontend/app_flowy/packages/flowy_infra_ui/lib/widget/rounded_button.dart index 4d5a79fe79..33075f703c 100644 --- a/frontend/app_flowy/packages/flowy_infra_ui/lib/widget/rounded_button.dart +++ b/frontend/app_flowy/packages/flowy_infra_ui/lib/widget/rounded_button.dart @@ -42,11 +42,11 @@ class RoundedTextButton extends StatelessWidget { ), child: SizedBox.expand( child: TextButton( + onPressed: onPressed, child: Text( title ?? '', style: TextStyle(color: textColor, fontSize: fontSize), ), - onPressed: onPressed, ), ), ), diff --git a/frontend/app_flowy/packages/flowy_infra_ui/lib/widget/route/animation.dart b/frontend/app_flowy/packages/flowy_infra_ui/lib/widget/route/animation.dart index b4f53008d3..e0f328afc9 100644 --- a/frontend/app_flowy/packages/flowy_infra_ui/lib/widget/route/animation.dart +++ b/frontend/app_flowy/packages/flowy_infra_ui/lib/widget/route/animation.dart @@ -52,10 +52,10 @@ class PageRoutes { pageBuilder: (context, animation, secondaryAnimation) => pageBuilder(), transitionsBuilder: (context, animation, secondaryAnimation, child) { return SharedAxisTransition( - child: child, animation: animation, secondaryAnimation: secondaryAnimation, transitionType: type, + child: child, ); }, ); diff --git a/frontend/app_flowy/packages/flowy_infra_ui/lib/widget/seperated_column.dart b/frontend/app_flowy/packages/flowy_infra_ui/lib/widget/seperated_column.dart index f25a74426e..7362f989e5 100644 --- a/frontend/app_flowy/packages/flowy_infra_ui/lib/widget/seperated_column.dart +++ b/frontend/app_flowy/packages/flowy_infra_ui/lib/widget/seperated_column.dart @@ -31,13 +31,13 @@ class SeparatedColumn extends StatelessWidget { if (i > 0 && separatorBuilder != null) c.insert(i, separatorBuilder!()); } return Column( - children: c, mainAxisAlignment: mainAxisAlignment, crossAxisAlignment: crossAxisAlignment, mainAxisSize: mainAxisSize, textBaseline: textBaseline, textDirection: textDirection, verticalDirection: verticalDirection, + children: c, ); } } diff --git a/frontend/app_flowy/packages/flowy_infra_ui/pubspec.yaml b/frontend/app_flowy/packages/flowy_infra_ui/pubspec.yaml index a7c01e78ba..c95ba1119e 100644 --- a/frontend/app_flowy/packages/flowy_infra_ui/pubspec.yaml +++ b/frontend/app_flowy/packages/flowy_infra_ui/pubspec.yaml @@ -20,6 +20,7 @@ dependencies: equatable: "^2.0.3" animations: ^2.0.0 loading_indicator: ^3.0.1 + async: # Federated Platform Interface flowy_infra_ui_platform_interface: From 506af26b6ddd0f72b2891f6fa4295188c225a763 Mon Sep 17 00:00:00 2001 From: appflowy Date: Wed, 31 Aug 2022 10:59:56 +0800 Subject: [PATCH 092/106] chore: save move to no status card --- .../appflowy_board/lib/src/utils/log.dart | 2 +- .../selection_type_option/select_option.rs | 3 +- .../src/services/grid_view_manager.rs | 4 +- .../src/services/group/configuration.rs | 96 +++++++++---------- .../src/services/group/controller.rs | 2 +- 5 files changed, 54 insertions(+), 53 deletions(-) diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/utils/log.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/utils/log.dart index 6f35ae4195..9c23060b26 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/utils/log.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/utils/log.dart @@ -4,7 +4,7 @@ import 'package:flutter/material.dart'; const DART_LOG = "Dart_LOG"; class Log { - static const enableLog = true; + static const enableLog = false; static void info(String? message) { if (enableLog) { diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/select_option.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/select_option.rs index 12f94c943c..dcec7772a7 100644 --- a/frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/select_option.rs +++ b/frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/select_option.rs @@ -155,11 +155,12 @@ pub fn select_option_color_from_index(index: usize) -> SelectOptionColorPB { } } +#[derive(Default)] pub struct SelectOptionIds(Vec); impl SelectOptionIds { pub fn new() -> Self { - Self(vec![]) + Self::default() } pub fn into_inner(self) -> Vec { self.0 diff --git a/frontend/rust-lib/flowy-grid/src/services/grid_view_manager.rs b/frontend/rust-lib/flowy-grid/src/services/grid_view_manager.rs index 70e4cc29b1..0444c52763 100644 --- a/frontend/rust-lib/flowy-grid/src/services/grid_view_manager.rs +++ b/frontend/rust-lib/flowy-grid/src/services/grid_view_manager.rs @@ -134,7 +134,7 @@ impl GridViewManager { row_rev: Arc, to_group_id: String, to_row_id: Option, - with_row_changeset: impl FnOnce(RowChangeset) -> AFFuture<()>, + recv_row_changeset: impl FnOnce(RowChangeset) -> AFFuture<()>, ) -> FlowyResult<()> { let mut row_changeset = RowChangeset::new(row_rev.id.clone()); let view_editor = self.get_default_view_editor().await?; @@ -143,7 +143,7 @@ impl GridViewManager { .await; if !row_changeset.is_empty() { - with_row_changeset(row_changeset).await; + recv_row_changeset(row_changeset).await; } for group_changeset in group_changesets { diff --git a/frontend/rust-lib/flowy-grid/src/services/group/configuration.rs b/frontend/rust-lib/flowy-grid/src/services/group/configuration.rs index fea71ebbbc..ab25cd4307 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/configuration.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/configuration.rs @@ -93,16 +93,47 @@ where }) } - pub(crate) fn groups(&self) -> Vec<&Group> { + /// Returns the groups without the default group + pub(crate) fn concrete_groups(&self) -> Vec<&Group> { self.groups_map.values().collect() } + /// Returns the all the groups that contain the default group. pub(crate) fn clone_groups(&self) -> Vec { let mut groups: Vec = self.groups_map.values().cloned().collect(); groups.push(self.default_group.clone()); groups } + /// Iterate mut the groups. The default group will be the last one that get mutated. + pub(crate) fn iter_mut_groups(&mut self, mut each: impl FnMut(&mut Group)) { + self.groups_map.iter_mut().for_each(|(_, group)| { + each(group); + }); + + each(&mut self.default_group); + } + + pub(crate) fn move_group(&mut self, from_id: &str, to_id: &str) -> FlowyResult<()> { + let from_index = self.groups_map.get_index_of(from_id); + let to_index = self.groups_map.get_index_of(to_id); + match (from_index, to_index) { + (Some(from_index), Some(to_index)) => { + self.groups_map.swap_indices(from_index, to_index); + self.mut_configuration(|configuration| { + let from_index = configuration.groups.iter().position(|group| group.id == from_id); + let to_index = configuration.groups.iter().position(|group| group.id == to_id); + if let (Some(from), Some(to)) = (from_index, to_index) { + configuration.groups.swap(from, to); + } + true + })?; + Ok(()) + } + _ => Err(FlowyError::out_of_bounds()), + } + } + pub(crate) fn merge_groups(&mut self, groups: Vec) -> FlowyResult> { let MergeGroupResult { groups, @@ -154,7 +185,7 @@ where #[allow(dead_code)] pub(crate) async fn hide_group(&mut self, group_id: &str) -> FlowyResult<()> { - self.mut_configuration_group(group_id, |group_rev| { + self.mut_group_rev(group_id, |group_rev| { group_rev.visible = false; })?; Ok(()) @@ -162,45 +193,18 @@ where #[allow(dead_code)] pub(crate) async fn show_group(&mut self, group_id: &str) -> FlowyResult<()> { - self.mut_configuration_group(group_id, |group_rev| { + self.mut_group_rev(group_id, |group_rev| { group_rev.visible = true; })?; Ok(()) } - pub(crate) fn iter_mut_groups(&mut self, mut each: impl FnMut(&mut Group)) { - self.groups_map.iter_mut().for_each(|(_, group)| { - each(group); - }) - } - - pub(crate) fn get_mut_group(&mut self, group_id: &str) -> Option<&mut Group> { - self.groups_map.get_mut(group_id) - } - pub(crate) fn get_mut_default_group(&mut self) -> &mut Group { &mut self.default_group } - pub(crate) fn move_group(&mut self, from_id: &str, to_id: &str) -> FlowyResult<()> { - let from_index = self.groups_map.get_index_of(from_id); - let to_index = self.groups_map.get_index_of(to_id); - match (from_index, to_index) { - (Some(from_index), Some(to_index)) => { - self.groups_map.swap_indices(from_index, to_index); - - self.mut_configuration(|configuration| { - let from_index = configuration.groups.iter().position(|group| group.id == from_id); - let to_index = configuration.groups.iter().position(|group| group.id == to_id); - if let (Some(from), Some(to)) = (from_index, to_index) { - configuration.groups.swap(from, to); - } - true - })?; - Ok(()) - } - _ => Err(FlowyError::out_of_bounds()), - } + pub(crate) fn get_mut_group(&mut self, group_id: &str) -> Option<&mut Group> { + self.groups_map.get_mut(group_id) } // Returns the index and group specified by the group_id @@ -231,22 +235,6 @@ where Ok(()) } - fn mut_configuration_group( - &mut self, - group_id: &str, - mut_groups_fn: impl Fn(&mut GroupRevision), - ) -> FlowyResult<()> { - self.mut_configuration(|configuration| { - match configuration.groups.iter_mut().find(|group| group.id == group_id) { - None => false, - Some(group_rev) => { - mut_groups_fn(group_rev); - true - } - } - }) - } - fn mut_configuration( &mut self, mut_configuration_fn: impl FnOnce(&mut GroupConfigurationRevision) -> bool, @@ -258,6 +246,18 @@ where } Ok(()) } + + fn mut_group_rev(&mut self, group_id: &str, mut_groups_fn: impl Fn(&mut GroupRevision)) -> FlowyResult<()> { + self.mut_configuration(|configuration| { + match configuration.groups.iter_mut().find(|group| group.id == group_id) { + None => false, + Some(group_rev) => { + mut_groups_fn(group_rev); + true + } + } + }) + } } fn merge_groups(old_groups: &[GroupRevision], groups: Vec) -> MergeGroupResult { diff --git a/frontend/rust-lib/flowy-grid/src/services/group/controller.rs b/frontend/rust-lib/flowy-grid/src/services/group/controller.rs index 562f21eb17..62647d44b8 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/controller.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/controller.rs @@ -195,7 +195,7 @@ where let mut grouped_rows: Vec = vec![]; let cell_bytes = decode_any_cell_data(cell_rev.data.clone(), field_rev); let cell_data = cell_bytes.parser::

()?; - for group in self.configuration.groups() { + for group in self.configuration.concrete_groups() { if self.can_group(&group.content, &cell_data) { grouped_rows.push(GroupedRow { row: row_rev.into(), From cde8ad908482da7814890f0a33e7786724e22ae6 Mon Sep 17 00:00:00 2001 From: appflowy Date: Wed, 31 Aug 2022 14:56:03 +0800 Subject: [PATCH 093/106] chore: enable create card from header button --- .../plugins/board/application/board_bloc.dart | 50 ++++++++++++----- .../application/board_data_controller.dart | 5 +- .../board/application/group_controller.dart | 9 +++- .../board/presentation/board_page.dart | 54 ++++++++++++------- .../grid/application/grid_service.dart | 10 +++- .../example/lib/multi_board_list_example.dart | 6 +-- .../appflowy_board/lib/src/widgets/board.dart | 6 ++- .../widgets/board_column/board_column.dart | 6 +-- .../src/entities/group_entities/group.rs | 9 +++- .../src/services/grid_view_editor.rs | 6 ++- 10 files changed, 115 insertions(+), 46 deletions(-) diff --git a/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart b/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart index 53038c8d42..15db532129 100644 --- a/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart +++ b/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart @@ -69,16 +69,31 @@ class BoardBloc extends Bloc { _startListening(); await _loadGrid(emit); }, - createRow: (groupId) async { + createBottomRow: (groupId) async { + final startRowId = groupControllers[groupId]?.lastRow()?.id; + final result = await _gridDataController.createBoardCard( + groupId, + startRowId: startRowId, + ); + result.fold( + (_) {}, + (err) => Log.error(err), + ); + }, + createHeaderRow: (String groupId) async { final result = await _gridDataController.createBoardCard(groupId); result.fold( (_) {}, (err) => Log.error(err), ); }, - didCreateRow: (String groupId, RowPB row) { + didCreateRow: (String groupId, RowPB row, int? index) { emit(state.copyWith( - editingRow: Some(BoardEditingRow(columnId: groupId, row: row)), + editingRow: Some(BoardEditingRow( + columnId: groupId, + row: row, + index: index, + )), )); }, endEditRow: (rowId) { @@ -142,8 +157,8 @@ class BoardBloc extends Bloc { for (final group in groups) { final delegate = GroupControllerDelegateImpl( controller: boardController, - onNewColumnItem: (groupId, row) { - add(BoardEvent.didCreateRow(groupId, row)); + onNewColumnItem: (groupId, row, index) { + add(BoardEvent.didCreateRow(groupId, row, index)); }, ); final controller = GroupController( @@ -231,9 +246,13 @@ class BoardBloc extends Bloc { @freezed class BoardEvent with _$BoardEvent { const factory BoardEvent.initial() = _InitialBoard; - const factory BoardEvent.createRow(String groupId) = _CreateRow; - const factory BoardEvent.didCreateRow(String groupId, RowPB row) = - _DidCreateRow; + const factory BoardEvent.createBottomRow(String groupId) = _CreateBottomRow; + const factory BoardEvent.createHeaderRow(String groupId) = _CreateHeaderRow; + const factory BoardEvent.didCreateRow( + String groupId, + RowPB row, + int? index, + ) = _DidCreateRow; const factory BoardEvent.endEditRow(String rowId) = _EndEditRow; const factory BoardEvent.didReceiveError(FlowyError error) = _DidReceiveError; const factory BoardEvent.didReceiveGridUpdate( @@ -313,7 +332,7 @@ class BoardColumnItem extends AFColumnItem { class GroupControllerDelegateImpl extends GroupControllerDelegate { final AFBoardDataController controller; - final void Function(String, RowPB) onNewColumnItem; + final void Function(String, RowPB, int?) onNewColumnItem; GroupControllerDelegateImpl({ required this.controller, @@ -351,23 +370,30 @@ class GroupControllerDelegateImpl extends GroupControllerDelegate { } @override - void addNewRow(GroupPB group, RowPB row) { + void addNewRow(GroupPB group, RowPB row, int? index) { final item = BoardColumnItem( row: row, fieldId: group.fieldId, requestFocus: true, ); - controller.addColumnItem(group.groupId, item); - onNewColumnItem(group.groupId, row); + + if (index != null) { + controller.insertColumnItem(group.groupId, index, item); + } else { + controller.addColumnItem(group.groupId, item); + } + onNewColumnItem(group.groupId, row, index); } } class BoardEditingRow { String columnId; RowPB row; + int? index; BoardEditingRow({ required this.columnId, required this.row, + required this.index, }); } diff --git a/frontend/app_flowy/lib/plugins/board/application/board_data_controller.dart b/frontend/app_flowy/lib/plugins/board/application/board_data_controller.dart index 79f53093f1..fdc1179fa1 100644 --- a/frontend/app_flowy/lib/plugins/board/application/board_data_controller.dart +++ b/frontend/app_flowy/lib/plugins/board/application/board_data_controller.dart @@ -118,8 +118,9 @@ class BoardDataController { ); } - Future> createBoardCard(String groupId) { - return _gridFFIService.createBoardCard(groupId); + Future> createBoardCard(String groupId, + {String? startRowId}) { + return _gridFFIService.createBoardCard(groupId, startRowId); } Future dispose() async { diff --git a/frontend/app_flowy/lib/plugins/board/application/group_controller.dart b/frontend/app_flowy/lib/plugins/board/application/group_controller.dart index 9407ae360b..6a148c0312 100644 --- a/frontend/app_flowy/lib/plugins/board/application/group_controller.dart +++ b/frontend/app_flowy/lib/plugins/board/application/group_controller.dart @@ -9,7 +9,7 @@ abstract class GroupControllerDelegate { void removeRow(GroupPB group, String rowId); void insertRow(GroupPB group, RowPB row, int? index); void updateRow(GroupPB group, RowPB row); - void addNewRow(GroupPB group, RowPB row); + void addNewRow(GroupPB group, RowPB row, int? index); } class GroupController { @@ -31,6 +31,11 @@ class GroupController { } } + RowPB? lastRow() { + if (group.rows.isEmpty) return null; + return group.rows.last; + } + void startListening() { _listener.start(onGroupChanged: (result) { result.fold( @@ -50,7 +55,7 @@ class GroupController { } if (insertedRow.isNew) { - delegate.addNewRow(group, insertedRow.row); + delegate.addNewRow(group, insertedRow.row, index); } else { delegate.insertRow(group, insertedRow.row, index); } diff --git a/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart b/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart index e850186f89..bc8a4fcfa7 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart @@ -84,11 +84,14 @@ class _BoardContentState extends State { () => null, (editingRow) { WidgetsBinding.instance.addPostFrameCallback((_) { - scrollManager.scrollToBottom(editingRow.columnId, () { - context - .read() - .add(BoardEvent.endEditRow(editingRow.row.id)); - }); + if (editingRow.index != null) { + } else { + scrollManager.scrollToBottom(editingRow.columnId, () { + context + .read() + .add(BoardEvent.endEditRow(editingRow.row.id)); + }); + } }); }, ); @@ -131,26 +134,32 @@ class _BoardContentState extends State { } Widget _buildHeader( - BuildContext context, AFBoardColumnHeaderData headerData) { + BuildContext context, + AFBoardColumnData columnData, + ) { return AppFlowyColumnHeader( title: Flexible( fit: FlexFit.tight, child: FlowyText.medium( - headerData.columnName, + columnData.headerData.columnName, fontSize: 14, overflow: TextOverflow.clip, color: context.read().textColor, ), ), - // addIcon: const Icon(Icons.add, size: 20), - // moreIcon: SizedBox( - // width: 20, - // height: 20, - // child: svgWidget( - // 'grid/details', - // color: context.read().iconColor, - // ), - // ), + addIcon: SizedBox( + height: 20, + width: 20, + child: svgWidget( + "home/add", + color: context.read().iconColor, + ), + ), + onAddButtonClick: () { + context.read().add( + BoardEvent.createHeaderRow(columnData.id), + ); + }, height: 50, margin: config.headerPadding, ); @@ -178,7 +187,9 @@ class _BoardContentState extends State { height: 50, margin: config.footerPadding, onAddButtonClick: () { - context.read().add(BoardEvent.createRow(columnData.id)); + context.read().add( + BoardEvent.createBottomRow(columnData.id), + ); }, ); } @@ -205,8 +216,13 @@ class _BoardContentState extends State { ); final cellBuilder = BoardCellBuilder(cardController); - - final isEditing = context.read().state.editingRow.isSome(); + bool isEditing = false; + context.read().state.editingRow.fold( + () => null, + (editingRow) { + isEditing = editingRow.row.id == columnItem.row.id; + }, + ); return AppFlowyColumnItemCard( key: ValueKey(columnItem.id), diff --git a/frontend/app_flowy/lib/plugins/grid/application/grid_service.dart b/frontend/app_flowy/lib/plugins/grid/application/grid_service.dart index c8b6873d91..40dd5eeda1 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/grid_service.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/grid_service.dart @@ -27,10 +27,18 @@ class GridFFIService { return GridEventCreateTableRow(payload).send(); } - Future> createBoardCard(String groupId) { + Future> createBoardCard( + String groupId, + String? startRowId, + ) { CreateBoardCardPayloadPB payload = CreateBoardCardPayloadPB.create() ..gridId = gridId ..groupId = groupId; + + if (startRowId != null) { + payload.startRowId = startRowId; + } + return GridEventCreateBoardCard(payload).send(); } diff --git a/frontend/app_flowy/packages/appflowy_board/example/lib/multi_board_list_example.dart b/frontend/app_flowy/packages/appflowy_board/example/lib/multi_board_list_example.dart index 7b9a84b6d2..b69fb1dbf3 100644 --- a/frontend/app_flowy/packages/appflowy_board/example/lib/multi_board_list_example.dart +++ b/frontend/app_flowy/packages/appflowy_board/example/lib/multi_board_list_example.dart @@ -73,17 +73,17 @@ class _MultiBoardListExampleState extends State { margin: config.columnItemPadding, ); }, - headerBuilder: (context, headerData) { + headerBuilder: (context, columnData) { return AppFlowyColumnHeader( icon: const Icon(Icons.lightbulb_circle), title: SizedBox( width: 60, child: TextField( controller: TextEditingController() - ..text = headerData.columnName, + ..text = columnData.headerData.columnName, onSubmitted: (val) { boardDataController - .getColumnController(headerData.columnId)! + .getColumnController(columnData.headerData.columnId)! .updateColumnName(val); }, ), diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board.dart index 3360325e8e..5b44ec64b7 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board.dart @@ -282,14 +282,16 @@ class _AFBoardContentState extends State { } Widget? _buildHeader( - BuildContext context, AFBoardColumnHeaderData headerData) { + BuildContext context, + AFBoardColumnData columnData, + ) { if (widget.headerBuilder == null) { return null; } return Selector( selector: (context, controller) => controller.columnData.headerData, builder: (context, headerData, _) { - return widget.headerBuilder!(context, headerData)!; + return widget.headerBuilder!(context, columnData)!; }, ); } diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column.dart index 2065d88ba2..79fe534941 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column.dart @@ -31,7 +31,7 @@ typedef AFBoardColumnCardBuilder = Widget Function( typedef AFBoardColumnHeaderBuilder = Widget? Function( BuildContext context, - AFBoardColumnHeaderData headerData, + AFBoardColumnData headerData, ); typedef AFBoardColumnFooterBuilder = Widget Function( @@ -132,8 +132,8 @@ class _AFBoardColumnWidgetState extends State { .map((item) => _buildWidget(context, item)) .toList(); - final header = widget.headerBuilder - ?.call(context, widget.dataSource.columnData.headerData); + final header = + widget.headerBuilder?.call(context, widget.dataSource.columnData); final footer = widget.footBuilder?.call(context, widget.dataSource.columnData); diff --git a/frontend/rust-lib/flowy-grid/src/entities/group_entities/group.rs b/frontend/rust-lib/flowy-grid/src/entities/group_entities/group.rs index 75b133344f..002cb73c6d 100644 --- a/frontend/rust-lib/flowy-grid/src/entities/group_entities/group.rs +++ b/frontend/rust-lib/flowy-grid/src/entities/group_entities/group.rs @@ -14,6 +14,9 @@ pub struct CreateBoardCardPayloadPB { #[pb(index = 2)] pub group_id: String, + + #[pb(index = 3, one_of)] + pub start_row_id: Option, } impl TryInto for CreateBoardCardPayloadPB { @@ -22,9 +25,13 @@ impl TryInto for CreateBoardCardPayloadPB { fn try_into(self) -> Result { let grid_id = NotEmptyStr::parse(self.grid_id).map_err(|_| ErrorCode::GridIdIsEmpty)?; let group_id = NotEmptyStr::parse(self.group_id).map_err(|_| ErrorCode::GroupIdIsEmpty)?; + let start_row_id = match self.start_row_id { + None => None, + Some(start_row_id) => Some(NotEmptyStr::parse(start_row_id).map_err(|_| ErrorCode::RowIdIsEmpty)?.0), + }; Ok(CreateRowParams { grid_id: grid_id.0, - start_row_id: None, + start_row_id, group_id: Some(group_id.0), layout: GridLayout::Board, }) diff --git a/frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs b/frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs index 013f59cd42..688a844707 100644 --- a/frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs +++ b/frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs @@ -95,9 +95,13 @@ impl GridViewRevisionEditor { match params.group_id.as_ref() { None => {} Some(group_id) => { + let index = match params.start_row_id { + None => Some(0), + Some(_) => None, + }; let inserted_row = InsertedRowPB { row: row_pb.clone(), - index: None, + index, is_new: true, }; let changeset = GroupChangesetPB::insert(group_id.clone(), vec![inserted_row]); From 6de77d5a5b390fa158431f369e81b7042afd84c1 Mon Sep 17 00:00:00 2001 From: appflowy Date: Wed, 31 Aug 2022 15:05:16 +0800 Subject: [PATCH 094/106] fix: fix docker build again --- frontend/scripts/docker-buildfiles/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/scripts/docker-buildfiles/Dockerfile b/frontend/scripts/docker-buildfiles/Dockerfile index df16c45b3f..183920f72f 100644 --- a/frontend/scripts/docker-buildfiles/Dockerfile +++ b/frontend/scripts/docker-buildfiles/Dockerfile @@ -26,7 +26,7 @@ RUN flutter channel stable RUN flutter config --enable-linux-desktop RUN flutter doctor RUN dart pub global activate protoc_plugin -RUN apt-get install keybinder-3.0 +RUN pacman -Syu --needed --noconfirm git xdg-user-dirs libkeybinder3 RUN git clone https://github.com/AppFlowy-IO/appflowy.git && \ cd appflowy/frontend && \ From f098c543e60a48409e0337957415f9ad8283fdc6 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Tue, 30 Aug 2022 20:32:38 +0800 Subject: [PATCH 095/106] feat: #953 improve arrow keys handler --- .../lib/src/document/selection.dart | 4 +- .../render/rich_text/default_selectable.dart | 3 +- .../src/render/rich_text/flowy_rich_text.dart | 13 +- .../arrow_keys_handler.dart | 358 ++++++++++++------ .../copy_paste_handler.dart | 9 +- .../default_key_event_handlers.dart | 3 +- .../legacy/arrow_keys_handler.dart | 153 ++++++++ .../slash_handler.dart | 4 - .../lib/src/service/selection_service.dart | 10 +- .../test/infra/test_raw_key_event.dart | 3 + .../arrow_keys_handler_test.dart | 282 ++++++++++++++ 11 files changed, 710 insertions(+), 132 deletions(-) create mode 100644 frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/legacy/arrow_keys_handler.dart diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/document/selection.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/document/selection.dart index 99a81c9273..986dd37468 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/document/selection.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/document/selection.dart @@ -46,9 +46,9 @@ class Selection { (start.path <= end.path && !pathEquals(start.path, end.path)) || (isSingle && start.offset < end.offset); - Selection normalize() { + Selection get normalize { if (isForward) { - return Selection(start: end, end: start); + return reversed; } return this; } diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/default_selectable.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/default_selectable.dart index 91b9cbf981..c477478deb 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/default_selectable.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/default_selectable.dart @@ -32,7 +32,8 @@ mixin DefaultSelectable { Selection getSelectionInRange(Offset start, Offset end) => forward.getSelectionInRange(start, end); - Offset localToGlobal(Offset offset) => forward.localToGlobal(offset); + Offset localToGlobal(Offset offset) => + forward.localToGlobal(offset) - baseOffset; Selection? getWorldBoundaryInOffset(Offset offset) => forward.getWorldBoundaryInOffset(offset); diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/flowy_rich_text.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/flowy_rich_text.dart index 3489c2bb52..9d1b7f119e 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/flowy_rich_text.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/flowy_rich_text.dart @@ -132,17 +132,24 @@ class _FlowyRichTextState extends State with Selectable { @override List getRectsInSelection(Selection selection) { - assert(pathEquals(selection.start.path, selection.end.path) && + assert(selection.isSingle && pathEquals(selection.start.path, widget.textNode.path)); final textSelection = TextSelection( baseOffset: selection.start.offset, extentOffset: selection.end.offset, ); - return _renderParagraph + final rects = _renderParagraph .getBoxesForSelection(textSelection, boxHeightStyle: BoxHeightStyle.max) .map((box) => box.toRect()) - .toList(); + .toList(growable: false); + if (rects.isEmpty) { + // If the rich text widget does not contain any text, + // there will be no selection boxes, + // so we need to return to the default selection. + return [Rect.fromLTWH(0, 0, 0, _renderParagraph.size.height)]; + } + return rects; } @override diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/arrow_keys_handler.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/arrow_keys_handler.dart index ecaf3325e4..a19541da0f 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/arrow_keys_handler.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/arrow_keys_handler.dart @@ -1,26 +1,225 @@ import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_editor/src/extensions/node_extensions.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -int _endOffsetOfNode(Node node) { - if (node is TextNode) { - return node.delta.length; +AppFlowyKeyEventHandler arrowKeysHandler = (editorState, event) { + if (!_arrowKeys.contains(event.logicalKey)) { + return KeyEventResult.ignored; } - return 0; + + if (event.isMetaPressed && event.isShiftPressed) { + return _arrowKeysWithMetaAndShift(editorState, event); + } else if (event.isMetaPressed) { + return _arrowKeysWithMeta(editorState, event); + } else if (event.isShiftPressed) { + return _arrowKeysWithShift(editorState, event); + } else { + return _arrowKeysOnly(editorState, event); + } +}; + +final _arrowKeys = [ + LogicalKeyboardKey.arrowLeft, + LogicalKeyboardKey.arrowRight, + LogicalKeyboardKey.arrowUp, + LogicalKeyboardKey.arrowDown +]; + +KeyEventResult _arrowKeysWithMetaAndShift( + EditorState editorState, RawKeyEvent event) { + if (!event.isMetaPressed || + !event.isShiftPressed || + !_arrowKeys.contains(event.logicalKey)) { + assert(false); + return KeyEventResult.ignored; + } + + final nodes = editorState.service.selectionService.currentSelectedNodes; + final selection = editorState.service.selectionService.currentSelection.value; + if (nodes.isEmpty || selection == null) { + return KeyEventResult.ignored; + } + + var start = selection.start; + var end = selection.end; + if (event.logicalKey == LogicalKeyboardKey.arrowLeft) { + final position = nodes.first.selectable?.start(); + if (position != null) { + end = position; + } + } else if (event.logicalKey == LogicalKeyboardKey.arrowRight) { + final position = nodes.first.selectable?.end(); + if (position != null) { + end = position; + } + } else if (event.logicalKey == LogicalKeyboardKey.arrowUp) { + final position = editorState.document.root.children + .whereType() + .first + .selectable + ?.start(); + if (position != null) { + end = position; + } + } else if (event.logicalKey == LogicalKeyboardKey.arrowDown) { + final position = editorState.document.root.children + .whereType() + .last + .selectable + ?.end(); + if (position != null) { + end = position; + } + } + editorState.service.selectionService.updateSelection( + selection.copyWith(start: start, end: end), + ); + return KeyEventResult.handled; +} + +// Move the cursor to top, bottom, left and right of the document. +KeyEventResult _arrowKeysWithMeta(EditorState editorState, RawKeyEvent event) { + if (!event.isMetaPressed || + event.isShiftPressed || + !_arrowKeys.contains(event.logicalKey)) { + assert(false); + return KeyEventResult.ignored; + } + + final nodes = editorState.service.selectionService.currentSelectedNodes; + if (nodes.isEmpty) { + return KeyEventResult.ignored; + } + Position? position; + if (event.logicalKey == LogicalKeyboardKey.arrowLeft) { + position = nodes.first.selectable?.start(); + } else if (event.logicalKey == LogicalKeyboardKey.arrowRight) { + position = nodes.last.selectable?.end(); + } else if (event.logicalKey == LogicalKeyboardKey.arrowUp) { + position = editorState.document.root.children + .whereType() + .first + .selectable + ?.start(); + } else if (event.logicalKey == LogicalKeyboardKey.arrowDown) { + position = editorState.document.root.children + .whereType() + .last + .selectable + ?.end(); + } + if (position == null) { + return KeyEventResult.ignored; + } + editorState.service.selectionService.updateSelection( + Selection.collapsed(position), + ); + return KeyEventResult.handled; +} + +KeyEventResult _arrowKeysWithShift(EditorState editorState, RawKeyEvent event) { + if (event.isMetaPressed || + !event.isShiftPressed || + !_arrowKeys.contains(event.logicalKey)) { + assert(false); + return KeyEventResult.ignored; + } + + final nodes = editorState.service.selectionService.currentSelectedNodes; + final selection = editorState.service.selectionService.currentSelection.value; + if (nodes.isEmpty || selection == null) { + return KeyEventResult.ignored; + } + Position? end; + if (event.logicalKey == LogicalKeyboardKey.arrowLeft) { + end = selection.end.goLeft(editorState); + } else if (event.logicalKey == LogicalKeyboardKey.arrowRight) { + end = selection.end.goRight(editorState); + } else if (event.logicalKey == LogicalKeyboardKey.arrowUp) { + end = _goUp(editorState); + } else if (event.logicalKey == LogicalKeyboardKey.arrowDown) { + end = _goDown(editorState); + } + if (end == null) { + return KeyEventResult.ignored; + } + editorState.service.selectionService + .updateSelection(selection.copyWith(end: end)); + return KeyEventResult.handled; +} + +KeyEventResult _arrowKeysOnly(EditorState editorState, RawKeyEvent event) { + if (event.isMetaPressed || + event.isShiftPressed || + !_arrowKeys.contains(event.logicalKey)) { + assert(false); + return KeyEventResult.ignored; + } + + final nodes = editorState.service.selectionService.currentSelectedNodes; + final selection = + editorState.service.selectionService.currentSelection.value?.normalize; + if (nodes.isEmpty || selection == null) { + return KeyEventResult.ignored; + } + if (event.logicalKey == LogicalKeyboardKey.arrowLeft) { + if (selection.isCollapsed) { + final leftPosition = selection.start.goLeft(editorState); + if (leftPosition != null) { + editorState.service.selectionService.updateSelection( + Selection.collapsed(leftPosition), + ); + } + } else { + editorState.service.selectionService.updateSelection( + Selection.collapsed(selection.start), + ); + } + return KeyEventResult.handled; + } else if (event.logicalKey == LogicalKeyboardKey.arrowRight) { + if (selection.isCollapsed) { + final rightPosition = selection.start.goRight(editorState); + if (rightPosition != null) { + editorState.service.selectionService.updateSelection( + Selection.collapsed(rightPosition), + ); + } + } else { + editorState.service.selectionService.updateSelection( + Selection.collapsed(selection.end), + ); + } + return KeyEventResult.handled; + } else if (event.logicalKey == LogicalKeyboardKey.arrowUp) { + final upPosition = _goUp(editorState); + editorState.updateCursorSelection( + upPosition == null ? null : Selection.collapsed(upPosition), + ); + return KeyEventResult.handled; + } else if (event.logicalKey == LogicalKeyboardKey.arrowDown) { + final downPosition = _goDown(editorState); + editorState.updateCursorSelection( + downPosition == null ? null : Selection.collapsed(downPosition), + ); + return KeyEventResult.handled; + } + return KeyEventResult.ignored; } extension on Position { Position? goLeft(EditorState editorState) { - final node = editorState.document.nodeAtPath(path)!; + final node = editorState.document.nodeAtPath(path); + if (node == null) { + return null; + } if (offset == 0) { - final prevNode = node.previous; - if (prevNode != null) { - return Position( - path: prevNode.path, offset: _endOffsetOfNode(prevNode)); + final previousEnd = node.previous?.selectable?.end(); + if (previousEnd != null) { + return previousEnd; } return null; } - if (node is TextNode) { return Position(path: path, offset: node.delta.prevRunePosition(offset)); } else { @@ -29,16 +228,18 @@ extension on Position { } Position? goRight(EditorState editorState) { - final node = editorState.document.nodeAtPath(path)!; - final lengthOfNode = _endOffsetOfNode(node); - if (offset >= lengthOfNode) { - final nextNode = node.next; - if (nextNode != null) { - return Position(path: nextNode.path, offset: 0); + final node = editorState.document.nodeAtPath(path); + if (node == null) { + return null; + } + final end = node.selectable?.end(); + if (end != null && offset >= end.offset) { + final nextStart = node.next?.selectable?.start(); + if (nextStart != null) { + return nextStart; } return null; } - if (node is TextNode) { return Position(path: path, offset: node.delta.nextRunePosition(offset)); } else { @@ -48,106 +249,43 @@ extension on Position { } Position? _goUp(EditorState editorState) { + final selection = editorState.service.selectionService.currentSelection.value; final rects = editorState.service.selectionService.selectionRects; - if (rects.isEmpty) { + if (rects.isEmpty || selection == null) { return null; } - final first = rects.first; - final firstOffset = Offset(first.left, first.top); - final hitOffset = firstOffset - Offset(0, first.height * 0.5); - return editorState.service.selectionService.getPositionInOffset(hitOffset); + Offset offset; + if (selection.isBackward) { + final rect = rects.reduce( + (current, next) => current.bottom >= next.bottom ? current : next, + ); + offset = rect.topRight.translate(0, -rect.height); + } else { + final rect = rects.reduce( + (current, next) => current.top <= next.top ? current : next, + ); + offset = rect.topLeft.translate(0, -rect.height); + } + return editorState.service.selectionService.getPositionInOffset(offset); } Position? _goDown(EditorState editorState) { + final selection = editorState.service.selectionService.currentSelection.value; final rects = editorState.service.selectionService.selectionRects; - if (rects.isEmpty) { + if (rects.isEmpty || selection == null) { return null; } - final first = rects.last; - final firstOffset = Offset(first.right, first.bottom); - final hitOffset = firstOffset + Offset(0, first.height * 0.5); - return editorState.service.selectionService.getPositionInOffset(hitOffset); + Offset offset; + if (selection.isBackward) { + final rect = rects.reduce( + (current, next) => current.bottom >= next.bottom ? current : next, + ); + offset = rect.bottomRight.translate(0, rect.height); + } else { + final rect = rects.reduce( + (current, next) => current.top <= next.top ? current : next, + ); + offset = rect.bottomLeft.translate(0, rect.height); + } + return editorState.service.selectionService.getPositionInOffset(offset); } - -KeyEventResult _handleShiftKey(EditorState editorState, RawKeyEvent event) { - final currentSelection = editorState.cursorSelection; - if (currentSelection == null) { - return KeyEventResult.ignored; - } - - if (event.logicalKey == LogicalKeyboardKey.arrowLeft) { - final leftPosition = currentSelection.end.goLeft(editorState); - editorState.updateCursorSelection(leftPosition == null - ? null - : Selection(start: currentSelection.start, end: leftPosition)); - return KeyEventResult.handled; - } else if (event.logicalKey == LogicalKeyboardKey.arrowRight) { - final rightPosition = currentSelection.start.goRight(editorState); - editorState.updateCursorSelection(rightPosition == null - ? null - : Selection(start: rightPosition, end: currentSelection.end)); - return KeyEventResult.handled; - } else if (event.logicalKey == LogicalKeyboardKey.arrowUp) { - final position = _goUp(editorState); - editorState.updateCursorSelection(position == null - ? null - : Selection(start: position, end: currentSelection.end)); - return KeyEventResult.handled; - } else if (event.logicalKey == LogicalKeyboardKey.arrowDown) { - final position = _goDown(editorState); - editorState.updateCursorSelection(position == null - ? null - : Selection(start: currentSelection.start, end: position)); - return KeyEventResult.handled; - } - return KeyEventResult.ignored; -} - -AppFlowyKeyEventHandler arrowKeysHandler = (editorState, event) { - if (event.isShiftPressed) { - return _handleShiftKey(editorState, event); - } - - final currentSelection = editorState.cursorSelection; - if (currentSelection == null) { - return KeyEventResult.ignored; - } - - if (event.logicalKey == LogicalKeyboardKey.arrowLeft) { - if (currentSelection.isCollapsed) { - final leftPosition = currentSelection.start.goLeft(editorState); - if (leftPosition != null) { - editorState.updateCursorSelection(Selection.collapsed(leftPosition)); - } - } else { - editorState.updateCursorSelection( - currentSelection.collapse(atStart: currentSelection.isBackward), - ); - } - return KeyEventResult.handled; - } else if (event.logicalKey == LogicalKeyboardKey.arrowRight) { - if (currentSelection.isCollapsed) { - final rightPosition = currentSelection.end.goRight(editorState); - if (rightPosition != null) { - editorState.updateCursorSelection(Selection.collapsed(rightPosition)); - } - } else { - editorState.updateCursorSelection( - currentSelection.collapse(atStart: !currentSelection.isBackward), - ); - } - return KeyEventResult.handled; - } else if (event.logicalKey == LogicalKeyboardKey.arrowUp) { - final position = _goUp(editorState); - editorState.updateCursorSelection( - position == null ? null : Selection.collapsed(position)); - return KeyEventResult.handled; - } else if (event.logicalKey == LogicalKeyboardKey.arrowDown) { - final position = _goDown(editorState); - editorState.updateCursorSelection( - position == null ? null : Selection.collapsed(position)); - return KeyEventResult.handled; - } - - return KeyEventResult.ignored; -}; diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart index 3f49a4b566..f6dba97c49 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart @@ -1,13 +1,12 @@ import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/src/infra/html_converter.dart'; import 'package:appflowy_editor/src/document/node_iterator.dart'; -import 'package:appflowy_editor/src/infra/log.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:rich_clipboard/rich_clipboard.dart'; _handleCopy(EditorState editorState) async { - final selection = editorState.cursorSelection?.normalize(); + final selection = editorState.cursorSelection?.normalize; if (selection == null || selection.isCollapsed) { return; } @@ -43,7 +42,7 @@ _handleCopy(EditorState editorState) async { } _pasteHTML(EditorState editorState, String html) { - final selection = editorState.cursorSelection?.normalize(); + final selection = editorState.cursorSelection?.normalize; if (selection == null) { return; } @@ -191,7 +190,7 @@ Delta _lineContentToDelta(String lineContent) { } _handlePastePlainText(EditorState editorState, String plainText) { - final selection = editorState.cursorSelection?.normalize(); + final selection = editorState.cursorSelection?.normalize; if (selection == null) { return; } @@ -256,7 +255,7 @@ _handleCut(EditorState editorState) { } _deleteSelectedContent(EditorState editorState) { - final selection = editorState.cursorSelection?.normalize(); + final selection = editorState.cursorSelection?.normalize; if (selection == null || selection.isCollapsed) { return; } diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/default_key_event_handlers.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/default_key_event_handlers.dart index a8cbdee3ab..f8c7fa71cb 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/default_key_event_handlers.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/default_key_event_handlers.dart @@ -1,7 +1,7 @@ -import 'package:appflowy_editor/src/service/internal_key_event_handlers/arrow_keys_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/backspace_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/arrow_keys_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/slash_handler.dart'; import 'package:appflowy_editor/src/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart'; @@ -13,6 +13,7 @@ import 'package:appflowy_editor/src/service/keyboard_service.dart'; List defaultKeyEventHandlers = [ deleteTextHandler, slashShortcutHandler, + // arrowKeysHandler, arrowKeysHandler, copyPasteKeysHandler, redoUndoKeysHandler, diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/legacy/arrow_keys_handler.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/legacy/arrow_keys_handler.dart new file mode 100644 index 0000000000..ecaf3325e4 --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/legacy/arrow_keys_handler.dart @@ -0,0 +1,153 @@ +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +int _endOffsetOfNode(Node node) { + if (node is TextNode) { + return node.delta.length; + } + return 0; +} + +extension on Position { + Position? goLeft(EditorState editorState) { + final node = editorState.document.nodeAtPath(path)!; + if (offset == 0) { + final prevNode = node.previous; + if (prevNode != null) { + return Position( + path: prevNode.path, offset: _endOffsetOfNode(prevNode)); + } + return null; + } + + if (node is TextNode) { + return Position(path: path, offset: node.delta.prevRunePosition(offset)); + } else { + return Position(path: path, offset: offset); + } + } + + Position? goRight(EditorState editorState) { + final node = editorState.document.nodeAtPath(path)!; + final lengthOfNode = _endOffsetOfNode(node); + if (offset >= lengthOfNode) { + final nextNode = node.next; + if (nextNode != null) { + return Position(path: nextNode.path, offset: 0); + } + return null; + } + + if (node is TextNode) { + return Position(path: path, offset: node.delta.nextRunePosition(offset)); + } else { + return Position(path: path, offset: offset); + } + } +} + +Position? _goUp(EditorState editorState) { + final rects = editorState.service.selectionService.selectionRects; + if (rects.isEmpty) { + return null; + } + final first = rects.first; + final firstOffset = Offset(first.left, first.top); + final hitOffset = firstOffset - Offset(0, first.height * 0.5); + return editorState.service.selectionService.getPositionInOffset(hitOffset); +} + +Position? _goDown(EditorState editorState) { + final rects = editorState.service.selectionService.selectionRects; + if (rects.isEmpty) { + return null; + } + final first = rects.last; + final firstOffset = Offset(first.right, first.bottom); + final hitOffset = firstOffset + Offset(0, first.height * 0.5); + return editorState.service.selectionService.getPositionInOffset(hitOffset); +} + +KeyEventResult _handleShiftKey(EditorState editorState, RawKeyEvent event) { + final currentSelection = editorState.cursorSelection; + if (currentSelection == null) { + return KeyEventResult.ignored; + } + + if (event.logicalKey == LogicalKeyboardKey.arrowLeft) { + final leftPosition = currentSelection.end.goLeft(editorState); + editorState.updateCursorSelection(leftPosition == null + ? null + : Selection(start: currentSelection.start, end: leftPosition)); + return KeyEventResult.handled; + } else if (event.logicalKey == LogicalKeyboardKey.arrowRight) { + final rightPosition = currentSelection.start.goRight(editorState); + editorState.updateCursorSelection(rightPosition == null + ? null + : Selection(start: rightPosition, end: currentSelection.end)); + return KeyEventResult.handled; + } else if (event.logicalKey == LogicalKeyboardKey.arrowUp) { + final position = _goUp(editorState); + editorState.updateCursorSelection(position == null + ? null + : Selection(start: position, end: currentSelection.end)); + return KeyEventResult.handled; + } else if (event.logicalKey == LogicalKeyboardKey.arrowDown) { + final position = _goDown(editorState); + editorState.updateCursorSelection(position == null + ? null + : Selection(start: currentSelection.start, end: position)); + return KeyEventResult.handled; + } + return KeyEventResult.ignored; +} + +AppFlowyKeyEventHandler arrowKeysHandler = (editorState, event) { + if (event.isShiftPressed) { + return _handleShiftKey(editorState, event); + } + + final currentSelection = editorState.cursorSelection; + if (currentSelection == null) { + return KeyEventResult.ignored; + } + + if (event.logicalKey == LogicalKeyboardKey.arrowLeft) { + if (currentSelection.isCollapsed) { + final leftPosition = currentSelection.start.goLeft(editorState); + if (leftPosition != null) { + editorState.updateCursorSelection(Selection.collapsed(leftPosition)); + } + } else { + editorState.updateCursorSelection( + currentSelection.collapse(atStart: currentSelection.isBackward), + ); + } + return KeyEventResult.handled; + } else if (event.logicalKey == LogicalKeyboardKey.arrowRight) { + if (currentSelection.isCollapsed) { + final rightPosition = currentSelection.end.goRight(editorState); + if (rightPosition != null) { + editorState.updateCursorSelection(Selection.collapsed(rightPosition)); + } + } else { + editorState.updateCursorSelection( + currentSelection.collapse(atStart: !currentSelection.isBackward), + ); + } + return KeyEventResult.handled; + } else if (event.logicalKey == LogicalKeyboardKey.arrowUp) { + final position = _goUp(editorState); + editorState.updateCursorSelection( + position == null ? null : Selection.collapsed(position)); + return KeyEventResult.handled; + } else if (event.logicalKey == LogicalKeyboardKey.arrowDown) { + final position = _goDown(editorState); + editorState.updateCursorSelection( + position == null ? null : Selection.collapsed(position)); + return KeyEventResult.handled; + } + + return KeyEventResult.ignored; +}; diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/slash_handler.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/slash_handler.dart index 32e27a808a..79b7ee6579 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/slash_handler.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/slash_handler.dart @@ -25,10 +25,6 @@ AppFlowyKeyEventHandler slashShortcutHandler = (editorState, event) { if (selection == null || context == null || selectable == null) { return KeyEventResult.ignored; } - final selectionRects = editorState.service.selectionService.selectionRects; - if (selectionRects.isEmpty) { - return KeyEventResult.ignored; - } TransactionBuilder(editorState) ..replaceText(textNode, selection.start.offset, selection.end.offset - selection.start.offset, event.character ?? '') diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/selection_service.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/selection_service.dart index 6f6897596f..ca442f4ff9 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/selection_service.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/selection_service.dart @@ -348,10 +348,8 @@ class _AppFlowySelectionState extends State final backwardNodes = selection.isBackward ? nodes : nodes.reversed.toList(growable: false); - final backwardSelection = selection.isBackward - ? selection - : selection.copyWith(start: selection.end, end: selection.start); - assert(backwardSelection.isBackward); + final normalizedSelection = selection.normalize; + assert(normalizedSelection.isBackward); for (var i = 0; i < backwardNodes.length; i++) { final node = backwardNodes[i]; @@ -360,7 +358,7 @@ class _AppFlowySelectionState extends State continue; } - var newSelection = backwardSelection.copy(); + var newSelection = normalizedSelection.copy(); /// In the case of multiple selections, /// we need to return a new selection for each selected node individually. @@ -370,7 +368,7 @@ class _AppFlowySelectionState extends State /// text: ghijkl /// text: mn>opqr /// - if (!backwardSelection.isSingle) { + if (!normalizedSelection.isSingle) { if (i == 0) { newSelection = newSelection.copyWith(end: selectable.end()); } else if (i == nodes.length - 1) { diff --git a/frontend/app_flowy/packages/appflowy_editor/test/infra/test_raw_key_event.dart b/frontend/app_flowy/packages/appflowy_editor/test/infra/test_raw_key_event.dart index 2450c4e6db..47cacc18b1 100644 --- a/frontend/app_flowy/packages/appflowy_editor/test/infra/test_raw_key_event.dart +++ b/frontend/app_flowy/packages/appflowy_editor/test/infra/test_raw_key_event.dart @@ -103,6 +103,9 @@ extension on LogicalKeyboardKey { if (this == LogicalKeyboardKey.slash) { return PhysicalKeyboardKey.slash; } + if (this == LogicalKeyboardKey.arrowUp) { + return PhysicalKeyboardKey.arrowUp; + } if (this == LogicalKeyboardKey.arrowDown) { return PhysicalKeyboardKey.arrowDown; } diff --git a/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/arrow_keys_handler_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/arrow_keys_handler_test.dart index e4631b56ad..ebbd71a392 100644 --- a/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/arrow_keys_handler_test.dart +++ b/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/arrow_keys_handler_test.dart @@ -58,6 +58,219 @@ void main() async { (tester) async { await _testPressArrowKeyInNotCollapsedSelection(tester, false); }); + + testWidgets('Presses arrow left/right + shift in collapsed selection', + (tester) async { + const text = 'Welcome to Appflowy'; + final editor = tester.editor + ..insertTextNode(text) + ..insertTextNode(text); + await editor.startTesting(); + const offset = 8; + final selection = Selection.single(path: [1], startOffset: offset); + await editor.updateSelection(selection); + for (var i = offset - 1; i >= 0; i--) { + await editor.pressLogicKey( + LogicalKeyboardKey.arrowLeft, + isShiftPressed: true, + ); + expect( + editor.documentSelection, + selection.copyWith( + end: Position(path: [1], offset: i), + ), + ); + } + for (var i = text.length; i >= 0; i--) { + await editor.pressLogicKey( + LogicalKeyboardKey.arrowLeft, + isShiftPressed: true, + ); + expect( + editor.documentSelection, + selection.copyWith( + end: Position(path: [0], offset: i), + ), + ); + } + for (var i = 1; i <= text.length; i++) { + await editor.pressLogicKey( + LogicalKeyboardKey.arrowRight, + isShiftPressed: true, + ); + expect( + editor.documentSelection, + selection.copyWith( + end: Position(path: [0], offset: i), + ), + ); + } + for (var i = 0; i < text.length; i++) { + await editor.pressLogicKey( + LogicalKeyboardKey.arrowRight, + isShiftPressed: true, + ); + expect( + editor.documentSelection, + selection.copyWith( + end: Position(path: [1], offset: i), + ), + ); + } + }); + + testWidgets( + 'Presses arrow left/right + shift in not collapsed and backward selection', + (tester) async { + const text = 'Welcome to Appflowy'; + final editor = tester.editor + ..insertTextNode(text) + ..insertTextNode(text); + await editor.startTesting(); + const start = 8; + const end = 12; + final selection = Selection.single( + path: [0], + startOffset: start, + endOffset: end, + ); + await editor.updateSelection(selection); + for (var i = end + 1; i <= text.length; i++) { + await editor.pressLogicKey( + LogicalKeyboardKey.arrowRight, + isShiftPressed: true, + ); + expect( + editor.documentSelection, + selection.copyWith( + end: Position(path: [0], offset: i), + ), + ); + } + for (var i = text.length - 1; i >= 0; i--) { + await editor.pressLogicKey( + LogicalKeyboardKey.arrowLeft, + isShiftPressed: true, + ); + expect( + editor.documentSelection, + selection.copyWith( + end: Position(path: [0], offset: i), + ), + ); + } + }); + + testWidgets( + 'Presses arrow left/right + command in not collapsed and forward selection', + (tester) async { + const text = 'Welcome to Appflowy'; + final editor = tester.editor + ..insertTextNode(text) + ..insertTextNode(text); + await editor.startTesting(); + const start = 12; + const end = 8; + final selection = Selection.single( + path: [0], + startOffset: start, + endOffset: end, + ); + await editor.updateSelection(selection); + for (var i = end - 1; i >= 0; i--) { + await editor.pressLogicKey( + LogicalKeyboardKey.arrowLeft, + isShiftPressed: true, + ); + expect( + editor.documentSelection, + selection.copyWith( + end: Position(path: [0], offset: i), + ), + ); + } + for (var i = 1; i <= text.length; i++) { + await editor.pressLogicKey( + LogicalKeyboardKey.arrowRight, + isShiftPressed: true, + ); + expect( + editor.documentSelection, + selection.copyWith( + end: Position(path: [0], offset: i), + ), + ); + } + }); + + testWidgets('Presses arrow left/right/up/down + meta in collapsed selection', + (tester) async { + await _testPressArrowKeyWithMetaInSelection(tester, true, false); + }); + + testWidgets( + 'Presses arrow left/right/up/down + meta in not collapsed and backward selection', + (tester) async { + await _testPressArrowKeyWithMetaInSelection(tester, false, true); + }); + + testWidgets( + 'Presses arrow left/right/up/down + meta in not collapsed and forward selection', + (tester) async { + await _testPressArrowKeyWithMetaInSelection(tester, false, false); + }); + + testWidgets('Presses arrow up/down + shift in not collapsed selection', + (tester) async { + const text = 'Welcome to Appflowy 😁'; + final editor = tester.editor + ..insertTextNode(text) + ..insertTextNode(text) + ..insertTextNode(null) + ..insertTextNode(text) + ..insertTextNode(null) + ..insertTextNode(text) + ..insertTextNode(text); + await editor.startTesting(); + final selection = Selection.single(path: [3], startOffset: 8); + await editor.updateSelection(selection); + for (int i = 0; i < 3; i++) { + await editor.pressLogicKey( + LogicalKeyboardKey.arrowUp, + isShiftPressed: true, + ); + } + expect( + editor.documentSelection, + selection.copyWith( + end: Position(path: [0], offset: 0), + ), + ); + for (int i = 0; i < 7; i++) { + await editor.pressLogicKey( + LogicalKeyboardKey.arrowDown, + isShiftPressed: true, + ); + } + expect( + editor.documentSelection, + selection.copyWith( + end: Position(path: [6], offset: 0), + ), + ); + for (int i = 0; i < 3; i++) { + await editor.pressLogicKey( + LogicalKeyboardKey.arrowUp, + isShiftPressed: true, + ); + } + expect( + editor.documentSelection, + selection.copyWith( + end: Position(path: [3], offset: 0), + ), + ); + }); } Future _testPressArrowKeyInNotCollapsedSelection( @@ -82,3 +295,72 @@ Future _testPressArrowKeyInNotCollapsedSelection( await editor.pressLogicKey(LogicalKeyboardKey.arrowRight); expect(editor.documentSelection?.end, end); } + +Future _testPressArrowKeyWithMetaInSelection( + WidgetTester tester, + bool isSingle, + bool isBackward, +) async { + const text = 'Welcome to Appflowy'; + final editor = tester.editor + ..insertTextNode(text) + ..insertTextNode(text); + await editor.startTesting(); + Selection selection; + if (isSingle) { + selection = Selection.single( + path: [0], + startOffset: 8, + ); + } else { + if (isBackward) { + selection = Selection.single( + path: [0], + startOffset: 8, + endOffset: text.length, + ); + } else { + selection = Selection.single( + path: [0], + startOffset: text.length, + endOffset: 8, + ); + } + } + await editor.updateSelection(selection); + await editor.pressLogicKey( + LogicalKeyboardKey.arrowLeft, + isMetaPressed: true, + ); + expect( + editor.documentSelection, + Selection.single(path: [0], startOffset: 0), + ); + + await editor.pressLogicKey( + LogicalKeyboardKey.arrowRight, + isMetaPressed: true, + ); + expect( + editor.documentSelection, + Selection.single(path: [0], startOffset: text.length), + ); + + await editor.pressLogicKey( + LogicalKeyboardKey.arrowUp, + isMetaPressed: true, + ); + expect( + editor.documentSelection, + Selection.single(path: [0], startOffset: 0), + ); + + await editor.pressLogicKey( + LogicalKeyboardKey.arrowDown, + isMetaPressed: true, + ); + expect( + editor.documentSelection, + Selection.single(path: [1], startOffset: text.length), + ); +} From b5c3b6115854dc442d5ec46b6391b91de8dc05d8 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Wed, 31 Aug 2022 15:49:01 +0800 Subject: [PATCH 096/106] fix: #955 upgrade build_resolvers to 2.0.9 --- .github/workflows/flowy_editor_test.yml | 4 ++++ frontend/app_flowy/pubspec.lock | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/flowy_editor_test.yml b/.github/workflows/flowy_editor_test.yml index 1bff599ace..1459a47518 100644 --- a/.github/workflows/flowy_editor_test.yml +++ b/.github/workflows/flowy_editor_test.yml @@ -4,10 +4,14 @@ on: push: branches: - "main" + paths: + - "frontend/app_flowy/packages/appflowy_editor" pull_request: branches: - "main" + paths: + - "frontend/app_flowy/packages/appflowy_editor" env: CARGO_TERM_COLOR: always diff --git a/frontend/app_flowy/pubspec.lock b/frontend/app_flowy/pubspec.lock index 8466696ec9..80f8f2560b 100644 --- a/frontend/app_flowy/pubspec.lock +++ b/frontend/app_flowy/pubspec.lock @@ -98,7 +98,7 @@ packages: name: build_resolvers url: "https://pub.dartlang.org" source: hosted - version: "2.0.8" + version: "2.0.9" build_runner: dependency: "direct dev" description: From a7cbb3d31aaa624cba770c73b3ac248cd14ac742 Mon Sep 17 00:00:00 2001 From: appflowy Date: Wed, 31 Aug 2022 16:25:00 +0800 Subject: [PATCH 097/106] chore: show board setting --- .../toolbar/board_setting_bloc.dart | 46 +++++ .../board/presentation/board_page.dart | 63 +++++-- .../presentation/toolbar/board_setting.dart | 168 ++++++++++++++++++ .../presentation/toolbar/board_toolbar.dart | 60 +++++++ 4 files changed, 320 insertions(+), 17 deletions(-) create mode 100644 frontend/app_flowy/lib/plugins/board/application/toolbar/board_setting_bloc.dart create mode 100644 frontend/app_flowy/lib/plugins/board/presentation/toolbar/board_setting.dart create mode 100644 frontend/app_flowy/lib/plugins/board/presentation/toolbar/board_toolbar.dart diff --git a/frontend/app_flowy/lib/plugins/board/application/toolbar/board_setting_bloc.dart b/frontend/app_flowy/lib/plugins/board/application/toolbar/board_setting_bloc.dart new file mode 100644 index 0000000000..480b3a4768 --- /dev/null +++ b/frontend/app_flowy/lib/plugins/board/application/toolbar/board_setting_bloc.dart @@ -0,0 +1,46 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'dart:async'; +import 'package:dartz/dartz.dart'; + +part 'board_setting_bloc.freezed.dart'; + +class BoardSettingBloc extends Bloc { + final String gridId; + BoardSettingBloc({required this.gridId}) + : super(BoardSettingState.initial()) { + on( + (event, emit) async { + event.when(performAction: (action) { + emit(state.copyWith(selectedAction: Some(action))); + }); + }, + ); + } + + @override + Future close() async { + return super.close(); + } +} + +@freezed +class BoardSettingEvent with _$BoardSettingEvent { + const factory BoardSettingEvent.performAction(BoardSettingAction action) = + _PerformAction; +} + +@freezed +class BoardSettingState with _$BoardSettingState { + const factory BoardSettingState({ + required Option selectedAction, + }) = _BoardSettingState; + + factory BoardSettingState.initial() => BoardSettingState( + selectedAction: none(), + ); +} + +enum BoardSettingAction { + properties, +} diff --git a/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart b/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart index bc8a4fcfa7..7235217d4c 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart @@ -24,6 +24,7 @@ import '../../grid/application/row/row_cache.dart'; import '../application/board_bloc.dart'; import 'card/card.dart'; import 'card/card_cell_builder.dart'; +import 'toolbar/board_toolbar.dart'; class BoardPage extends StatelessWidget { final ViewPB view; @@ -100,25 +101,34 @@ class _BoardContentState extends State { buildWhen: (previous, current) => previous.groupIds.length != current.groupIds.length, builder: (context, state) { + final theme = context.read(); return Container( - color: Colors.white, + color: theme.surface, child: Padding( - padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 20), - child: AFBoard( - scrollManager: scrollManager, - scrollController: scrollController, - dataController: context.read().boardController, - headerBuilder: _buildHeader, - footBuilder: _buildFooter, - cardBuilder: (_, column, columnItem) => _buildCard( - context, - column, - columnItem, - ), - columnConstraints: const BoxConstraints.tightFor(width: 300), - config: AFBoardConfig( - columnBackgroundColor: HexColor.fromHex('#F7F8FC'), - ), + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Column( + children: [ + const _ToolbarBlocAdaptor(), + Expanded( + child: AFBoard( + scrollManager: scrollManager, + scrollController: scrollController, + dataController: context.read().boardController, + headerBuilder: _buildHeader, + footBuilder: _buildFooter, + cardBuilder: (_, column, columnItem) => _buildCard( + context, + column, + columnItem, + ), + columnConstraints: + const BoxConstraints.tightFor(width: 300), + config: AFBoardConfig( + columnBackgroundColor: HexColor.fromHex('#F7F8FC'), + ), + ), + ), + ], ), ), ); @@ -277,6 +287,25 @@ class _BoardContentState extends State { } } +class _ToolbarBlocAdaptor extends StatelessWidget { + const _ToolbarBlocAdaptor({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + final bloc = context.read(); + final toolbarContext = BoardToolbarContext( + viewId: bloc.gridId, + fieldCache: bloc.fieldCache, + ); + + return BoardToolbar(toolbarContext: toolbarContext); + }, + ); + } +} + extension HexColor on Color { static Color fromHex(String hexString) { final buffer = StringBuffer(); diff --git a/frontend/app_flowy/lib/plugins/board/presentation/toolbar/board_setting.dart b/frontend/app_flowy/lib/plugins/board/presentation/toolbar/board_setting.dart new file mode 100644 index 0000000000..76ab265a90 --- /dev/null +++ b/frontend/app_flowy/lib/plugins/board/presentation/toolbar/board_setting.dart @@ -0,0 +1,168 @@ +import 'package:app_flowy/generated/locale_keys.g.dart'; +import 'package:app_flowy/plugins/board/application/toolbar/board_setting_bloc.dart'; +import 'package:app_flowy/plugins/grid/application/field/field_cache.dart'; +import 'package:app_flowy/plugins/grid/presentation/layout/sizes.dart'; +import 'package:app_flowy/plugins/grid/presentation/widgets/toolbar/grid_property.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/image.dart'; +import 'package:flowy_infra/theme.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flowy_infra_ui/style_widget/button.dart'; +import 'package:flowy_infra_ui/style_widget/scrolling/styled_list.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:flowy_infra_ui/widget/spacing.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'board_toolbar.dart'; + +class BoardSettingContext { + final String viewId; + final GridFieldCache fieldCache; + BoardSettingContext({ + required this.viewId, + required this.fieldCache, + }); + + factory BoardSettingContext.from(BoardToolbarContext toolbarContext) => + BoardSettingContext( + viewId: toolbarContext.viewId, + fieldCache: toolbarContext.fieldCache, + ); +} + +class BoardSettingList extends StatelessWidget { + final BoardSettingContext settingContext; + final Function(BoardSettingAction, BoardSettingContext) onAction; + const BoardSettingList({ + required this.settingContext, + required this.onAction, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => BoardSettingBloc(gridId: settingContext.viewId), + child: BlocListener( + listenWhen: (previous, current) => + previous.selectedAction != current.selectedAction, + listener: (context, state) { + state.selectedAction.foldLeft(null, (_, action) { + FlowyOverlay.of(context).remove(identifier()); + onAction(action, settingContext); + }); + }, + child: BlocBuilder( + builder: (context, state) { + return _renderList(); + }, + ), + ), + ); + } + + Widget _renderList() { + final cells = BoardSettingAction.values.map((action) { + return _SettingItem(action: action); + }).toList(); + + return SizedBox( + width: 140, + child: ListView.separated( + shrinkWrap: true, + controller: ScrollController(), + itemCount: cells.length, + separatorBuilder: (context, index) { + return VSpace(GridSize.typeOptionSeparatorHeight); + }, + physics: StyledScrollPhysics(), + itemBuilder: (BuildContext context, int index) { + return cells[index]; + }, + ), + ); + } + + static void show(BuildContext context, BoardSettingContext settingContext) { + final list = BoardSettingList( + settingContext: settingContext, + onAction: (action, settingContext) { + switch (action) { + case BoardSettingAction.properties: + GridPropertyList( + gridId: settingContext.viewId, + fieldCache: settingContext.fieldCache) + .show(context); + break; + } + }, + ); + + FlowyOverlay.of(context).insertWithAnchor( + widget: OverlayContainer( + constraints: BoxConstraints.loose(const Size(140, 400)), + child: list, + ), + identifier: identifier(), + anchorContext: context, + anchorDirection: AnchorDirection.bottomRight, + style: FlowyOverlayStyle(blur: false), + ); + } + + static String identifier() { + return (BoardSettingList).toString(); + } +} + +class _SettingItem extends StatelessWidget { + final BoardSettingAction action; + + const _SettingItem({ + required this.action, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final theme = context.read(); + final isSelected = context + .read() + .state + .selectedAction + .foldLeft(false, (_, selectedAction) => selectedAction == action); + + return SizedBox( + height: 30, + child: FlowyButton( + isSelected: isSelected, + text: FlowyText.medium(action.title(), + fontSize: 12, color: theme.textColor), + hoverColor: theme.hover, + onTap: () { + context + .read() + .add(BoardSettingEvent.performAction(action)); + }, + leftIcon: svgWidget(action.iconName(), color: theme.iconColor), + ), + ); + } +} + +extension _GridSettingExtension on BoardSettingAction { + String iconName() { + switch (this) { + case BoardSettingAction.properties: + return 'grid/setting/properties'; + } + } + + String title() { + switch (this) { + case BoardSettingAction.properties: + return LocaleKeys.grid_settings_Properties.tr(); + } + } +} diff --git a/frontend/app_flowy/lib/plugins/board/presentation/toolbar/board_toolbar.dart b/frontend/app_flowy/lib/plugins/board/presentation/toolbar/board_toolbar.dart new file mode 100644 index 0000000000..fae27851a9 --- /dev/null +++ b/frontend/app_flowy/lib/plugins/board/presentation/toolbar/board_toolbar.dart @@ -0,0 +1,60 @@ +import 'package:app_flowy/plugins/grid/application/field/field_cache.dart'; +import 'package:flowy_infra/image.dart'; +import 'package:flowy_infra/theme.dart'; +import 'package:flowy_infra_ui/style_widget/icon_button.dart'; +import 'package:flutter/widgets.dart'; +import 'package:provider/provider.dart'; + +import 'board_setting.dart'; + +class BoardToolbarContext { + final String viewId; + final GridFieldCache fieldCache; + + BoardToolbarContext({ + required this.viewId, + required this.fieldCache, + }); +} + +class BoardToolbar extends StatelessWidget { + final BoardToolbarContext toolbarContext; + const BoardToolbar({ + required this.toolbarContext, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 40, + child: Row( + children: [ + _SettingButton( + settingContext: BoardSettingContext.from(toolbarContext), + ), + ], + ), + ); + } +} + +class _SettingButton extends StatelessWidget { + final BoardSettingContext settingContext; + const _SettingButton({required this.settingContext, Key? key}) + : super(key: key); + + @override + Widget build(BuildContext context) { + final theme = context.read(); + return FlowyIconButton( + hoverColor: theme.hover, + width: 22, + onPressed: () => BoardSettingList.show(context, settingContext), + icon: Padding( + padding: const EdgeInsets.symmetric(vertical: 3.0, horizontal: 3.0), + child: svgWidget("grid/setting/setting"), + ), + ); + } +} From c9d575301f0bb1f7ac68e7c085477e27a65eb6d4 Mon Sep 17 00:00:00 2001 From: appflowy Date: Tue, 30 Aug 2022 22:36:13 +0800 Subject: [PATCH 098/106] chore: edit card directly by clicking edit button --- .../board/presentation/card/board_cell.dart | 8 +++ .../presentation/card/board_text_cell.dart | 55 +++++++++++++------ .../plugins/board/presentation/card/card.dart | 1 + .../appflowy_board/lib/src/widgets/board.dart | 1 - 4 files changed, 48 insertions(+), 17 deletions(-) diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/board_cell.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/board_cell.dart index 4a7455f365..580ebe6c5e 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/board_cell.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/board_cell.dart @@ -34,6 +34,14 @@ class EditableRowNotifier { notifier.resignFirstResponder.notify(); } } + + void dispose() { + for (final notifier in cells.values) { + notifier.resignFirstResponder.notify(); + } + + cells.clear(); + } } abstract class EditableCell { diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/board_text_cell.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/board_text_cell.dart index 49048d2472..6bb114de21 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/board_text_cell.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/board_text_cell.dart @@ -1,6 +1,7 @@ import 'package:app_flowy/plugins/board/application/card/board_text_cell_bloc.dart'; import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart'; import 'package:app_flowy/plugins/grid/presentation/widgets/cell/cell_builder.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -42,9 +43,17 @@ class _BoardTextCellState extends State { focusNode.requestFocus(); } + focusNode.addListener(() { + if (!focusNode.hasFocus) { + _cellBloc.add(const BoardTextCellEvent.enableEdit(false)); + } + }); + widget.editableNotifier?.becomeFirstResponder.addListener(() { if (!mounted) return; - focusNode.requestFocus(); + WidgetsBinding.instance.addPostFrameCallback((_) { + focusNode.requestFocus(); + }); _cellBloc.add(const BoardTextCellEvent.enableEdit(true)); }); @@ -67,22 +76,36 @@ class _BoardTextCellState extends State { } }, child: BlocBuilder( - buildWhen: (previous, current) => - previous.enableEdit != current.enableEdit, builder: (context, state) { - return TextField( - // autofocus: true, - // enabled: state.enableEdit, - controller: _controller, - focusNode: focusNode, - onChanged: (value) => focusChanged(), - onEditingComplete: () => focusNode.unfocus(), - maxLines: 1, - style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500), - decoration: const InputDecoration( - contentPadding: EdgeInsets.symmetric(vertical: 6), - border: InputBorder.none, - isDense: true, + Widget child; + if (state.enableEdit) { + child = TextField( + controller: _controller, + focusNode: focusNode, + onChanged: (value) => focusChanged(), + onEditingComplete: () => focusNode.unfocus(), + maxLines: 1, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + fontFamily: 'Mulish', + ), + decoration: const InputDecoration( + // Magic number 4 makes the textField take up the same space as FlowyText + contentPadding: EdgeInsets.symmetric(vertical: 4), + border: InputBorder.none, + isDense: true, + ), + ); + } else { + child = FlowyText.medium(state.content, fontSize: 14); + } + + return Align( + alignment: Alignment.centerLeft, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 6), + child: child, ), ); }, diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart index 6c0aaf3c48..a35a9e96c4 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart @@ -122,6 +122,7 @@ class _BoardCardState extends State { @override Future dispose() async { + rowNotifier.dispose(); _cardBloc.close(); super.dispose(); } diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board.dart index 5b44ec64b7..a565838da4 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board.dart @@ -250,7 +250,6 @@ class _AFBoardContentState extends State { builder: (context, value, child) { final boardColumn = AFBoardColumnWidget( // key: PageStorageKey(columnData.id), - // key: GlobalObjectKey(columnData.id), margin: _marginFromIndex(columnIndex), itemMargin: widget.config.columnItemPadding, headerBuilder: _buildHeader, From a9f5f8d508e7d10847ffe531ad037568b91f0f85 Mon Sep 17 00:00:00 2001 From: appflowy Date: Wed, 31 Aug 2022 20:21:06 +0800 Subject: [PATCH 099/106] chore: support create new field when editing the row --- .../app_flowy/assets/translations/en.json | 3 +- .../card/board_select_option_cell.dart | 5 +- .../presentation/card/board_text_cell.dart | 56 ++++++------ .../plugins/board/presentation/card/card.dart | 19 ++-- .../widgets/header/grid_header.dart | 7 +- .../presentation/widgets/row/row_detail.dart | 87 ++++++++++++++----- 6 files changed, 112 insertions(+), 65 deletions(-) diff --git a/frontend/app_flowy/assets/translations/en.json b/frontend/app_flowy/assets/translations/en.json index 1fd48702ac..953e5b02bb 100644 --- a/frontend/app_flowy/assets/translations/en.json +++ b/frontend/app_flowy/assets/translations/en.json @@ -189,7 +189,8 @@ "addSelectOption": "Add an option", "optionTitle": "Options", "addOption": "Add option", - "editProperty": "Edit property" + "editProperty": "Edit property", + "newColumn": "New column" }, "row": { "duplicate": "Duplicate", diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/board_select_option_cell.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/board_select_option_cell.dart index fb1cc15de6..8814d83a5e 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/board_select_option_cell.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/board_select_option_cell.dart @@ -65,7 +65,10 @@ class _BoardSelectOptionCellState extends State { alignment: AlignmentDirectional.center, fit: StackFit.expand, children: [ - Wrap(spacing: 4, runSpacing: 2, children: children), + Padding( + padding: const EdgeInsets.symmetric(vertical: 6), + child: Wrap(spacing: 4, runSpacing: 2, children: children), + ), _SelectOptionDialog( controller: widget.cellControllerBuilder.build(), ), diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/board_text_cell.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/board_text_cell.dart index 6bb114de21..357c5707a1 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/board_text_cell.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/board_text_cell.dart @@ -78,36 +78,38 @@ class _BoardTextCellState extends State { child: BlocBuilder( builder: (context, state) { Widget child; - if (state.enableEdit) { - child = TextField( - controller: _controller, - focusNode: focusNode, - onChanged: (value) => focusChanged(), - onEditingComplete: () => focusNode.unfocus(), - maxLines: 1, - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - fontFamily: 'Mulish', - ), - decoration: const InputDecoration( - // Magic number 4 makes the textField take up the same space as FlowyText - contentPadding: EdgeInsets.symmetric(vertical: 4), - border: InputBorder.none, - isDense: true, - ), - ); + if (state.content.isEmpty) { + child = const SizedBox(); } else { - child = FlowyText.medium(state.content, fontSize: 14); + if (state.enableEdit) { + child = TextField( + controller: _controller, + focusNode: focusNode, + onChanged: (value) => focusChanged(), + onEditingComplete: () => focusNode.unfocus(), + maxLines: 1, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + fontFamily: 'Mulish', + ), + decoration: const InputDecoration( + // Magic number 4 makes the textField take up the same space as FlowyText + contentPadding: EdgeInsets.symmetric(vertical: 4), + border: InputBorder.none, + isDense: true, + ), + ); + } else { + child = FlowyText.medium(state.content, fontSize: 14); + child = Padding( + padding: const EdgeInsets.symmetric(vertical: 6), + child: child, + ); + } } - return Align( - alignment: Alignment.centerLeft, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 6), - child: child, - ), - ); + return Align(alignment: Alignment.centerLeft, child: child); }, ), ), diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart index a35a9e96c4..9e3bd4dd48 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart @@ -100,20 +100,11 @@ class _BoardCardState extends State { ); rowNotifier.insertCell(cellId, cellNotifier); - if (index != 0) { - child = Padding( - key: cellId.key(), - padding: const EdgeInsets.only(left: 4, right: 4, top: 8), - child: child, - ); - } else { - child = Padding( - key: UniqueKey(), - padding: const EdgeInsets.only(left: 4, right: 4), - child: child, - ); - } - + child = Padding( + key: cellId.key(), + padding: const EdgeInsets.only(left: 4, right: 4), + child: child, + ); children.add(child); }, ); diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/grid_header.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/grid_header.dart index 9d33768e65..c5bdb448c3 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/grid_header.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/grid_header.dart @@ -1,7 +1,9 @@ +import 'package:app_flowy/generated/locale_keys.g.dart'; import 'package:app_flowy/plugins/grid/application/field/field_cache.dart'; import 'package:app_flowy/plugins/grid/application/field/type_option/type_option_context.dart'; import 'package:app_flowy/startup/startup.dart'; import 'package:app_flowy/plugins/grid/application/prelude.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/image.dart'; import 'package:flowy_infra/theme.dart'; import 'package:flowy_infra_ui/style_widget/button.dart'; @@ -155,7 +157,10 @@ class CreateFieldButton extends StatelessWidget { final theme = context.watch(); return FlowyButton( - text: const FlowyText.medium('New column', fontSize: 12), + text: FlowyText.medium( + LocaleKeys.grid_field_newColumn.tr(), + fontSize: 12, + ), hoverColor: theme.shader6, onTap: () => FieldEditor( gridId: gridId, diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/row_detail.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/row_detail.dart index 7d07e9fa3f..58e9b2808c 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/row_detail.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/row_detail.dart @@ -5,8 +5,10 @@ import 'package:app_flowy/plugins/grid/application/row/row_detail_bloc.dart'; import 'package:flowy_infra/image.dart'; import 'package:flowy_infra/theme.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flowy_infra_ui/style_widget/button.dart'; import 'package:flowy_infra_ui/style_widget/icon_button.dart'; import 'package:flowy_infra_ui/style_widget/scrolling/styled_scroll_bar.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flowy_infra_ui/widget/spacing.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:app_flowy/generated/locale_keys.g.dart'; @@ -67,16 +69,21 @@ class _RowDetailPageState extends State { return bloc; }, child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 40, vertical: 20), + padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 20), child: Column( children: [ SizedBox( - height: 40, + height: 30, child: Row( children: const [Spacer(), _CloseButton()], ), ), - Expanded(child: _PropertyList(cellBuilder: widget.cellBuilder)), + Expanded( + child: _PropertyList( + cellBuilder: widget.cellBuilder, + viewId: widget.dataController.rowInfo.gridId, + ), + ), ], ), ), @@ -101,9 +108,11 @@ class _CloseButton extends StatelessWidget { } class _PropertyList extends StatelessWidget { + final String viewId; final GridCellBuilder cellBuilder; final ScrollController _scrollController; _PropertyList({ + required this.viewId, required this.cellBuilder, Key? key, }) : _scrollController = ScrollController(), @@ -114,29 +123,63 @@ class _PropertyList extends StatelessWidget { return BlocBuilder( buildWhen: (previous, current) => previous.gridCells != current.gridCells, builder: (context, state) { - return ScrollbarListStack( - axis: Axis.vertical, - controller: _scrollController, - barSize: GridSize.scrollBarSize, - child: ListView.separated( - controller: _scrollController, - itemCount: state.gridCells.length, - itemBuilder: (BuildContext context, int index) { - return _RowDetailCell( - cellId: state.gridCells[index], - cellBuilder: cellBuilder, - ); - }, - separatorBuilder: (BuildContext context, int index) { - return const VSpace(2); - }, - ), + return Column( + children: [ + Expanded( + child: ScrollbarListStack( + axis: Axis.vertical, + controller: _scrollController, + barSize: GridSize.scrollBarSize, + child: ListView.separated( + controller: _scrollController, + itemCount: state.gridCells.length, + itemBuilder: (BuildContext context, int index) { + return _RowDetailCell( + cellId: state.gridCells[index], + cellBuilder: cellBuilder, + ); + }, + separatorBuilder: (BuildContext context, int index) { + return const VSpace(2); + }, + ), + ), + ), + _CreateFieldButton(viewId: viewId), + ], ); }, ); } } +class _CreateFieldButton extends StatelessWidget { + final String viewId; + const _CreateFieldButton({required this.viewId, Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final theme = context.read(); + + return SizedBox( + height: 40, + child: FlowyButton( + text: FlowyText.medium( + LocaleKeys.grid_field_newColumn.tr(), + fontSize: 12, + ), + hoverColor: theme.shader6, + onTap: () => FieldEditor( + gridId: viewId, + fieldName: "", + typeOptionLoader: NewFieldTypeOptionLoader(gridId: viewId), + ).show(context), + leftIcon: svgWidget("home/add"), + ), + ); + } +} + class _RowDetailCell extends StatelessWidget { final GridCellIdentifier cellId; final GridCellBuilder cellBuilder; @@ -172,7 +215,9 @@ class _RowDetailCell extends StatelessWidget { SizedBox( width: 150, child: FieldCellButton( - field: cellId.field, onTap: () => _showFieldEditor(context)), + field: cellId.field, + onTap: () => _showFieldEditor(context), + ), ), const HSpace(10), Expanded(child: gesture), From 349f599a43ad4d41f25a27e9f99ff6b6ecfdb9ce Mon Sep 17 00:00:00 2001 From: appflowy Date: Wed, 31 Aug 2022 20:57:34 +0800 Subject: [PATCH 100/106] chore: fix edit bugs --- .../plugins/board/presentation/card/board_text_cell.dart | 2 +- .../app_flowy/lib/plugins/board/presentation/card/card.dart | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/board_text_cell.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/board_text_cell.dart index 357c5707a1..f707a29381 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/board_text_cell.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/board_text_cell.dart @@ -78,7 +78,7 @@ class _BoardTextCellState extends State { child: BlocBuilder( builder: (context, state) { Widget child; - if (state.content.isEmpty) { + if (state.content.isEmpty && state.enableEdit == false) { child = const SizedBox(); } else { if (state.enableEdit) { diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart index 9e3bd4dd48..924faef0a5 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart @@ -98,13 +98,17 @@ class _BoardCardState extends State { widget.isEditing, cellNotifier, ); - rowNotifier.insertCell(cellId, cellNotifier); + + if (index == 0) { + rowNotifier.insertCell(cellId, cellNotifier); + } child = Padding( key: cellId.key(), padding: const EdgeInsets.only(left: 4, right: 4), child: child, ); + children.add(child); }, ); From c8ce8ef5c29d8510b45ebff3800783e04afb2d54 Mon Sep 17 00:00:00 2001 From: appflowy Date: Wed, 31 Aug 2022 23:00:58 +0800 Subject: [PATCH 101/106] chore: run command in sudo mode --- frontend/scripts/docker-buildfiles/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/scripts/docker-buildfiles/Dockerfile b/frontend/scripts/docker-buildfiles/Dockerfile index 183920f72f..a15a31d2e3 100644 --- a/frontend/scripts/docker-buildfiles/Dockerfile +++ b/frontend/scripts/docker-buildfiles/Dockerfile @@ -26,7 +26,7 @@ RUN flutter channel stable RUN flutter config --enable-linux-desktop RUN flutter doctor RUN dart pub global activate protoc_plugin -RUN pacman -Syu --needed --noconfirm git xdg-user-dirs libkeybinder3 +RUN sudo pacman -Syu --needed --noconfirm git xdg-user-dirs libkeybinder3 RUN git clone https://github.com/AppFlowy-IO/appflowy.git && \ cd appflowy/frontend && \ From bd482d0ece301ba3ca546753a8e01cef158a0c00 Mon Sep 17 00:00:00 2001 From: appflowy Date: Thu, 1 Sep 2022 09:09:37 +0800 Subject: [PATCH 102/106] chore: update changelog --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 14cc9abcc4..4758c89f79 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Release Notes +## Version 0.0.5 - beta.2 - beta.1 - 09/01/2022 + +New features +- Board-view database + - Support start editing after creating a new card + - Support edit the card directly by clicking edit button + - Add the `No Status` column to display the cards while their status is empty + - Optimize insert card animation + - Fix some UI bugs + ## Version 0.0.5 - beta.1 - 08/25/2022 New features From c6cf0011c2e82c1cb241ee3ac047306661b5c073 Mon Sep 17 00:00:00 2001 From: appflowy Date: Thu, 1 Sep 2022 09:17:33 +0800 Subject: [PATCH 103/106] chore: update changelog --- CHANGELOG.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4758c89f79..b485eb1211 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,10 +5,12 @@ New features - Board-view database - Support start editing after creating a new card - - Support edit the card directly by clicking edit button + - Support editing the card directly by clicking the edit button - Add the `No Status` column to display the cards while their status is empty - - Optimize insert card animation - - Fix some UI bugs + +### Bug Fixes +- Optimize insert card animation +- Fix some UI bugs ## Version 0.0.5 - beta.1 - 08/25/2022 From be1819981cceaa2b7aee0addbe302ff4195c5cae Mon Sep 17 00:00:00 2001 From: appflowy Date: Thu, 1 Sep 2022 11:01:52 +0800 Subject: [PATCH 104/106] fix: install keybinder --- .github/workflows/release.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2b608276f8..e8a826f031 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -58,6 +58,7 @@ jobs: sudo wget -qO /etc/apt/trusted.gpg.d/dart_linux_signing_key.asc https://dl-ssl.google.com/linux/linux_signing_key.pub sudo apt-get update sudo apt-get install -y build-essential libsqlite3-dev libssl-dev clang cmake ninja-build pkg-config libgtk-3-dev + sudo apt-get install keybinder-3.0 source $HOME/.cargo/env cargo install --force cargo-make cargo install --force duckscript_cli From 70f9a289a40c4d4eadf6c1ab998ae5a583dcf9bc Mon Sep 17 00:00:00 2001 From: gabrielztk <38334108+gabrielztk@users.noreply.github.com> Date: Thu, 1 Sep 2022 01:26:51 -0300 Subject: [PATCH 105/106] Resolve rust clippy warnings (#946) * refactor: avoid using `collect()` when not needed and cascade notation * refactor: The user might expect to be able to use Default as the type can be constructed without arguments. * refactor: using `clone` on type `indextree::NodeId` which implements the `Copy` trait * refactor: remove intermediary variables and use cascade notation * refactor: using `clone` on type `indextree::NodeId` which implements the `Copy` trait * refactor: unneeded `return` statement * refactor: ok_or_else avoids executing a function when it's not needed * refactor: dereferenced by the compiler * refactor: user enumeration for index * refactor: using `clone` on type `usize` which implements the `Copy` trait * refactor: useless conversion to the same type: `&str` * refactor: The user might expect to be able use Default as type can be constructed without arguments * refactor: The user might expect to be able use Default as type can be constructed without arguments * fix: rust formating with fmt * fix: conflict default implementation Co-authored-by: appflowy --- .../src/services/group/configuration.rs | 9 ++++--- .../lib-ot/src/core/document/attributes.rs | 6 +++++ .../lib-ot/src/core/document/document.rs | 27 ++++++++++--------- .../lib-ot/src/core/document/position.rs | 3 ++- .../lib-ot/src/core/document/transaction.rs | 19 +++++-------- 5 files changed, 34 insertions(+), 30 deletions(-) diff --git a/frontend/rust-lib/flowy-grid/src/services/group/configuration.rs b/frontend/rust-lib/flowy-grid/src/services/group/configuration.rs index ab25cd4307..3fb5cefaa3 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/configuration.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/configuration.rs @@ -285,10 +285,11 @@ fn merge_groups(old_groups: &[GroupRevision], groups: Vec) -> MergeGroupR } // Find out the new groups - let new_groups = group_map.into_values(); - for (index, group) in new_groups.into_iter().enumerate() { - merge_result.add_insert_group(index, group); - } + group_map + .into_values() + .enumerate() + .for_each(|(index, group)| merge_result.add_insert_group(index, group)); + merge_result } diff --git a/shared-lib/lib-ot/src/core/document/attributes.rs b/shared-lib/lib-ot/src/core/document/attributes.rs index 52b33dce08..d9242c67cc 100644 --- a/shared-lib/lib-ot/src/core/document/attributes.rs +++ b/shared-lib/lib-ot/src/core/document/attributes.rs @@ -3,6 +3,12 @@ use std::collections::HashMap; #[derive(Clone, serde::Serialize, serde::Deserialize)] pub struct NodeAttributes(pub HashMap>); +impl Default for NodeAttributes { + fn default() -> Self { + Self::new() + } +} + impl NodeAttributes { pub fn new() -> NodeAttributes { NodeAttributes(HashMap::new()) diff --git a/shared-lib/lib-ot/src/core/document/document.rs b/shared-lib/lib-ot/src/core/document/document.rs index 73ff03fe64..204a920ad8 100644 --- a/shared-lib/lib-ot/src/core/document/document.rs +++ b/shared-lib/lib-ot/src/core/document/document.rs @@ -10,10 +10,16 @@ pub struct DocumentTree { pub root: NodeId, } +impl Default for DocumentTree { + fn default() -> Self { + Self::new() + } +} + impl DocumentTree { pub fn new() -> DocumentTree { let mut arena = Arena::new(); - let root = arena.new_node(NodeData::new("root".into())); + let root = arena.new_node(NodeData::new("root")); DocumentTree { arena, root } } @@ -25,7 +31,7 @@ impl DocumentTree { let mut iterate_node = self.root; for id in &position.0 { - let child = self.child_at_index_of_path(iterate_node, id.clone()); + let child = self.child_at_index_of_path(iterate_node, *id); iterate_node = match child { Some(node) => node, None => return None, @@ -74,13 +80,10 @@ impl DocumentTree { fn child_at_index_of_path(&self, at_node: NodeId, index: usize) -> Option { let children = at_node.children(&self.arena); - let mut counter = 0; - for child in children { + for (counter, child) in children.enumerate() { if counter == index { return Some(child); } - - counter += 1; } None @@ -107,7 +110,7 @@ impl DocumentTree { let last_index = path.0[path.0.len() - 1]; let parent_node = self .node_at_path(&Position(parent_path.to_vec())) - .ok_or(ErrorBuilder::new(OTErrorCode::PathNotFound).build())?; + .ok_or_else(|| ErrorBuilder::new(OTErrorCode::PathNotFound).build())?; self.insert_child_at_index(parent_node, last_index, nodes.as_ref()) } @@ -132,7 +135,7 @@ impl DocumentTree { let node_to_insert = self .child_at_index_of_path(parent, index) - .ok_or(ErrorBuilder::new(OTErrorCode::PathNotFound).build())?; + .ok_or_else(|| ErrorBuilder::new(OTErrorCode::PathNotFound).build())?; self.insert_subtree_before(&node_to_insert, insert_children); Ok(()) @@ -160,11 +163,11 @@ impl DocumentTree { fn apply_update(&mut self, path: &Position, attributes: &NodeAttributes) -> Result<(), OTError> { let update_node = self .node_at_path(path) - .ok_or(ErrorBuilder::new(OTErrorCode::PathNotFound).build())?; + .ok_or_else(|| ErrorBuilder::new(OTErrorCode::PathNotFound).build())?; let node_data = self.arena.get_mut(update_node).unwrap(); let new_node = { let old_attributes = &node_data.get().attributes; - let new_attributes = NodeAttributes::compose(&old_attributes, attributes); + let new_attributes = NodeAttributes::compose(old_attributes, attributes); NodeData { attributes: new_attributes, ..node_data.get().clone() @@ -177,7 +180,7 @@ impl DocumentTree { fn apply_delete(&mut self, path: &Position, len: usize) -> Result<(), OTError> { let mut update_node = self .node_at_path(path) - .ok_or(ErrorBuilder::new(OTErrorCode::PathNotFound).build())?; + .ok_or_else(|| ErrorBuilder::new(OTErrorCode::PathNotFound).build())?; for _ in 0..len { let next = update_node.following_siblings(&self.arena).next(); update_node.remove_subtree(&mut self.arena); @@ -193,7 +196,7 @@ impl DocumentTree { fn apply_text_edit(&mut self, path: &Position, delta: &TextDelta) -> Result<(), OTError> { let edit_node = self .node_at_path(path) - .ok_or(ErrorBuilder::new(OTErrorCode::PathNotFound).build())?; + .ok_or_else(|| ErrorBuilder::new(OTErrorCode::PathNotFound).build())?; let node_data = self.arena.get_mut(edit_node).unwrap(); let new_delta = if let Some(old_delta) = &node_data.get().delta { Some(old_delta.compose(delta)?) diff --git a/shared-lib/lib-ot/src/core/document/position.rs b/shared-lib/lib-ot/src/core/document/position.rs index b98edd97f4..541ff98fb2 100644 --- a/shared-lib/lib-ot/src/core/document/position.rs +++ b/shared-lib/lib-ot/src/core/document/position.rs @@ -35,7 +35,8 @@ impl Position { prefix.push(b_at_index); } prefix.append(&mut suffix); - return Position(prefix); + + Position(prefix) } } diff --git a/shared-lib/lib-ot/src/core/document/transaction.rs b/shared-lib/lib-ot/src/core/document/transaction.rs index 73fce7d8ad..c5b00523da 100644 --- a/shared-lib/lib-ot/src/core/document/transaction.rs +++ b/shared-lib/lib-ot/src/core/document/transaction.rs @@ -63,7 +63,7 @@ impl<'a> TransactionBuilder<'a> { let mut deleted_nodes: Vec> = Vec::new(); for _ in 0..length { - deleted_nodes.push(self.get_deleted_nodes(node.clone())); + deleted_nodes.push(self.get_deleted_nodes(node)); node = node.following_siblings(&self.document.arena).next().unwrap(); } @@ -74,19 +74,12 @@ impl<'a> TransactionBuilder<'a> { } fn get_deleted_nodes(&self, node_id: NodeId) -> Box { - let node = self.document.arena.get(node_id.clone()).unwrap(); - let node_data = node.get(); - let mut children: Vec> = vec![]; + let node_data = self.document.arena.get(node_id).unwrap().get(); - let mut children_iterators = node_id.children(&self.document.arena); - loop { - let next_child = children_iterators.next(); - if let Some(child_id) = next_child { - children.push(self.get_deleted_nodes(child_id)); - } else { - break; - } - } + let mut children: Vec> = vec![]; + node_id.children(&self.document.arena).for_each(|child_id| { + children.push(self.get_deleted_nodes(child_id)); + }); Box::new(NodeSubTree { node_type: node_data.node_type.clone(), From 69e3aed6b55dda68f93f1939eb1ce640a63c500a Mon Sep 17 00:00:00 2001 From: Sean Riley Hawkins <42723553+rileyhawk1417@users.noreply.github.com> Date: Thu, 1 Sep 2022 06:27:09 +0200 Subject: [PATCH 106/106] Android vscode workflow (#912) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: fix linux build * Merge pull request #599 from AppFlowy-IO/refactor/grid_decode_cell_data Refactor/grid decode cell data * feat: ⭐ configured android vscode workflow * chore: clean up android vscode * fix: fixed typo * chore: remove unused code Co-authored-by: Nathan.fooo <86001920+appflowy@users.noreply.github.com> Co-authored-by: Lucas.Xu --- frontend/.vscode/launch.json | 38 +++++++++++ frontend/.vscode/tasks.json | 27 ++++++++ frontend/Makefile.toml | 5 ++ frontend/app_flowy/android/README.md | 64 +++++++++++++++++++ frontend/app_flowy/android/app/build.gradle | 13 +++- .../android/app/src/main/AndroidManifest.xml | 3 +- frontend/app_flowy/android/build.gradle | 2 +- frontend/app_flowy/android/gradle.properties | 1 + .../gradle/wrapper/gradle-wrapper.properties | 3 +- frontend/app_flowy/android/settings.gradle | 16 +++++ .../example/lib/multi_board_list_example.dart | 1 + .../flowy_infra_ui/android/build.gradle | 1 + ...aUiPlugin.java => FlowyInfraUIPlugin.java} | 0 .../example/android/app/build.gradle | 18 +++++- .../flowy_infra_ui_example/MainActivity.java | 7 -- .../example/android/build.gradle | 2 +- .../example/android/settings.gradle | 16 +++++ .../FlutterActivity.java | 4 ++ .../packages/flowy_sdk/android/build.gradle | 4 +- .../example/android/app/build.gradle | 2 +- .../flowy_sdk/example/android/build.gradle | 2 +- .../app_flowy/packages/flowy_sdk/lib/ffi.dart | 30 +++++---- frontend/rust-lib/.cargo/config.toml | 18 +++++- frontend/rust-lib/Cargo.lock | 11 ++++ frontend/rust-lib/dart-ffi/Cargo.toml | 2 +- frontend/rust-lib/lib-sqlite/Cargo.toml | 2 +- frontend/scripts/makefile/desktop.toml | 31 +++++++++ 27 files changed, 287 insertions(+), 36 deletions(-) create mode 100644 frontend/app_flowy/android/README.md rename frontend/app_flowy/packages/flowy_infra_ui/android/src/main/java/com/example/flowy_infra_ui/{FlowyInfraUiPlugin.java => FlowyInfraUIPlugin.java} (100%) delete mode 100644 frontend/app_flowy/packages/flowy_infra_ui/example/android/app/src/main/java/com/example/flowy_infra_ui_example/MainActivity.java create mode 100644 frontend/app_flowy/packages/flowy_infra_ui/example/example/android/app/src/main/java/com/example/flowy_infra_ui_example/FlutterActivity.java diff --git a/frontend/.vscode/launch.json b/frontend/.vscode/launch.json index 0efc79b00e..f40b4c713e 100644 --- a/frontend/.vscode/launch.json +++ b/frontend/.vscode/launch.json @@ -16,6 +16,18 @@ }, "cwd": "${workspaceRoot}/app_flowy" }, + { + // This task builds the Rust and Dart code of AppFlowy for android. + "name": "AF: Run Android", + "request": "launch", + "program": "./lib/main.dart", + "type": "dart", + "preLaunchTask": "AF: build_mobile_sdk", + "env": { + "RUST_LOG": "info" + }, + "cwd": "${workspaceRoot}/app_flowy" + }, { "name": "AF: Debug Rust", "request": "attach", @@ -48,6 +60,21 @@ }, "cwd": "${workspaceRoot}/app_flowy" }, + { + // This task builds will: + // - call the clean task, + // - rebuild all the generated Files (including freeze and language files) + // - rebuild the the Rust and Dart code of AppFlowy. + "name": "AF: Clean + Rebuild All (Android)", + "request": "launch", + "program": "./lib/main.dart", + "type": "dart", + "preLaunchTask": "AF: Clean + Rebuild All (Android)", + "env": { + "RUST_LOG": "info" + }, + "cwd": "${workspaceRoot}/app_flowy" + }, { "name": "AF: Build All (rustlog: trace)", "request": "launch", @@ -59,6 +86,17 @@ }, "cwd": "${workspaceRoot}/app_flowy" }, + { + "name": "AF: Build All Android (rustlog: trace)", + "request": "launch", + "program": "./lib/main.dart", + "type": "dart", + "preLaunchTask": "AF: build_mobile_sdk", + "env": { + "RUST_LOG": "trace" + }, + "cwd": "${workspaceRoot}/app_flowy" + }, { "name": "AF: app_flowy (profile mode)", "request": "launch", diff --git a/frontend/.vscode/tasks.json b/frontend/.vscode/tasks.json index 769ecd9b28..30fc1e134d 100644 --- a/frontend/.vscode/tasks.json +++ b/frontend/.vscode/tasks.json @@ -27,6 +27,33 @@ "panel": "new" } }, + { + "label": "AF: Clean + Rebuild All (Android)", + "type": "shell", + "dependsOrder": "sequence", + "dependsOn": [ + "AF: Rust Clean", + "AF: Flutter Clean", + "AF: build_flowy_sdk_for_android", + "AF: Flutter Pub Get", + "AF: Flutter Package Get", + "AF: Generate Language Files", + "AF: Generate Freezed Files", + ], + "presentation": { + "reveal": "always", + "panel": "new", + }, + }, + { + "label": "AF: build_flowy_sdk_for_android", + "type": "shell", + "command": "cargo make --profile development-android flowy-sdk-dev-android", + "group": "build", + "options": { + "cwd": "${workspaceFolder}" + } + }, { "label": "AF: build_flowy_sdk", "type": "shell", diff --git a/frontend/Makefile.toml b/frontend/Makefile.toml index f6e8194efa..185dac15ad 100644 --- a/frontend/Makefile.toml +++ b/frontend/Makefile.toml @@ -161,6 +161,11 @@ TARGET_OS = "ios" FLUTTER_OUTPUT_DIR = "Release" PRODUCT_EXT = "ipa" +[env.development-android] +BUILD_FLAG = "debug" +TARGET_OS = "android" +CRATE_TYPE = "cdylib" +FLUTTER_OUTPUT_DIR = "Debug" [tasks.setup-crate-type] private = true diff --git a/frontend/app_flowy/android/README.md b/frontend/app_flowy/android/README.md new file mode 100644 index 0000000000..a073fde807 --- /dev/null +++ b/frontend/app_flowy/android/README.md @@ -0,0 +1,64 @@ +# Description + +This is a guide on how to build the rust SDK for AppFlowy on android. +Compiling the sdk is easy it just needs a few tweaks. +When compiling for android we need the following pre-requisites: + +- Android NDK Tools. (v24 has been tested). +- Cargo NDK. (@latest version). + +**Getting the tools** +- Install cargo-ndk ```bash cargo install cargo-ndk```. +- [Download](https://developer.android.com/ndk/downloads/) Android NDK version 24. +- When downloading Android NDK you can get the compressed version as a standalone from the site. + Or you can download it through [Android Studio](https://developer.android.com/studio). +- After downloading the two you need to set the environment variables. For Windows that's a seperate process. + On MacOs and Linux the process is similar. +- The variables needed are '$ANDROID_NDK_HOME', this will point to where the NDK is located. +--- + +**Cargo Config File** +This code needs to be written in ~/.cargo/config, this helps cargo know where to locate the android tools(linker and archiver). +**NB** Keep in mind just replace 'user' with your own user name. Or just point it to the location of where you put the NDK. + +```toml +[target.aarch64-linux-android] +ar = "/home/user/Android/Sdk/ndk/24.0.8215888/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ar" +linker = "/home/user/Android/Sdk/ndk/24.0.8215888/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android29-clang" + +[target.armv7-linux-androideabi] +ar = "/home/user/Android/Sdk/ndk/24.0.8215888/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ar" +linker = "/home/user/Android/Sdk/ndk/24.0.8215888/toolchains/llvm/prebuilt/linux-x86_64/bin/armv7a-linux-androideabi29-clang" + +[target.i686-linux-android] +ar = "/home/user/Android/Sdk/ndk/24.0.8215888/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ar" +linker = "/home/user/Android/Sdk/ndk/24.0.8215888/toolchains/llvm/prebuilt/linux-x86_64/bin/i686-linux-android29-clang" + +[target.x86_64-linux-android] +ar = "/home/user/Android/Sdk/ndk/24.0.8215888/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ar" +linker = "/home/user/Android/Sdk/ndk/24.0.8215888/toolchains/llvm/prebuilt/linux-x86_64/bin/x86_64-linux-android29-clang" +``` + +**Clang Fix** + In order to get clang to work properly with version 24 you need to create this file. + libgcc.a, then add this one line. + ``` + INPUT(-lunwind) + ``` + +**Folder path: 'Android/Sdk/ndk/24.0.8215888/toolchains/llvm/prebuilt/linux-x86_64/lib64/clang/14.0.1/lib/linux'.** +After that you have to copy this file into three different folders namely aarch64, arm, i386 and x86_64. +We have to do this so we Android NDK can find clang on our system, if we used NDK 22 we wouldnt have to do this process. +Though using NDK v22 will not give us alot of features to work with. +This github [issue](https://github.com/fzyzcjy/flutter_rust_bridge/issues/419) explains the reason why we are doing this. + + --- + + **Android NDK** + + After installing the NDK tools for android you should export the PATH to your config file + (.vimrc, .zshrc, .profile, .bashrc file), That way it can be found. + + ```vim + export PATH=/home/sean/Android/Sdk/ndk/24.0.8215888 + ``` \ No newline at end of file diff --git a/frontend/app_flowy/android/app/build.gradle b/frontend/app_flowy/android/app/build.gradle index e2f9734817..2fc26c2fb5 100644 --- a/frontend/app_flowy/android/app/build.gradle +++ b/frontend/app_flowy/android/app/build.gradle @@ -26,7 +26,8 @@ apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { - compileSdkVersion 30 + compileSdkVersion 31 + ndkVersion "24.0.8215888" compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 @@ -39,21 +40,26 @@ android { sourceSets { main.java.srcDirs += 'src/main/kotlin' + main.jniLibs.srcDirs += 'jniLibs/' } defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "com.example.app_flowy" - minSdkVersion 16 - targetSdkVersion 30 + minSdkVersion 19 + targetSdkVersion 31 versionCode flutterVersionCode.toInteger() versionName flutterVersionName + multiDexEnabled true } buildTypes { release { // TODO: Add your own signing config for the release build. // Signing with the debug keys for now, so `flutter run --release` works. + minifyEnabled true + shrinkResources true + signingConfig signingConfigs.debug } } @@ -65,4 +71,5 @@ flutter { dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation "com.android.support:multidex:2.0.1" } diff --git a/frontend/app_flowy/android/app/src/main/AndroidManifest.xml b/frontend/app_flowy/android/app/src/main/AndroidManifest.xml index a9fd47bd31..a34dc98587 100644 --- a/frontend/app_flowy/android/app/src/main/AndroidManifest.xml +++ b/frontend/app_flowy/android/app/src/main/AndroidManifest.xml @@ -2,7 +2,8 @@ package="com.example.app_flowy"> + android:icon="@mipmap/ic_launcher" + android:name="${applicationName}"> properties.load(reader) } def flutterSdkPath = properties.getProperty("flutter.sdk") assert flutterSdkPath != null, "flutter.sdk not set in local.properties" apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" + + +def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() + +def plugins = new Properties() +def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') + +if(pluginsFile.exists()){ + pluginsFile.withReader('UTF-8'){reader -> plugins.load(reader)} +} + +plugins.each{name, path -> + def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() + include ":$name" + project(":$name").projectDir = pluginDirectory +} \ No newline at end of file diff --git a/frontend/app_flowy/packages/appflowy_board/example/lib/multi_board_list_example.dart b/frontend/app_flowy/packages/appflowy_board/example/lib/multi_board_list_example.dart index b69fb1dbf3..1decf21063 100644 --- a/frontend/app_flowy/packages/appflowy_board/example/lib/multi_board_list_example.dart +++ b/frontend/app_flowy/packages/appflowy_board/example/lib/multi_board_list_example.dart @@ -34,6 +34,7 @@ class _MultiBoardListExampleState extends State { RichTextItem(title: "Card 8", subtitle: 'Aug 1, 2020 4:05 PM'), TextItem("Card 9"), ]; + final column1 = AFBoardColumnData(id: "To Do", name: "To Do", items: a); final column2 = AFBoardColumnData( id: "In Progress", diff --git a/frontend/app_flowy/packages/flowy_infra_ui/android/build.gradle b/frontend/app_flowy/packages/flowy_infra_ui/android/build.gradle index 76e9272bbf..d129628362 100644 --- a/frontend/app_flowy/packages/flowy_infra_ui/android/build.gradle +++ b/frontend/app_flowy/packages/flowy_infra_ui/android/build.gradle @@ -2,6 +2,7 @@ group 'com.example.flowy_infra_ui' version '1.0' buildscript { + ext.kotlin_version = '1.6.10' repositories { google() mavenCentral() diff --git a/frontend/app_flowy/packages/flowy_infra_ui/android/src/main/java/com/example/flowy_infra_ui/FlowyInfraUiPlugin.java b/frontend/app_flowy/packages/flowy_infra_ui/android/src/main/java/com/example/flowy_infra_ui/FlowyInfraUIPlugin.java similarity index 100% rename from frontend/app_flowy/packages/flowy_infra_ui/android/src/main/java/com/example/flowy_infra_ui/FlowyInfraUiPlugin.java rename to frontend/app_flowy/packages/flowy_infra_ui/android/src/main/java/com/example/flowy_infra_ui/FlowyInfraUIPlugin.java diff --git a/frontend/app_flowy/packages/flowy_infra_ui/example/android/app/build.gradle b/frontend/app_flowy/packages/flowy_infra_ui/example/android/app/build.gradle index 7356196bc3..1aa1cc480b 100644 --- a/frontend/app_flowy/packages/flowy_infra_ui/example/android/app/build.gradle +++ b/frontend/app_flowy/packages/flowy_infra_ui/example/android/app/build.gradle @@ -24,8 +24,16 @@ if (flutterVersionName == null) { apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" +//apply plugin: 'kotlin-android-extensions' + + +//androidExtensions { +// experimental = true +//} android { + compileSdkVersion flutter.compileSdkVersion + ndkVersion flutter.ndkVersion compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 @@ -43,20 +51,23 @@ android { defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "com.example.flowy_infra_ui_example" - minSdkVersion 16 - targetSdkVersion 30 + minSdkVersion flutter.minSdkVersion + targetSdkVersion flutter.targetSdkVersion versionCode flutterVersionCode.toInteger() versionName flutterVersionName + multiDexEnabled true } buildTypes { release { + minifyEnabled true + shrinkResources true // TODO: Add your own signing config for the release build. // Signing with the debug keys for now, so `flutter run --release` works. signingConfig signingConfigs.debug } } - compileSdkVersion 30 + } flutter { @@ -65,4 +76,5 @@ flutter { dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation 'com.android.support:multidex:2.0.1' } diff --git a/frontend/app_flowy/packages/flowy_infra_ui/example/android/app/src/main/java/com/example/flowy_infra_ui_example/MainActivity.java b/frontend/app_flowy/packages/flowy_infra_ui/example/android/app/src/main/java/com/example/flowy_infra_ui_example/MainActivity.java deleted file mode 100644 index 8adb012232..0000000000 --- a/frontend/app_flowy/packages/flowy_infra_ui/example/android/app/src/main/java/com/example/flowy_infra_ui_example/MainActivity.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.example.flowy_infra_ui_example; - -import io.flutter.embedding.android.FlutterActivity; - -public class MainActivity extends FlutterActivity { - -} diff --git a/frontend/app_flowy/packages/flowy_infra_ui/example/android/build.gradle b/frontend/app_flowy/packages/flowy_infra_ui/example/android/build.gradle index 3dd86e4db3..cb243569ed 100644 --- a/frontend/app_flowy/packages/flowy_infra_ui/example/android/build.gradle +++ b/frontend/app_flowy/packages/flowy_infra_ui/example/android/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.kotlin_version = '1.3.50' + ext.kotlin_version = '1.6.10' repositories { google() mavenCentral() diff --git a/frontend/app_flowy/packages/flowy_infra_ui/example/android/settings.gradle b/frontend/app_flowy/packages/flowy_infra_ui/example/android/settings.gradle index 44e62bcf06..e15f18b2ec 100644 --- a/frontend/app_flowy/packages/flowy_infra_ui/example/android/settings.gradle +++ b/frontend/app_flowy/packages/flowy_infra_ui/example/android/settings.gradle @@ -9,3 +9,19 @@ localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } def flutterSdkPath = properties.getProperty("flutter.sdk") assert flutterSdkPath != null, "flutter.sdk not set in local.properties" apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" + + + +def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() + +def plugins = new Properties() +def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') +if (pluginsFile.exists()) { + pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) } +} + +plugins.each { name, path -> + def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() + include ":$name" + project(":$name").projectDir = pluginDirectory +} \ No newline at end of file diff --git a/frontend/app_flowy/packages/flowy_infra_ui/example/example/android/app/src/main/java/com/example/flowy_infra_ui_example/FlutterActivity.java b/frontend/app_flowy/packages/flowy_infra_ui/example/example/android/app/src/main/java/com/example/flowy_infra_ui_example/FlutterActivity.java new file mode 100644 index 0000000000..33c3ea5970 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_infra_ui/example/example/android/app/src/main/java/com/example/flowy_infra_ui_example/FlutterActivity.java @@ -0,0 +1,4 @@ +package example.android.app.src.main.java.com.example.flowy_infra_ui_example; + +public class FlutterActivity { +} diff --git a/frontend/app_flowy/packages/flowy_sdk/android/build.gradle b/frontend/app_flowy/packages/flowy_sdk/android/build.gradle index 52a1508a46..f28d45b3a1 100644 --- a/frontend/app_flowy/packages/flowy_sdk/android/build.gradle +++ b/frontend/app_flowy/packages/flowy_sdk/android/build.gradle @@ -2,7 +2,7 @@ group 'com.plugin.flowy_sdk' version '1.0-SNAPSHOT' buildscript { - ext.kotlin_version = '1.3.50' + ext.kotlin_version = '1.6.10' repositories { google() jcenter() @@ -25,7 +25,7 @@ apply plugin: 'com.android.library' apply plugin: 'kotlin-android' android { - compileSdkVersion 30 + compileSdkVersion 31 sourceSets { main.java.srcDirs += 'src/main/kotlin' diff --git a/frontend/app_flowy/packages/flowy_sdk/example/android/app/build.gradle b/frontend/app_flowy/packages/flowy_sdk/example/android/app/build.gradle index cccedb641b..aa5daf1e9d 100644 --- a/frontend/app_flowy/packages/flowy_sdk/example/android/app/build.gradle +++ b/frontend/app_flowy/packages/flowy_sdk/example/android/app/build.gradle @@ -36,7 +36,7 @@ android { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "com.plugin.flowy_sdk_example" minSdkVersion 16 - targetSdkVersion 30 + targetSdkVersion 31 versionCode flutterVersionCode.toInteger() versionName flutterVersionName } diff --git a/frontend/app_flowy/packages/flowy_sdk/example/android/build.gradle b/frontend/app_flowy/packages/flowy_sdk/example/android/build.gradle index c505a86352..714549c265 100644 --- a/frontend/app_flowy/packages/flowy_sdk/example/android/build.gradle +++ b/frontend/app_flowy/packages/flowy_sdk/example/android/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.kotlin_version = '1.3.50' + ext.kotlin_version = '1.6.10' repositories { google() jcenter() diff --git a/frontend/app_flowy/packages/flowy_sdk/lib/ffi.dart b/frontend/app_flowy/packages/flowy_sdk/lib/ffi.dart index 0ade770a23..5ced5b2e83 100644 --- a/frontend/app_flowy/packages/flowy_sdk/lib/ffi.dart +++ b/frontend/app_flowy/packages/flowy_sdk/lib/ffi.dart @@ -14,11 +14,14 @@ final DynamicLibrary dl = _dl; DynamicLibrary _open() { if (Platform.environment.containsKey('FLUTTER_TEST')) { final prefix = "${Directory.current.path}/.sandbox"; - if (Platform.isLinux) return DynamicLibrary.open('${prefix}/libdart_ffi.so'); - if (Platform.isAndroid) return DynamicLibrary.open('${prefix}/libdart_ffi.so'); + if (Platform.isLinux) + return DynamicLibrary.open('${prefix}/libdart_ffi.so'); + if (Platform.isAndroid) + return DynamicLibrary.open('${prefix}/libdart_ffi.so'); if (Platform.isMacOS) return DynamicLibrary.open('${prefix}/libdart_ffi.a'); if (Platform.isIOS) return DynamicLibrary.open('${prefix}/libdart_ffi.a'); - if (Platform.isWindows) return DynamicLibrary.open('${prefix}/dart_ffi.dll'); + if (Platform.isWindows) + return DynamicLibrary.open('${prefix}/dart_ffi.dll'); } else { if (Platform.isLinux) return DynamicLibrary.open('libdart_ffi.so'); if (Platform.isAndroid) return DynamicLibrary.open('libdart_ffi.so'); @@ -39,7 +42,8 @@ void async_event( _invoke_async(port, input, len); } -final _invoke_async_Dart _invoke_async = _dl.lookupFunction<_invoke_async_C, _invoke_async_Dart>('async_event'); +final _invoke_async_Dart _invoke_async = + _dl.lookupFunction<_invoke_async_C, _invoke_async_Dart>('async_event'); typedef _invoke_async_C = Void Function( Int64 port, Pointer input, @@ -59,7 +63,8 @@ Pointer sync_event( return _invoke_sync(input, len); } -final _invoke_sync_Dart _invoke_sync = _dl.lookupFunction<_invoke_sync_C, _invoke_sync_Dart>('sync_event'); +final _invoke_sync_Dart _invoke_sync = + _dl.lookupFunction<_invoke_sync_C, _invoke_sync_Dart>('sync_event'); typedef _invoke_sync_C = Pointer Function( Pointer input, Uint64 len, @@ -76,7 +81,8 @@ int init_sdk( return _init_sdk(path); } -final _init_sdk_Dart _init_sdk = _dl.lookupFunction<_init_sdk_C, _init_sdk_Dart>('init_sdk'); +final _init_sdk_Dart _init_sdk = + _dl.lookupFunction<_init_sdk_C, _init_sdk_Dart>('init_sdk'); typedef _init_sdk_C = Int64 Function( Pointer path, ); @@ -90,7 +96,8 @@ int set_stream_port(int port) { } final _set_stream_port_Dart _set_stream_port = - _dl.lookupFunction<_set_stream_port_C, _set_stream_port_Dart>('set_stream_port'); + _dl.lookupFunction<_set_stream_port_C, _set_stream_port_Dart>( + 'set_stream_port'); typedef _set_stream_port_C = Int32 Function( Int64 port, @@ -104,8 +111,8 @@ void link_me_please() { _link_me_please(); } -final _link_me_please_Dart _link_me_please = - _dl.lookupFunction<_link_me_please_C, _link_me_please_Dart>('link_me_please'); +final _link_me_please_Dart _link_me_please = _dl + .lookupFunction<_link_me_please_C, _link_me_please_Dart>('link_me_please'); typedef _link_me_please_C = Void Function(); typedef _link_me_please_Dart = void Function(); @@ -116,8 +123,9 @@ void store_dart_post_cobject( _store_dart_post_cobject(ptr); } -final _store_dart_post_cobject_Dart _store_dart_post_cobject = - _dl.lookupFunction<_store_dart_post_cobject_C, _store_dart_post_cobject_Dart>('store_dart_post_cobject'); +final _store_dart_post_cobject_Dart _store_dart_post_cobject = _dl + .lookupFunction<_store_dart_post_cobject_C, _store_dart_post_cobject_Dart>( + 'store_dart_post_cobject'); typedef _store_dart_post_cobject_C = Void Function( Pointer)>> ptr, ); diff --git a/frontend/rust-lib/.cargo/config.toml b/frontend/rust-lib/.cargo/config.toml index 06a85fae37..5be8246f16 100644 --- a/frontend/rust-lib/.cargo/config.toml +++ b/frontend/rust-lib/.cargo/config.toml @@ -5,4 +5,20 @@ rustflags=["-C", "link-arg=-mmacosx-version-min=10.11"] [target.aarch64-apple-darwin] -rustflags=["-C", "link-arg=-mmacosx-version-min=10.11"] \ No newline at end of file +rustflags=["-C", "link-arg=-mmacosx-version-min=10.11"] + +[target.aarch64-linux-android] +ar = "path-to-ndk/llvm-ar" +linker = "path-to-ndk/aarch64-linux-android29-clang" + +[target.armv7-linux-androideabi] +ar = "path-to-ndk/llvm-ar" +linker = "path-to-ndk/armv7a-linux-androideabi29-clang" + +[target.i686-linux-android] +ar = "path-to-ndk/llvm-ar" +linker = "path-to-ndk/i686-linux-android29-clang" + +[target.x86_64-linux-android] +ar = "path-to-ndk/llvm-ar" +linker = "path-to-ndk/x86_64-linux-android29-clang" \ No newline at end of file diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index 4119cc1edf..c2d3fa7339 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -1796,6 +1796,7 @@ dependencies = [ "lazy_static", "libsqlite3-sys", "log", + "openssl", "r2d2", "scheduled-thread-pool", ] @@ -2096,6 +2097,15 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" +[[package]] +name = "openssl-src" +version = "111.22.0+1.1.1q" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f31f0d509d1c1ae9cada2f9539ff8f37933831fd5098879e482aa687d659853" +dependencies = [ + "cc", +] + [[package]] name = "openssl-sys" version = "0.9.72" @@ -2105,6 +2115,7 @@ dependencies = [ "autocfg", "cc", "libc", + "openssl-src", "pkg-config", "vcpkg", ] diff --git a/frontend/rust-lib/dart-ffi/Cargo.toml b/frontend/rust-lib/dart-ffi/Cargo.toml index 44fd254cbb..117d78c064 100644 --- a/frontend/rust-lib/dart-ffi/Cargo.toml +++ b/frontend/rust-lib/dart-ffi/Cargo.toml @@ -38,4 +38,4 @@ http_sync = ["flowy-sdk/http_sync", "flowy-sdk/use_bunyan"] #use_protobuf= ["protobuf"] [build-dependencies] -lib-infra = { path = "../../../shared-lib/lib-infra", features = ["protobuf_file_gen", "dart"] } \ No newline at end of file +lib-infra = { path = "../../../shared-lib/lib-infra", features = ["protobuf_file_gen", "dart"] } diff --git a/frontend/rust-lib/lib-sqlite/Cargo.toml b/frontend/rust-lib/lib-sqlite/Cargo.toml index 6536a19a56..ea45b7d6ae 100644 --- a/frontend/rust-lib/lib-sqlite/Cargo.toml +++ b/frontend/rust-lib/lib-sqlite/Cargo.toml @@ -15,6 +15,6 @@ lazy_static = "1.4.0" scheduled-thread-pool = "0.2.5" error-chain = "=0.12.0" log = "0.4.11" - +openssl = { version = "0.10.38", features = ["vendored"] } #[features] #windows = ["libsqlite3-sys/bundled-windows"] \ No newline at end of file diff --git a/frontend/scripts/makefile/desktop.toml b/frontend/scripts/makefile/desktop.toml index 458801ee26..09b1680f3f 100644 --- a/frontend/scripts/makefile/desktop.toml +++ b/frontend/scripts/makefile/desktop.toml @@ -13,6 +13,11 @@ mac_alias = "flowy-sdk-dev-macos" windows_alias = "flowy-sdk-dev-windows" linux_alias = "flowy-sdk-dev-linux" +[tasks.flowy-sdk-dev-android] +category = "Build" +dependencies = ["env_check"] +run_task = { name = ["setup-crate-type","sdk-build-android", "restore-crate-type"] } + [tasks.flowy-sdk-dev-macos] category = "Build" dependencies = ["env_check"] @@ -42,6 +47,32 @@ script = [ ] script_runner = "@shell" +[tasks.sdk-build-android] +private = true +script = [ + """ + cd rust-lib/ + rustup show + rustup target add aarch64-linux-android \ + armv7-linux-androideabi \ + i686-linux-android \ + x86_64-linux-android + DEST=${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/app_flowy/android/app/src/main/jniLibs + rm -rf $DEST/arm64-v8a \ + $DEST/armeabi-v7a \ + $DEST/x86 \ + $DEST/x86_64 + cargo ndk \ + -t arm64-v8a \ + -t armeabi-v7a \ + -t x86 \ + -t x86_64 \ + -o $DEST build + cd ../ + """, +] +script_runner = "@shell" + [tasks.sdk-build.windows] private = true script = [