From f022f9aa06c0a080347b2e0347287581d98c9f93 Mon Sep 17 00:00:00 2001 From: Daniyar Nurmukhamet Date: Tue, 30 May 2023 09:04:10 +0600 Subject: [PATCH] feature: changed url field ux to be directly editable (#2523) * feature: changed url field ux to be directly editable * feature: added link svg icon --- .../assets/images/editor/link.svg | 4 + .../widgets/row/cells/url_cell/url_cell.dart | 133 +++++++++++------- .../database_view/widgets/row/row_detail.dart | 2 +- 3 files changed, 91 insertions(+), 48 deletions(-) create mode 100644 frontend/appflowy_flutter/assets/images/editor/link.svg diff --git a/frontend/appflowy_flutter/assets/images/editor/link.svg b/frontend/appflowy_flutter/assets/images/editor/link.svg new file mode 100644 index 0000000000..5fbcc8d787 --- /dev/null +++ b/frontend/appflowy_flutter/assets/images/editor/link.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/url_cell/url_cell.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/url_cell/url_cell.dart index 81c0f5cb31..6c3662ce6c 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/url_cell/url_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/url_cell/url_cell.dart @@ -19,6 +19,8 @@ import 'url_cell_bloc.dart'; class GridURLCellStyle extends GridCellStyle { String? placeholder; + TextStyle? textStyle; + bool? autofocus; List accessoryTypes; @@ -29,8 +31,8 @@ class GridURLCellStyle extends GridCellStyle { } enum GridURLCellAccessoryType { - edit, copyURL, + visitURL, } class GridURLCell extends GridCellWidget { @@ -56,15 +58,14 @@ class GridURLCell extends GridCellWidget { GridCellAccessoryBuildContext buildContext, ) { switch (ty) { - case GridURLCellAccessoryType.edit: + case GridURLCellAccessoryType.visitURL: + final cellContext = cellControllerBuilder.build() as URLCellController; return GridCellAccessoryBuilder( - builder: (Key key) => _EditURLAccessory( + builder: (Key key) => _VisitURLAccessory( key: key, - anchorContext: buildContext.anchorContext, - cellControllerBuilder: cellControllerBuilder, + cellContext: cellContext, ), ); - case GridURLCellAccessoryType.copyURL: final cellContext = cellControllerBuilder.build() as URLCellController; return GridCellAccessoryBuilder( @@ -89,11 +90,11 @@ class GridURLCell extends GridCellWidget { ); } - // If the accessories is empty then the default accessory will be GridURLCellAccessoryType.edit + // If the accessories is empty then the default accessory will be GridURLCellAccessoryType.visitURL if (accessories.isEmpty) { accessories.add( accessoryFromType( - GridURLCellAccessoryType.edit, + GridURLCellAccessoryType.visitURL, buildContext, ), ); @@ -106,6 +107,8 @@ class GridURLCell extends GridCellWidget { class _GridURLCellState extends GridCellState { final _popoverController = PopoverController(); late URLCellBloc _cellBloc; + late TextEditingController _controller; + late FocusNode _focusNode; @override void initState() { @@ -113,6 +116,8 @@ class _GridURLCellState extends GridCellState { widget.cellControllerBuilder.build() as URLCellController; _cellBloc = URLCellBloc(cellController: cellController); _cellBloc.add(const URLCellEvent.initial()); + _controller = TextEditingController(text: _cellBloc.state.content); + _focusNode = FocusNode(); super.initState(); } @@ -122,60 +127,61 @@ class _GridURLCellState extends GridCellState { value: _cellBloc, child: BlocBuilder( builder: (context, state) { - final richText = Padding( - padding: GridSize.cellContentInsets, - child: FlowyText.medium( - state.content, - color: Theme.of(context).colorScheme.primary, - decoration: TextDecoration.underline, + final urlEditor = Padding( + padding: EdgeInsets.only( + left: GridSize.cellContentInsets.left, + right: GridSize.cellContentInsets.right, ), - ); - - return AppFlowyPopover( - margin: EdgeInsets.zero, - controller: _popoverController, - constraints: BoxConstraints.loose(const Size(300, 160)), - direction: PopoverDirection.bottomWithLeftAligned, - triggerActions: PopoverTriggerFlags.none, - offset: const Offset(0, 8), - child: SizedBox.expand( - child: GestureDetector( - child: Align(alignment: Alignment.centerLeft, child: richText), - onTap: () async { - final url = context.read().state.url; - await _openUrlOrEdit(url); - }, + child: TextField( + controller: _controller, + focusNode: _focusNode, + maxLines: 1, + style: (widget.cellStyle?.textStyle ?? + Theme.of(context).textTheme.bodyMedium) + ?.copyWith( + color: Theme.of(context).colorScheme.primary, + decoration: TextDecoration.underline, + ), + autofocus: false, + onEditingComplete: focusChanged, + onSubmitted: (value) => focusChanged(isUrlSubmitted: true), + decoration: InputDecoration( + contentPadding: EdgeInsets.only( + top: GridSize.cellContentInsets.top, + bottom: GridSize.cellContentInsets.bottom, + ), + border: InputBorder.none, + hintText: widget.cellStyle?.placeholder, + isDense: true, ), ), - popupBuilder: (BuildContext popoverContext) { - return URLEditorPopover( - cellController: - widget.cellControllerBuilder.build() as URLCellController, - onExit: () => _popoverController.close(), - ); - }, - onClose: () { - widget.onCellEditing.value = false; - }, ); + return urlEditor; }, ), ); } + void focusChanged({ + bool isUrlSubmitted = false, + }) { + if (mounted) { + if (_cellBloc.isClosed == false && + _controller.text != _cellBloc.state.content) { + _cellBloc.add(URLCellEvent.updateURL(_controller.text)); + } + if (isUrlSubmitted) { + _focusNode.unfocus(); + } + } + } + @override Future dispose() async { _cellBloc.close(); super.dispose(); } - Future _openUrlOrEdit(String url) async { - final uri = Uri.parse(url); - if (url.isNotEmpty && await canLaunchUrl(uri)) { - await launchUrl(uri); - } - } - @override void requestBeginFocus() { widget.onCellEditing.value = true; @@ -269,3 +275,36 @@ class _CopyURLAccessoryState extends State<_CopyURLAccessory> showMessageToast(LocaleKeys.grid_row_copyProperty.tr()); } } + +class _VisitURLAccessory extends StatefulWidget { + final URLCellController cellContext; + const _VisitURLAccessory({required this.cellContext, Key? key}) + : super(key: key); + + @override + State createState() => _VisitURLAccessoryState(); +} + +class _VisitURLAccessoryState extends State<_VisitURLAccessory> + with GridCellAccessoryState { + @override + Widget build(BuildContext context) { + return svgWidget( + "editor/link", + color: AFThemeExtension.of(context).textColor, + ); + } + + @override + void onTap() { + var content = + widget.cellContext.getCellData(loadIfNotExist: false)?.content ?? ""; + if (!content.contains('http://')) { + content = 'http://$content'; + } + final uri = Uri.parse(content); + if (content.isNotEmpty) { + canLaunchUrl(uri).then((value) => launchUrl(uri)); + } + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_detail.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_detail.dart index ec5cc794a0..7f1bcd6fa3 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_detail.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_detail.dart @@ -361,8 +361,8 @@ GridCellStyle? _customCellStyle(FieldType fieldType) { return GridURLCellStyle( placeholder: LocaleKeys.grid_row_textPlaceholder.tr(), accessoryTypes: [ - GridURLCellAccessoryType.edit, GridURLCellAccessoryType.copyURL, + GridURLCellAccessoryType.visitURL, ], ); }