feat: implement mobile toolbar
This commit is contained in:
parent
e8280bd2b4
commit
2487408920
10 changed files with 205 additions and 52 deletions
|
|
@ -1,4 +1,5 @@
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
@ -33,19 +34,12 @@ class SimpleEditor extends StatelessWidget {
|
||||||
..handler = debugPrint
|
..handler = debugPrint
|
||||||
..level = LogLevel.all;
|
..level = LogLevel.all;
|
||||||
onEditorStateChange(editorState);
|
onEditorStateChange(editorState);
|
||||||
|
return Column(
|
||||||
return AppFlowyEditor(
|
children: [
|
||||||
editorState: editorState,
|
Expanded(child: _buildEditor(context, editorState)),
|
||||||
themeData: themeData,
|
if (Platform.isIOS || Platform.isAndroid)
|
||||||
autoFocus: editorState.document.isEmpty,
|
_buildMobileToolbar(context, editorState),
|
||||||
customBuilders: {
|
],
|
||||||
'paragraph': TextBlockComponentBuilder(),
|
|
||||||
'todo_list': TodoListBlockComponentBuilder(),
|
|
||||||
'bulleted_list': BulletedListBlockComponentBuilder(),
|
|
||||||
'numbered_list': NumberedListBlockComponentBuilder(),
|
|
||||||
'quote':
|
|
||||||
QuoteBlockComponentBuilder(padding: const EdgeInsets.all(0)),
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return const Center(
|
return const Center(
|
||||||
|
|
@ -55,4 +49,23 @@ class SimpleEditor extends StatelessWidget {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildEditor(BuildContext context, EditorState editorState) {
|
||||||
|
return AppFlowyEditor(
|
||||||
|
editorState: editorState,
|
||||||
|
themeData: themeData,
|
||||||
|
autoFocus: editorState.document.isEmpty,
|
||||||
|
customBuilders: {
|
||||||
|
'paragraph': TextBlockComponentBuilder(),
|
||||||
|
'todo_list': TodoListBlockComponentBuilder(),
|
||||||
|
'bulleted_list': BulletedListBlockComponentBuilder(),
|
||||||
|
'numbered_list': NumberedListBlockComponentBuilder(),
|
||||||
|
'quote': QuoteBlockComponentBuilder(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildMobileToolbar(BuildContext context, EditorState editorState) {
|
||||||
|
return MobileToolbar(editorState: editorState);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -54,3 +54,4 @@ export 'src/infra/html_converter.dart';
|
||||||
export 'src/service/internal_key_event_handlers/copy_paste_handler.dart';
|
export 'src/service/internal_key_event_handlers/copy_paste_handler.dart';
|
||||||
|
|
||||||
export 'src/editor/block_component/block_component.dart';
|
export 'src/editor/block_component/block_component.dart';
|
||||||
|
export 'src/editor/editor_component/editor_component.dart';
|
||||||
|
|
|
||||||
1
lib/src/editor/editor_component/editor_component.dart
Normal file
1
lib/src/editor/editor_component/editor_component.dart
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export 'toolbar/mobile_toolbar.dart';
|
||||||
|
|
@ -86,6 +86,7 @@ class _KeyboardServiceWidgetState extends State<KeyboardServiceWidget> {
|
||||||
'attach text editing value: $textEditingValue',
|
'attach text editing value: $textEditingValue',
|
||||||
);
|
);
|
||||||
textInputService.attach(textEditingValue);
|
textInputService.attach(textEditingValue);
|
||||||
|
isAttached = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,8 +22,7 @@ class DesktopScrollService extends StatefulWidget {
|
||||||
|
|
||||||
class _DesktopScrollServiceState extends State<DesktopScrollService>
|
class _DesktopScrollServiceState extends State<DesktopScrollService>
|
||||||
implements AppFlowyScrollService {
|
implements AppFlowyScrollService {
|
||||||
bool forward = false;
|
AxisDirection _direction = AxisDirection.down;
|
||||||
double _panStartDy = 0.0;
|
|
||||||
|
|
||||||
bool _scrollEnabled = true;
|
bool _scrollEnabled = true;
|
||||||
|
|
||||||
|
|
@ -57,14 +56,8 @@ class _DesktopScrollServiceState extends State<DesktopScrollService>
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Listener(
|
return Listener(
|
||||||
onPointerSignal: _onPointerSignal,
|
onPointerSignal: _onPointerSignal,
|
||||||
onPointerPanZoomStart: (event) {
|
|
||||||
_panStartDy = event.localPosition.dy;
|
|
||||||
},
|
|
||||||
onPointerPanZoomUpdate: _onPointerPanZoomUpdate,
|
onPointerPanZoomUpdate: _onPointerPanZoomUpdate,
|
||||||
onPointerPanZoomEnd: (event) {
|
onPointerPanZoomEnd: _onPointerPanZoomEnd,
|
||||||
final forward = this.forward ? 1.0 : -1.0;
|
|
||||||
goBallistic(-1000 * forward);
|
|
||||||
},
|
|
||||||
child: widget.child,
|
child: widget.child,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -91,6 +84,24 @@ class _DesktopScrollServiceState extends State<DesktopScrollService>
|
||||||
Log.scroll.debug('enable scroll service');
|
Log.scroll.debug('enable scroll service');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void startAutoScroll(Offset offset) {
|
||||||
|
widget.autoScroller.startAutoScroll(offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void stopAutoScroll() {
|
||||||
|
widget.autoScroller.stopAutoScroll();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void goBallistic(double velocity) {
|
||||||
|
final position = widget.scrollController.position;
|
||||||
|
if (position is ScrollPositionWithSingleContext) {
|
||||||
|
position.goBallistic(velocity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void _onPointerSignal(PointerSignalEvent event) {
|
void _onPointerSignal(PointerSignalEvent event) {
|
||||||
if (event is PointerScrollEvent && _scrollEnabled) {
|
if (event is PointerScrollEvent && _scrollEnabled) {
|
||||||
final dy =
|
final dy =
|
||||||
|
|
@ -101,27 +112,20 @@ class _DesktopScrollServiceState extends State<DesktopScrollService>
|
||||||
|
|
||||||
void _onPointerPanZoomUpdate(PointerPanZoomUpdateEvent event) {
|
void _onPointerPanZoomUpdate(PointerPanZoomUpdateEvent event) {
|
||||||
if (_scrollEnabled) {
|
if (_scrollEnabled) {
|
||||||
forward = event.panDelta.dy > 0;
|
|
||||||
final dy = (widget.scrollController.position.pixels - event.panDelta.dy);
|
final dy = (widget.scrollController.position.pixels - event.panDelta.dy);
|
||||||
scrollTo(dy);
|
scrollTo(dy);
|
||||||
|
|
||||||
|
_direction =
|
||||||
|
event.panDelta.dy > 0 ? AxisDirection.down : AxisDirection.up;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
void _onPointerPanZoomEnd(PointerPanZoomEndEvent event) {
|
||||||
void startAutoScroll(Offset offset) =>
|
// TODO: calculate the pixelsPerSecond
|
||||||
widget.autoScroller.startAutoScroll(offset);
|
// var dyPerSecond = -1000.0;
|
||||||
|
// if (_direction == AxisDirection.up) {
|
||||||
@override
|
// dyPerSecond *= -1.0;
|
||||||
void stopAutoScroll() {
|
// }
|
||||||
// TODO: implement stopAutoScroll
|
// goBallistic(dyPerSecond);
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void goBallistic(double velocity) {
|
|
||||||
final position = widget.scrollController.position;
|
|
||||||
if (position is ScrollPositionWithSingleContext) {
|
|
||||||
position.goBallistic(velocity);
|
|
||||||
position.context.setIgnorePointer(false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,92 @@
|
||||||
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
import 'package:appflowy_editor/src/editor/editor_component/service/scroll/auto_scroller.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class MobileScrollService extends StatefulWidget {
|
||||||
|
const MobileScrollService({
|
||||||
|
Key? key,
|
||||||
|
required this.scrollController,
|
||||||
|
required this.autoScroller,
|
||||||
|
required this.child,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
final ScrollController scrollController;
|
||||||
|
final AutoScroller autoScroller;
|
||||||
|
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<MobileScrollService> createState() => _MobileScrollServiceState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MobileScrollServiceState extends State<MobileScrollService>
|
||||||
|
implements AppFlowyScrollService {
|
||||||
|
@override
|
||||||
|
double get dy => widget.scrollController.position.pixels;
|
||||||
|
|
||||||
|
@override
|
||||||
|
double? get onePageHeight {
|
||||||
|
final renderBox = context.findRenderObject()?.unwrapOrNull<RenderBox>();
|
||||||
|
return renderBox?.size.height;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
double get maxScrollExtent =>
|
||||||
|
widget.scrollController.position.maxScrollExtent;
|
||||||
|
|
||||||
|
@override
|
||||||
|
double get minScrollExtent =>
|
||||||
|
widget.scrollController.position.minScrollExtent;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int? get page {
|
||||||
|
if (onePageHeight != null) {
|
||||||
|
final scrollExtent = maxScrollExtent - minScrollExtent;
|
||||||
|
return (scrollExtent / onePageHeight!).ceil();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return widget.child;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void scrollTo(double dy) {
|
||||||
|
widget.scrollController.position.jumpTo(
|
||||||
|
dy.clamp(
|
||||||
|
widget.scrollController.position.minScrollExtent,
|
||||||
|
widget.scrollController.position.maxScrollExtent,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void disable() {
|
||||||
|
Log.scroll.debug('disable scroll service');
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void enable() {
|
||||||
|
Log.scroll.debug('enable scroll service');
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void startAutoScroll(Offset offset) {
|
||||||
|
widget.autoScroller.startAutoScroll(offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void stopAutoScroll() {
|
||||||
|
widget.autoScroller.stopAutoScroll();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void goBallistic(double velocity) {
|
||||||
|
final position = widget.scrollController.position;
|
||||||
|
if (position is ScrollPositionWithSingleContext) {
|
||||||
|
position.goBallistic(velocity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,6 +4,7 @@ import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
import 'package:appflowy_editor/src/editor/editor_component/service/scroll/auto_scrollable_widget.dart';
|
import 'package:appflowy_editor/src/editor/editor_component/service/scroll/auto_scrollable_widget.dart';
|
||||||
import 'package:appflowy_editor/src/editor/editor_component/service/scroll/auto_scroller.dart';
|
import 'package:appflowy_editor/src/editor/editor_component/service/scroll/auto_scroller.dart';
|
||||||
import 'package:appflowy_editor/src/editor/editor_component/service/scroll/desktop_scroll_service.dart';
|
import 'package:appflowy_editor/src/editor/editor_component/service/scroll/desktop_scroll_service.dart';
|
||||||
|
import 'package:appflowy_editor/src/editor/editor_component/service/scroll/mobile_scroll_service.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
|
@ -83,7 +84,7 @@ class _ScrollServiceWidgetState extends State<ScrollServiceWidget>
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
AutoScroller autoScroller,
|
AutoScroller autoScroller,
|
||||||
) {
|
) {
|
||||||
return DesktopScrollService(
|
return MobileScrollService(
|
||||||
key: _forwardKey,
|
key: _forwardKey,
|
||||||
scrollController: _scrollController,
|
scrollController: _scrollController,
|
||||||
autoScroller: autoScroller,
|
autoScroller: autoScroller,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
import 'package:appflowy_editor/src/editor/util/debounce.dart';
|
|
||||||
import 'package:appflowy_editor/src/flutter/overlay.dart';
|
import 'package:appflowy_editor/src/flutter/overlay.dart';
|
||||||
import 'package:appflowy_editor/src/service/context_menu/built_in_context_menu_item.dart';
|
import 'package:appflowy_editor/src/service/context_menu/built_in_context_menu_item.dart';
|
||||||
import 'package:appflowy_editor/src/service/context_menu/context_menu.dart';
|
import 'package:appflowy_editor/src/service/context_menu/context_menu.dart';
|
||||||
|
|
@ -67,11 +66,11 @@ class _DesktopSelectionServiceWidgetState
|
||||||
|
|
||||||
// Need to refresh the selection when the metrics changed.
|
// Need to refresh the selection when the metrics changed.
|
||||||
if (currentSelection.value != null) {
|
if (currentSelection.value != null) {
|
||||||
Debounce.debounce(
|
// Debounce.debounce(
|
||||||
'didChangeMetrics - update selection ',
|
// 'didChangeMetrics - update selection ',
|
||||||
const Duration(milliseconds: 100),
|
// const Duration(milliseconds: 100),
|
||||||
() => updateSelection(currentSelection.value!),
|
// () => updateSelection(currentSelection.value!),
|
||||||
);
|
// );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
import 'package:appflowy_editor/src/editor/util/debounce.dart';
|
|
||||||
import 'package:appflowy_editor/src/flutter/overlay.dart';
|
import 'package:appflowy_editor/src/flutter/overlay.dart';
|
||||||
import 'package:appflowy_editor/src/service/context_menu/built_in_context_menu_item.dart';
|
import 'package:appflowy_editor/src/service/context_menu/built_in_context_menu_item.dart';
|
||||||
import 'package:appflowy_editor/src/service/context_menu/context_menu.dart';
|
import 'package:appflowy_editor/src/service/context_menu/context_menu.dart';
|
||||||
|
|
@ -67,11 +66,11 @@ class _MobileSelectionServiceWidgetState
|
||||||
|
|
||||||
// Need to refresh the selection when the metrics changed.
|
// Need to refresh the selection when the metrics changed.
|
||||||
if (currentSelection.value != null) {
|
if (currentSelection.value != null) {
|
||||||
Debounce.debounce(
|
// Debounce.debounce(
|
||||||
'didChangeMetrics - update selection ',
|
// 'didChangeMetrics - update selection ',
|
||||||
const Duration(milliseconds: 100),
|
// const Duration(milliseconds: 100),
|
||||||
() => updateSelection(currentSelection.value!),
|
// () => updateSelection(currentSelection.value!),
|
||||||
);
|
// );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
42
lib/src/editor/editor_component/toolbar/mobile_toolbar.dart
Normal file
42
lib/src/editor/editor_component/toolbar/mobile_toolbar.dart
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class MobileToolbar extends StatelessWidget {
|
||||||
|
const MobileToolbar({
|
||||||
|
super.key,
|
||||||
|
required this.editorState,
|
||||||
|
});
|
||||||
|
|
||||||
|
final EditorState editorState;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ValueListenableBuilder<Selection?>(
|
||||||
|
valueListenable: editorState.service.selectionService.currentSelection,
|
||||||
|
builder: (_, Selection? selection, __) {
|
||||||
|
if (selection == null) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
final width = MediaQuery.of(context).size.width;
|
||||||
|
return SizedBox(
|
||||||
|
width: width,
|
||||||
|
height: 30,
|
||||||
|
child: Container(
|
||||||
|
color: Colors.grey.withOpacity(0.3),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
editorState.selection.updateSelection(null);
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.keyboard_hide),
|
||||||
|
),
|
||||||
|
const Text('FIXME: Mobile Toolbar'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue