mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: adjust toggle list, callout, quote and divider on mobile (#3894)
* feat: adjust toggle list block * feat: show block actions when tapping divider * feat: add toggle list and callout to toolbar * feat: refactor the emoji picker button * fix: toggle list integration tests
This commit is contained in:
@ -15,14 +15,23 @@ void main() {
|
|||||||
TestWidgetsFlutterBinding.ensureInitialized();
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
group('toggle list in document', () {
|
group('toggle list in document', () {
|
||||||
|
Finder findToggleListIcon({
|
||||||
|
required bool isExpanded,
|
||||||
|
}) {
|
||||||
|
final turns = isExpanded ? 0.25 : 0.0;
|
||||||
|
return find.byWidgetPredicate(
|
||||||
|
(widget) => widget is AnimatedRotation && widget.turns == turns,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
void expectToggleListOpened() {
|
void expectToggleListOpened() {
|
||||||
expect(find.byIcon(Icons.arrow_drop_down), findsOneWidget);
|
expect(findToggleListIcon(isExpanded: true), findsOneWidget);
|
||||||
expect(find.byIcon(Icons.arrow_right), findsNothing);
|
expect(findToggleListIcon(isExpanded: false), findsNothing);
|
||||||
}
|
}
|
||||||
|
|
||||||
void expectToggleListClosed() {
|
void expectToggleListClosed() {
|
||||||
expect(find.byIcon(Icons.arrow_drop_down), findsNothing);
|
expect(findToggleListIcon(isExpanded: false), findsOneWidget);
|
||||||
expect(find.byIcon(Icons.arrow_right), findsOneWidget);
|
expect(findToggleListIcon(isExpanded: true), findsNothing);
|
||||||
}
|
}
|
||||||
|
|
||||||
testWidgets('convert > to toggle list, and click the icon to close it',
|
testWidgets('convert > to toggle list, and click the icon to close it',
|
||||||
@ -63,7 +72,7 @@ void main() {
|
|||||||
expect(find.text(text2, findRichText: true), findsOneWidget);
|
expect(find.text(text2, findRichText: true), findsOneWidget);
|
||||||
|
|
||||||
// Click the toggle list icon to close it
|
// Click the toggle list icon to close it
|
||||||
final toggleListIcon = find.byIcon(Icons.arrow_drop_down);
|
final toggleListIcon = find.byIcon(Icons.arrow_right);
|
||||||
await tester.tapButton(toggleListIcon);
|
await tester.tapButton(toggleListIcon);
|
||||||
|
|
||||||
// expect the toggle list to be closed
|
// expect the toggle list to be closed
|
||||||
@ -88,7 +97,7 @@ void main() {
|
|||||||
await tester.ime.insertText('> $text');
|
await tester.ime.insertText('> $text');
|
||||||
|
|
||||||
// Click the toggle list icon to close it
|
// Click the toggle list icon to close it
|
||||||
final toggleListIcon = find.byIcon(Icons.arrow_drop_down);
|
final toggleListIcon = find.byIcon(Icons.arrow_right);
|
||||||
await tester.tapButton(toggleListIcon);
|
await tester.tapButton(toggleListIcon);
|
||||||
|
|
||||||
// Press the enter key
|
// Press the enter key
|
||||||
@ -164,7 +173,7 @@ void main() {
|
|||||||
|
|
||||||
// Press the enter key
|
// Press the enter key
|
||||||
// Click the toggle list icon to close it
|
// Click the toggle list icon to close it
|
||||||
final toggleListIcon = find.byIcon(Icons.arrow_drop_down);
|
final toggleListIcon = find.byIcon(Icons.arrow_right);
|
||||||
await tester.tapButton(toggleListIcon);
|
await tester.tapButton(toggleListIcon);
|
||||||
|
|
||||||
await tester.editor.updateSelection(
|
await tester.editor.updateSelection(
|
||||||
|
@ -1,22 +1,21 @@
|
|||||||
|
import 'package:appflowy/plugins/base/icon/icon_picker.dart';
|
||||||
import 'package:appflowy/plugins/base/icon/icon_picker_page.dart';
|
import 'package:appflowy/plugins/base/icon/icon_picker_page.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
class MobileEmojiPickerScreen extends StatelessWidget {
|
class MobileEmojiPickerScreen extends StatelessWidget {
|
||||||
static const routeName = '/emoji_picker';
|
static const routeName = '/emoji_picker';
|
||||||
static const viewId = 'id';
|
|
||||||
|
|
||||||
const MobileEmojiPickerScreen({
|
const MobileEmojiPickerScreen({
|
||||||
super.key,
|
super.key,
|
||||||
required this.id,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/// view id
|
|
||||||
final String id;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return IconPickerPage(
|
return IconPickerPage(
|
||||||
id: id,
|
onSelected: (result) {
|
||||||
|
context.pop<EmojiPickerResult>(result);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,13 +12,23 @@ enum FlowyIconType {
|
|||||||
custom;
|
custom;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class EmojiPickerResult {
|
||||||
|
const EmojiPickerResult(
|
||||||
|
this.type,
|
||||||
|
this.emoji,
|
||||||
|
);
|
||||||
|
|
||||||
|
final FlowyIconType type;
|
||||||
|
final String emoji;
|
||||||
|
}
|
||||||
|
|
||||||
class FlowyIconPicker extends StatefulWidget {
|
class FlowyIconPicker extends StatefulWidget {
|
||||||
const FlowyIconPicker({
|
const FlowyIconPicker({
|
||||||
super.key,
|
super.key,
|
||||||
required this.onSelected,
|
required this.onSelected,
|
||||||
});
|
});
|
||||||
|
|
||||||
final void Function(FlowyIconType type, String value) onSelected;
|
final void Function(EmojiPickerResult result) onSelected;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<FlowyIconPicker> createState() => _FlowyIconPickerState();
|
State<FlowyIconPicker> createState() => _FlowyIconPickerState();
|
||||||
@ -45,7 +55,12 @@ class _FlowyIconPickerState extends State<FlowyIconPicker>
|
|||||||
const Spacer(),
|
const Spacer(),
|
||||||
_RemoveIconButton(
|
_RemoveIconButton(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
widget.onSelected(FlowyIconType.icon, '');
|
widget.onSelected(
|
||||||
|
const EmojiPickerResult(
|
||||||
|
FlowyIconType.icon,
|
||||||
|
'',
|
||||||
|
),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -58,7 +73,12 @@ class _FlowyIconPickerState extends State<FlowyIconPicker>
|
|||||||
children: [
|
children: [
|
||||||
FlowyEmojiPicker(
|
FlowyEmojiPicker(
|
||||||
onEmojiSelected: (_, emoji) {
|
onEmojiSelected: (_, emoji) {
|
||||||
widget.onSelected(FlowyIconType.emoji, emoji);
|
widget.onSelected(
|
||||||
|
EmojiPickerResult(
|
||||||
|
FlowyIconType.emoji,
|
||||||
|
emoji,
|
||||||
|
),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import 'package:appflowy/mobile/presentation/base/app_bar_actions.dart';
|
import 'package:appflowy/mobile/presentation/base/app_bar_actions.dart';
|
||||||
import 'package:appflowy/plugins/base/icon/icon_picker.dart';
|
import 'package:appflowy/plugins/base/icon/icon_picker.dart';
|
||||||
import 'package:appflowy/workspace/application/view/view_service.dart';
|
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
@ -8,11 +7,10 @@ import 'package:go_router/go_router.dart';
|
|||||||
class IconPickerPage extends StatefulWidget {
|
class IconPickerPage extends StatefulWidget {
|
||||||
const IconPickerPage({
|
const IconPickerPage({
|
||||||
super.key,
|
super.key,
|
||||||
required this.id,
|
required this.onSelected,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// view id
|
final void Function(EmojiPickerResult) onSelected;
|
||||||
final String id;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<IconPickerPage> createState() => _IconPickerPageState();
|
State<IconPickerPage> createState() => _IconPickerPageState();
|
||||||
@ -34,13 +32,7 @@ class _IconPickerPageState extends State<IconPickerPage> {
|
|||||||
),
|
),
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
child: FlowyIconPicker(
|
child: FlowyIconPicker(
|
||||||
onSelected: (_, emoji) {
|
onSelected: widget.onSelected,
|
||||||
ViewBackendService.updateViewIcon(
|
|
||||||
viewId: widget.id,
|
|
||||||
viewIcon: emoji,
|
|
||||||
);
|
|
||||||
context.pop();
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/mobile_block_action_buttons.dart';
|
||||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/custom_image_block_component.dart';
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/custom_image_block_component.dart';
|
||||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
||||||
import 'package:appflowy/plugins/document/presentation/editor_style.dart';
|
import 'package:appflowy/plugins/document/presentation/editor_style.dart';
|
||||||
@ -26,6 +27,9 @@ Map<String, BlockComponentBuilder> getEditorBuilderMap({
|
|||||||
|
|
||||||
final configuration = BlockComponentConfiguration(
|
final configuration = BlockComponentConfiguration(
|
||||||
padding: (_) => const EdgeInsets.symmetric(vertical: 5.0),
|
padding: (_) => const EdgeInsets.symmetric(vertical: 5.0),
|
||||||
|
indentPadding: (node, textDirection) => textDirection == TextDirection.ltr
|
||||||
|
? const EdgeInsets.only(left: 26.0)
|
||||||
|
: const EdgeInsets.only(right: 26.0),
|
||||||
);
|
);
|
||||||
|
|
||||||
final customBlockComponentBuilderMap = {
|
final customBlockComponentBuilderMap = {
|
||||||
@ -119,6 +123,14 @@ Map<String, BlockComponentBuilder> getEditorBuilderMap({
|
|||||||
DividerBlockKeys.type: DividerBlockComponentBuilder(
|
DividerBlockKeys.type: DividerBlockComponentBuilder(
|
||||||
configuration: configuration,
|
configuration: configuration,
|
||||||
height: 28.0,
|
height: 28.0,
|
||||||
|
wrapper: (context, node, child) {
|
||||||
|
return MobileBlockActionButtons(
|
||||||
|
showThreeDots: false,
|
||||||
|
node: node,
|
||||||
|
editorState: editorState,
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
MathEquationBlockKeys.type: MathEquationBlockComponentBuilder(
|
MathEquationBlockKeys.type: MathEquationBlockComponentBuilder(
|
||||||
configuration: configuration,
|
configuration: configuration,
|
||||||
@ -146,9 +158,7 @@ Map<String, BlockComponentBuilder> getEditorBuilderMap({
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
errorBlockComponentBuilderKey: ErrorBlockComponentBuilder(
|
errorBlockComponentBuilderKey: ErrorBlockComponentBuilder(
|
||||||
configuration: configuration.copyWith(
|
configuration: configuration,
|
||||||
padding: (_) => const EdgeInsets.symmetric(vertical: 10),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -286,10 +286,8 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
|
|||||||
textDecorationMobileToolbarItem,
|
textDecorationMobileToolbarItem,
|
||||||
buildTextAndBackgroundColorMobileToolbarItem(),
|
buildTextAndBackgroundColorMobileToolbarItem(),
|
||||||
headingMobileToolbarItem,
|
headingMobileToolbarItem,
|
||||||
todoListMobileToolbarItem,
|
customListMobileToolbarItem,
|
||||||
listMobileToolbarItem,
|
|
||||||
linkMobileToolbarItem,
|
linkMobileToolbarItem,
|
||||||
quoteMobileToolbarItem,
|
|
||||||
dividerMobileToolbarItem,
|
dividerMobileToolbarItem,
|
||||||
imageMobileToolbarItem,
|
imageMobileToolbarItem,
|
||||||
mathEquationMobileToolbarItem,
|
mathEquationMobileToolbarItem,
|
||||||
|
@ -21,6 +21,7 @@ class MobileBlockActionButtons extends StatelessWidget {
|
|||||||
const MobileBlockActionButtons({
|
const MobileBlockActionButtons({
|
||||||
super.key,
|
super.key,
|
||||||
this.extendActionWidgets = const [],
|
this.extendActionWidgets = const [],
|
||||||
|
this.showThreeDots = true,
|
||||||
required this.node,
|
required this.node,
|
||||||
required this.editorState,
|
required this.editorState,
|
||||||
required this.child,
|
required this.child,
|
||||||
@ -30,6 +31,7 @@ class MobileBlockActionButtons extends StatelessWidget {
|
|||||||
final EditorState editorState;
|
final EditorState editorState;
|
||||||
final List<Widget> extendActionWidgets;
|
final List<Widget> extendActionWidgets;
|
||||||
final Widget child;
|
final Widget child;
|
||||||
|
final bool showThreeDots;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -37,7 +39,15 @@ class MobileBlockActionButtons extends StatelessWidget {
|
|||||||
return child;
|
return child;
|
||||||
}
|
}
|
||||||
|
|
||||||
const padding = 5.0;
|
if (!showThreeDots) {
|
||||||
|
return GestureDetector(
|
||||||
|
behavior: HitTestBehavior.opaque,
|
||||||
|
onTap: () => _showBottomSheet(context),
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const padding = 10.0;
|
||||||
return Stack(
|
return Stack(
|
||||||
children: [
|
children: [
|
||||||
child,
|
child,
|
||||||
|
@ -1,7 +1,11 @@
|
|||||||
|
import 'package:appflowy/plugins/base/emoji/emoji_picker_screen.dart';
|
||||||
|
import 'package:appflowy/plugins/base/icon/icon_picker.dart';
|
||||||
import 'package:appflowy/workspace/presentation/settings/widgets/emoji_picker/emoji_picker.dart';
|
import 'package:appflowy/workspace/presentation/settings/widgets/emoji_picker/emoji_picker.dart';
|
||||||
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
class EmojiPickerButton extends StatelessWidget {
|
class EmojiPickerButton extends StatelessWidget {
|
||||||
EmojiPickerButton({
|
EmojiPickerButton({
|
||||||
@ -15,20 +19,43 @@ class EmojiPickerButton extends StatelessWidget {
|
|||||||
final String emoji;
|
final String emoji;
|
||||||
final double emojiSize;
|
final double emojiSize;
|
||||||
final Size emojiPickerSize;
|
final Size emojiPickerSize;
|
||||||
final void Function(String emoji, PopoverController controller) onSubmitted;
|
final void Function(String emoji, PopoverController? controller) onSubmitted;
|
||||||
final PopoverController popoverController = PopoverController();
|
final PopoverController popoverController = PopoverController();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return AppFlowyPopover(
|
if (PlatformExtension.isDesktopOrWeb) {
|
||||||
controller: popoverController,
|
return AppFlowyPopover(
|
||||||
triggerActions: PopoverTriggerFlags.click,
|
controller: popoverController,
|
||||||
constraints: BoxConstraints.expand(
|
triggerActions: PopoverTriggerFlags.click,
|
||||||
width: emojiPickerSize.width,
|
constraints: BoxConstraints.expand(
|
||||||
height: emojiPickerSize.height,
|
width: emojiPickerSize.width,
|
||||||
),
|
height: emojiPickerSize.height,
|
||||||
popupBuilder: (context) => _buildEmojiPicker(),
|
),
|
||||||
child: FlowyTextButton(
|
popupBuilder: (context) => Container(
|
||||||
|
width: emojiPickerSize.width,
|
||||||
|
height: emojiPickerSize.height,
|
||||||
|
padding: const EdgeInsets.all(4.0),
|
||||||
|
child: EmojiSelectionMenu(
|
||||||
|
onSubmitted: (emoji) => onSubmitted(emoji, popoverController),
|
||||||
|
onExit: () {},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: FlowyTextButton(
|
||||||
|
emoji,
|
||||||
|
overflow: TextOverflow.visible,
|
||||||
|
fontSize: emojiSize,
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
constraints: const BoxConstraints(minWidth: 35.0),
|
||||||
|
fillColor: Colors.transparent,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
onPressed: () {
|
||||||
|
popoverController.show();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return FlowyTextButton(
|
||||||
emoji,
|
emoji,
|
||||||
overflow: TextOverflow.visible,
|
overflow: TextOverflow.visible,
|
||||||
fontSize: emojiSize,
|
fontSize: emojiSize,
|
||||||
@ -36,22 +63,18 @@ class EmojiPickerButton extends StatelessWidget {
|
|||||||
constraints: const BoxConstraints(minWidth: 35.0),
|
constraints: const BoxConstraints(minWidth: 35.0),
|
||||||
fillColor: Colors.transparent,
|
fillColor: Colors.transparent,
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
onPressed: () {
|
onPressed: () async {
|
||||||
popoverController.show();
|
final result = await context.push<EmojiPickerResult>(
|
||||||
|
MobileEmojiPickerScreen.routeName,
|
||||||
|
);
|
||||||
|
if (result != null) {
|
||||||
|
onSubmitted(
|
||||||
|
result.emoji,
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
),
|
);
|
||||||
);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildEmojiPicker() {
|
|
||||||
return Container(
|
|
||||||
width: emojiPickerSize.width,
|
|
||||||
height: emojiPickerSize.height,
|
|
||||||
padding: const EdgeInsets.all(4.0),
|
|
||||||
child: EmojiSelectionMenu(
|
|
||||||
onSubmitted: (emoji) => onSubmitted(emoji, popoverController),
|
|
||||||
onExit: () {},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -187,7 +187,7 @@ class _CalloutBlockComponentWidgetState
|
|||||||
emoji: emoji,
|
emoji: emoji,
|
||||||
onSubmitted: (emoji, controller) {
|
onSubmitted: (emoji, controller) {
|
||||||
setEmoji(emoji);
|
setEmoji(emoji);
|
||||||
controller.close();
|
controller?.close();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/mobile_block_action_buttons.dart';
|
||||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';
|
||||||
import 'package:appflowy/startup/startup.dart';
|
import 'package:appflowy/startup/startup.dart';
|
||||||
import 'package:appflowy/workspace/presentation/home/toast.dart';
|
import 'package:appflowy/workspace/presentation/home/toast.dart';
|
||||||
@ -8,6 +9,7 @@ import 'package:appflowy_editor/appflowy_editor.dart';
|
|||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
class ErrorBlockComponentBuilder extends BlockComponentBuilder {
|
class ErrorBlockComponentBuilder extends BlockComponentBuilder {
|
||||||
ErrorBlockComponentBuilder({
|
ErrorBlockComponentBuilder({
|
||||||
@ -72,11 +74,15 @@ class _DividerBlockComponentWidgetState extends State<ErrorBlockComponentWidget>
|
|||||||
ClipboardServiceData(plainText: jsonEncode(node.toJson())),
|
ClipboardServiceData(plainText: jsonEncode(node.toJson())),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
text: Container(
|
text: SizedBox(
|
||||||
height: 48,
|
height: 52,
|
||||||
alignment: Alignment.center,
|
child: Row(
|
||||||
child: FlowyText(
|
children: [
|
||||||
LocaleKeys.document_errorBlock_theBlockIsNotSupported.tr(),
|
const HSpace(4),
|
||||||
|
FlowyText(
|
||||||
|
LocaleKeys.document_errorBlock_theBlockIsNotSupported.tr(),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -95,6 +101,14 @@ class _DividerBlockComponentWidgetState extends State<ErrorBlockComponentWidget>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (PlatformExtension.isMobile) {
|
||||||
|
child = MobileBlockActionButtons(
|
||||||
|
node: node,
|
||||||
|
editorState: context.read<EditorState>(),
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return child;
|
return child;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,6 @@ import 'package:appflowy/plugins/base/emoji/emoji_picker_screen.dart';
|
|||||||
import 'package:appflowy/plugins/base/icon/icon_picker.dart';
|
import 'package:appflowy/plugins/base/icon/icon_picker.dart';
|
||||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart';
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart';
|
||||||
import 'package:appflowy/plugins/document/presentation/editor_style.dart';
|
import 'package:appflowy/plugins/document/presentation/editor_style.dart';
|
||||||
import 'package:appflowy/workspace/application/view/view_bloc.dart';
|
|
||||||
import 'package:appflowy/workspace/application/view/view_listener.dart';
|
import 'package:appflowy/workspace/application/view/view_listener.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
@ -16,7 +15,6 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
|||||||
import 'package:flowy_infra_ui/widget/rounded_button.dart';
|
import 'package:flowy_infra_ui/widget/rounded_button.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
|
|
||||||
import 'cover_editor.dart';
|
import 'cover_editor.dart';
|
||||||
|
|
||||||
@ -303,15 +301,14 @@ class _DocumentHeaderToolbarState extends State<DocumentHeaderToolbar> {
|
|||||||
),
|
),
|
||||||
onTap: PlatformExtension.isDesktop
|
onTap: PlatformExtension.isDesktop
|
||||||
? null
|
? null
|
||||||
: () => context.push(
|
: () async {
|
||||||
Uri(
|
final result = await context.push<EmojiPickerResult>(
|
||||||
path: MobileEmojiPickerScreen.routeName,
|
MobileEmojiPickerScreen.routeName,
|
||||||
queryParameters: {
|
);
|
||||||
MobileEmojiPickerScreen.viewId:
|
if (result != null) {
|
||||||
context.read<ViewBloc>().state.view.id,
|
widget.onCoverChanged(icon: result.emoji);
|
||||||
},
|
}
|
||||||
).toString(),
|
},
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (PlatformExtension.isDesktop) {
|
if (PlatformExtension.isDesktop) {
|
||||||
@ -325,8 +322,8 @@ class _DocumentHeaderToolbarState extends State<DocumentHeaderToolbar> {
|
|||||||
popupBuilder: (BuildContext popoverContext) {
|
popupBuilder: (BuildContext popoverContext) {
|
||||||
isPopoverOpen = true;
|
isPopoverOpen = true;
|
||||||
return FlowyIconPicker(
|
return FlowyIconPicker(
|
||||||
onSelected: (type, value) {
|
onSelected: (result) {
|
||||||
widget.onCoverChanged(icon: value);
|
widget.onCoverChanged(icon: result.emoji);
|
||||||
_popoverController.close();
|
_popoverController.close();
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@ -532,8 +529,8 @@ class _DocumentIconState extends State<DocumentIcon> {
|
|||||||
child: child,
|
child: child,
|
||||||
popupBuilder: (BuildContext popoverContext) {
|
popupBuilder: (BuildContext popoverContext) {
|
||||||
return FlowyIconPicker(
|
return FlowyIconPicker(
|
||||||
onSelected: (type, value) {
|
onSelected: (result) {
|
||||||
widget.onIconChanged(value);
|
widget.onIconChanged(result.emoji);
|
||||||
_popoverController.close();
|
_popoverController.close();
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@ -542,15 +539,14 @@ class _DocumentIconState extends State<DocumentIcon> {
|
|||||||
} else {
|
} else {
|
||||||
child = GestureDetector(
|
child = GestureDetector(
|
||||||
child: child,
|
child: child,
|
||||||
onTap: () => context.push(
|
onTap: () async {
|
||||||
Uri(
|
final result = await context.push<EmojiPickerResult>(
|
||||||
path: MobileEmojiPickerScreen.routeName,
|
MobileEmojiPickerScreen.routeName,
|
||||||
queryParameters: {
|
);
|
||||||
MobileEmojiPickerScreen.viewId:
|
if (result != null) {
|
||||||
context.read<ViewBloc>().state.view.id,
|
widget.onIconChanged(result.emoji);
|
||||||
},
|
}
|
||||||
).toString(),
|
},
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,121 @@
|
|||||||
|
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||||
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
||||||
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
final customListMobileToolbarItem = MobileToolbarItem.withMenu(
|
||||||
|
itemIcon: const AFMobileIcon(afMobileIcons: AFMobileIcons.list),
|
||||||
|
itemMenuBuilder: (editorState, selection, _) {
|
||||||
|
return _MobileListMenu(
|
||||||
|
editorState: editorState,
|
||||||
|
selection: selection,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
class _MobileListMenu extends StatelessWidget {
|
||||||
|
const _MobileListMenu({
|
||||||
|
required this.editorState,
|
||||||
|
required this.selection,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Selection selection;
|
||||||
|
final EditorState editorState;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return GridView.count(
|
||||||
|
crossAxisCount: 2,
|
||||||
|
mainAxisSpacing: 8,
|
||||||
|
crossAxisSpacing: 8,
|
||||||
|
childAspectRatio: 5,
|
||||||
|
shrinkWrap: true,
|
||||||
|
children: [
|
||||||
|
// bulleted list, numbered list
|
||||||
|
_buildListButton(
|
||||||
|
context,
|
||||||
|
BulletedListBlockKeys.type,
|
||||||
|
const AFMobileIcon(afMobileIcons: AFMobileIcons.bulletedList),
|
||||||
|
LocaleKeys.document_plugins_bulletedList.tr(),
|
||||||
|
),
|
||||||
|
_buildListButton(
|
||||||
|
context,
|
||||||
|
NumberedListBlockKeys.type,
|
||||||
|
const AFMobileIcon(afMobileIcons: AFMobileIcons.numberedList),
|
||||||
|
LocaleKeys.document_plugins_numberedList.tr(),
|
||||||
|
),
|
||||||
|
|
||||||
|
// todo list, quote list
|
||||||
|
_buildListButton(
|
||||||
|
context,
|
||||||
|
TodoListBlockKeys.type,
|
||||||
|
const AFMobileIcon(afMobileIcons: AFMobileIcons.checkbox),
|
||||||
|
LocaleKeys.document_plugins_todoList.tr(),
|
||||||
|
),
|
||||||
|
_buildListButton(
|
||||||
|
context,
|
||||||
|
QuoteBlockKeys.type,
|
||||||
|
const AFMobileIcon(afMobileIcons: AFMobileIcons.quote),
|
||||||
|
LocaleKeys.document_plugins_quoteList.tr(),
|
||||||
|
),
|
||||||
|
|
||||||
|
// toggle list, callout
|
||||||
|
_buildListButton(
|
||||||
|
context,
|
||||||
|
ToggleListBlockKeys.type,
|
||||||
|
const FlowySvg(
|
||||||
|
FlowySvgs.toggle_list_s,
|
||||||
|
size: Size.square(24),
|
||||||
|
),
|
||||||
|
LocaleKeys.document_plugins_toggleList.tr(),
|
||||||
|
),
|
||||||
|
_buildListButton(
|
||||||
|
context,
|
||||||
|
CalloutBlockKeys.type,
|
||||||
|
const Icon(Icons.note_rounded),
|
||||||
|
LocaleKeys.document_plugins_callout.tr(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildListButton(
|
||||||
|
BuildContext context,
|
||||||
|
String listBlockType,
|
||||||
|
Widget icon,
|
||||||
|
String label,
|
||||||
|
) {
|
||||||
|
final node = editorState.getNodeAtPath(selection.start.path);
|
||||||
|
final type = node?.type;
|
||||||
|
if (node == null || type == null) {
|
||||||
|
const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
final isSelected = type == listBlockType;
|
||||||
|
return MobileToolbarItemMenuBtn(
|
||||||
|
icon: icon,
|
||||||
|
label: FlowyText(label),
|
||||||
|
isSelected: isSelected,
|
||||||
|
onPressed: () async {
|
||||||
|
await editorState.formatNode(
|
||||||
|
selection,
|
||||||
|
(node) {
|
||||||
|
final attributes = {
|
||||||
|
ParagraphBlockKeys.delta: (node.delta ?? Delta()).toJson(),
|
||||||
|
if (listBlockType == TodoListBlockKeys.type)
|
||||||
|
TodoListBlockKeys.checked: false,
|
||||||
|
if (listBlockType == CalloutBlockKeys.type)
|
||||||
|
CalloutBlockKeys.icon: '📌',
|
||||||
|
};
|
||||||
|
return node.copyWith(
|
||||||
|
type: isSelected ? ParagraphBlockKeys.type : listBlockType,
|
||||||
|
attributes: attributes,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -25,7 +25,8 @@ export 'image/mobile_image_toolbar_item.dart';
|
|||||||
export 'inline_math_equation/inline_math_equation.dart';
|
export 'inline_math_equation/inline_math_equation.dart';
|
||||||
export 'inline_math_equation/inline_math_equation_toolbar_item.dart';
|
export 'inline_math_equation/inline_math_equation_toolbar_item.dart';
|
||||||
export 'math_equation/math_equation_block_component.dart';
|
export 'math_equation/math_equation_block_component.dart';
|
||||||
export 'math_equation/mobile_math_eqaution_toolbar_item.dart';
|
export 'math_equation/mobile_math_equation_toolbar_item.dart';
|
||||||
|
export 'mobile_toolbar_item/list_mobile_toolbar_item.dart';
|
||||||
export 'openai/widgets/auto_completion_node_widget.dart';
|
export 'openai/widgets/auto_completion_node_widget.dart';
|
||||||
export 'openai/widgets/smart_edit_node_widget.dart';
|
export 'openai/widgets/smart_edit_node_widget.dart';
|
||||||
export 'openai/widgets/smart_edit_toolbar_item.dart';
|
export 'openai/widgets/smart_edit_toolbar_item.dart';
|
||||||
|
@ -166,13 +166,17 @@ class _ToggleListBlockComponentWidgetState
|
|||||||
Container(
|
Container(
|
||||||
constraints: const BoxConstraints(minWidth: 26, minHeight: 22),
|
constraints: const BoxConstraints(minWidth: 26, minHeight: 22),
|
||||||
padding: const EdgeInsets.only(right: 4.0),
|
padding: const EdgeInsets.only(right: 4.0),
|
||||||
child: FlowyIconButton(
|
child: AnimatedRotation(
|
||||||
width: 18.0,
|
turns: collapsed ? 0.0 : 0.25,
|
||||||
icon: Icon(
|
duration: const Duration(milliseconds: 200),
|
||||||
collapsed ? Icons.arrow_right : Icons.arrow_drop_down,
|
child: FlowyIconButton(
|
||||||
size: 18.0,
|
width: 18.0,
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.arrow_right,
|
||||||
|
size: 18.0,
|
||||||
|
),
|
||||||
|
onPressed: onCollapsed,
|
||||||
),
|
),
|
||||||
onPressed: onCollapsed,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
@ -209,11 +209,8 @@ GoRoute _mobileEmojiPickerPageRoute() {
|
|||||||
parentNavigatorKey: AppGlobals.rootNavKey,
|
parentNavigatorKey: AppGlobals.rootNavKey,
|
||||||
path: MobileEmojiPickerScreen.routeName,
|
path: MobileEmojiPickerScreen.routeName,
|
||||||
pageBuilder: (context, state) {
|
pageBuilder: (context, state) {
|
||||||
final id = state.uri.queryParameters[MobileEmojiPickerScreen.viewId]!;
|
return const MaterialPage(
|
||||||
return MaterialPage(
|
child: MobileEmojiPickerScreen(),
|
||||||
child: MobileEmojiPickerScreen(
|
|
||||||
id: id,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -361,10 +361,10 @@ class _SingleInnerViewItemState extends State<SingleInnerViewItem> {
|
|||||||
popupBuilder: (context) {
|
popupBuilder: (context) {
|
||||||
isIconPickerOpened = true;
|
isIconPickerOpened = true;
|
||||||
return FlowyIconPicker(
|
return FlowyIconPicker(
|
||||||
onSelected: (_, emoji) {
|
onSelected: (result) {
|
||||||
ViewBackendService.updateViewIcon(
|
ViewBackendService.updateViewIcon(
|
||||||
viewId: widget.view.id,
|
viewId: widget.view.id,
|
||||||
viewIcon: emoji,
|
viewIcon: result.emoji,
|
||||||
);
|
);
|
||||||
controller.close();
|
controller.close();
|
||||||
},
|
},
|
||||||
|
@ -54,8 +54,8 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
path: "."
|
path: "."
|
||||||
ref: a47fc6f
|
ref: "50117b6"
|
||||||
resolved-ref: a47fc6fc712b06991f578ae2ab314cbe23034e96
|
resolved-ref: "50117b6900e4b239603ee48f6f3e7b7bc603c865"
|
||||||
url: "https://github.com/AppFlowy-IO/appflowy-editor.git"
|
url: "https://github.com/AppFlowy-IO/appflowy-editor.git"
|
||||||
source: git
|
source: git
|
||||||
version: "1.5.2"
|
version: "1.5.2"
|
||||||
|
@ -47,7 +47,8 @@ dependencies:
|
|||||||
appflowy_editor:
|
appflowy_editor:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/AppFlowy-IO/appflowy-editor.git
|
url: https://github.com/AppFlowy-IO/appflowy-editor.git
|
||||||
ref: a47fc6f
|
ref: 50117b6
|
||||||
|
|
||||||
appflowy_popover:
|
appflowy_popover:
|
||||||
path: packages/appflowy_popover
|
path: packages/appflowy_popover
|
||||||
|
|
||||||
|
@ -616,7 +616,12 @@
|
|||||||
"smartEditDisabled": "Connect OpenAI in Settings",
|
"smartEditDisabled": "Connect OpenAI in Settings",
|
||||||
"discardResponse": "Do you want to discard the AI responses?",
|
"discardResponse": "Do you want to discard the AI responses?",
|
||||||
"createInlineMathEquation": "Create equation",
|
"createInlineMathEquation": "Create equation",
|
||||||
"toggleList": "Toggle List",
|
"toggleList": "Toggle list",
|
||||||
|
"quoteList":"Quote list",
|
||||||
|
"numberedList":"Numbered list",
|
||||||
|
"bulletedList":"Bulleted list",
|
||||||
|
"todoList": "Todo List",
|
||||||
|
"callout": "Callout",
|
||||||
"cover": {
|
"cover": {
|
||||||
"changeCover": "Change Cover",
|
"changeCover": "Change Cover",
|
||||||
"colors": "Colors",
|
"colors": "Colors",
|
||||||
|
Reference in New Issue
Block a user