chore: revamp mobile url editor (#4602)

* chore: revamp mobile url editor

* chore: add i18n

* chore: use shared url launcher

* chore: translation bump

* chore: add a toast notification after URL is copied to clipboard

* chore: listen to onchanged

* chore: improve text field editing experience

* chore: disable quick action buttons if url cell data is empty

* chore: apply suggestions from code review

Co-authored-by: Mathias Mogensen <42929161+Xazin@users.noreply.github.com>

* chore: provide the bloc and watch changes

---------

Co-authored-by: Mathias Mogensen <42929161+Xazin@users.noreply.github.com>
This commit is contained in:
Richard Shiue
2024-03-14 09:36:29 +08:00
committed by GitHub
parent 48cac4c5ac
commit 25d4b4f718
20 changed files with 192 additions and 140 deletions

View File

@ -115,7 +115,7 @@ class _CopyURLAccessoryState extends State<_CopyURLAccessory>
Widget build(BuildContext context) {
if (widget.cellDataNotifier.value.isNotEmpty) {
return FlowyTooltip(
message: LocaleKeys.tooltip_urlCopyAccessory.tr(),
message: LocaleKeys.grid_url_copy.tr(),
preferBelow: false,
child: _URLAccessoryIconContainer(
child: FlowySvg(
@ -161,7 +161,7 @@ class _VisitURLAccessoryState extends State<_VisitURLAccessory>
Widget build(BuildContext context) {
if (widget.cellDataNotifier.value.isNotEmpty) {
return FlowyTooltip(
message: LocaleKeys.tooltip_urlLaunchAccessory.tr(),
message: LocaleKeys.grid_url_launch.tr(),
preferBelow: false,
child: _URLAccessoryIconContainer(
child: FlowySvg(

View File

@ -1,5 +1,8 @@
import 'dart:async';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_quick_action_button.dart';
import 'package:appflowy/core/helpers/url_launcher.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
@ -8,8 +11,13 @@ import 'package:appflowy/plugins/database/widgets/row/accessory/cell_accessory.d
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
import 'package:appflowy/plugins/database/application/cell/bloc/url_cell_bloc.dart';
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:go_router/go_router.dart';
import '../desktop_grid/desktop_grid_url_cell.dart';
import '../desktop_row_detail/desktop_row_detail_url_cell.dart';
@ -106,7 +114,8 @@ class _GridURLCellState extends GridEditableTextCell<EditableURLCell> {
child: BlocListener<URLCellBloc, URLCellState>(
listenWhen: (previous, current) => previous.content != current.content,
listener: (context, state) {
_textEditingController.text = state.content;
_textEditingController.value =
_textEditingController.value.copyWith(text: state.content);
widget._cellDataNotifier.value = state.content;
},
child: widget.skin.build(
@ -135,6 +144,74 @@ class _GridURLCellState extends GridEditableTextCell<EditableURLCell> {
String? onCopy() => cellBloc.state.content;
}
class MobileURLEditor extends StatelessWidget {
const MobileURLEditor({
super.key,
required this.textEditingController,
});
final TextEditingController textEditingController;
@override
Widget build(BuildContext context) {
return Column(
children: [
const VSpace(4.0),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: FlowyTextField(
controller: textEditingController,
hintStyle: Theme.of(context)
.textTheme
.bodyMedium
?.copyWith(color: Theme.of(context).hintColor),
hintText: LocaleKeys.grid_url_textFieldHint.tr(),
textStyle: Theme.of(context).textTheme.bodyMedium,
keyboardType: TextInputType.url,
hintTextConstraints: const BoxConstraints(maxHeight: 52),
onChanged: (_) {
if (textEditingController.value.composing.isCollapsed) {
context
.read<URLCellBloc>()
.add(URLCellEvent.updateURL(textEditingController.text));
}
},
onSubmitted: (text) =>
context.read<URLCellBloc>().add(URLCellEvent.updateURL(text)),
),
),
const VSpace(8.0),
MobileQuickActionButton(
enable: context.watch<URLCellBloc>().state.content.isNotEmpty,
onTap: () {
openUrlCellLink(textEditingController.text);
context.pop();
},
icon: FlowySvgs.url_s,
text: LocaleKeys.grid_url_launch.tr(),
),
const Divider(height: 8.5, thickness: 0.5),
MobileQuickActionButton(
enable: context.watch<URLCellBloc>().state.content.isNotEmpty,
onTap: () {
Clipboard.setData(
ClipboardData(text: textEditingController.text),
);
Fluttertoast.showToast(
msg: LocaleKeys.grid_url_copiedNotification.tr(),
gravity: ToastGravity.BOTTOM,
);
context.pop();
},
icon: FlowySvgs.copy_s,
text: LocaleKeys.grid_url_copy.tr(),
),
const Divider(height: 8.5, thickness: 0.5),
],
);
}
}
void openUrlCellLink(String content) {
if (RegExp(regexUrl).hasMatch(content)) {
const linkPrefix = [

View File

@ -1,14 +1,9 @@
import 'package:flutter/material.dart';
import 'package:appflowy/core/helpers/url_launcher.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart';
import 'package:appflowy/plugins/database/application/cell/bloc/url_cell_bloc.dart';
import 'package:appflowy/plugins/database/widgets/row/accessory/cell_accessory.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import '../editable_cell_skeleton/url.dart';
@ -25,53 +20,9 @@ class MobileGridURLCellSkin extends IEditableURLCellSkin {
return BlocSelector<URLCellBloc, URLCellState, String>(
selector: (state) => state.content,
builder: (context, content) {
if (content.isEmpty) {
return TextField(
focusNode: focusNode,
keyboardType: TextInputType.url,
decoration: const InputDecoration(
enabledBorder: InputBorder.none,
focusedBorder: InputBorder.none,
contentPadding: EdgeInsets.symmetric(
horizontal: 14,
vertical: 12,
),
isCollapsed: true,
),
onTapOutside: (event) =>
FocusManager.instance.primaryFocus?.unfocus(),
onSubmitted: (value) => bloc.add(URLCellEvent.updateURL(value)),
);
}
return GestureDetector(
onTap: () {
if (content.isEmpty) {
return;
}
final shouldAddScheme = !['http', 'https']
.any((pattern) => content.startsWith(pattern));
final url = shouldAddScheme ? 'http://$content' : content;
afLaunchUrlString(url);
},
onLongPress: () => showMobileBottomSheet(
context,
title: LocaleKeys.board_mobile_editURL.tr(),
showHeader: true,
showCloseButton: true,
builder: (_) {
final controller = TextEditingController(text: content);
return TextField(
controller: controller,
autofocus: true,
keyboardType: TextInputType.url,
onEditingComplete: () {
bloc.add(URLCellEvent.updateURL(controller.text));
context.pop();
},
);
},
),
onTap: () => _showURLEditor(context, bloc, textEditingController),
behavior: HitTestBehavior.opaque,
child: Container(
alignment: AlignmentDirectional.centerStart,
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
@ -92,6 +43,23 @@ class MobileGridURLCellSkin extends IEditableURLCellSkin {
);
}
void _showURLEditor(
BuildContext context,
URLCellBloc bloc,
TextEditingController textEditingController,
) {
showMobileBottomSheet(
context,
showDragHandle: true,
builder: (context) => BlocProvider.value(
value: bloc,
child: MobileURLEditor(
textEditingController: textEditingController,
),
),
);
}
@override
List<GridCellAccessoryBuilder<State<StatefulWidget>>> accessoryBuilder(
GridCellAccessoryBuildContext context,

View File

@ -1,6 +1,5 @@
import 'package:flutter/material.dart';
import 'package:appflowy/core/helpers/url_launcher.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart';
import 'package:appflowy/plugins/database/application/cell/bloc/url_cell_bloc.dart';
@ -8,7 +7,6 @@ import 'package:appflowy/plugins/database/widgets/row/accessory/cell_accessory.d
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import '../editable_cell_skeleton/url.dart';
@ -27,17 +25,18 @@ class MobileRowDetailURLCellSkin extends IEditableURLCellSkin {
builder: (context, content) {
return InkWell(
borderRadius: const BorderRadius.all(Radius.circular(14)),
onTap: () {
if (content.isEmpty) {
_showURLEditor(context, bloc, content);
return;
}
final shouldAddScheme = !['http', 'https']
.any((pattern) => content.startsWith(pattern));
final url = shouldAddScheme ? 'http://$content' : content;
afLaunchUrlString(url);
},
onLongPress: () => _showURLEditor(context, bloc, content),
onTap: () => showMobileBottomSheet(
context,
showDragHandle: true,
builder: (_) {
return BlocProvider.value(
value: bloc,
child: MobileURLEditor(
textEditingController: textEditingController,
),
);
},
),
child: Container(
constraints: const BoxConstraints(
minHeight: 48,
@ -77,25 +76,4 @@ class MobileRowDetailURLCellSkin extends IEditableURLCellSkin {
URLCellDataNotifier cellDataNotifier,
) =>
const [];
void _showURLEditor(BuildContext context, URLCellBloc bloc, String content) {
final controller = TextEditingController(text: content);
showMobileBottomSheet(
context,
title: LocaleKeys.board_mobile_editURL.tr(),
showHeader: true,
showCloseButton: true,
builder: (_) {
return TextField(
controller: controller,
autofocus: true,
keyboardType: TextInputType.url,
onEditingComplete: () {
bloc.add(URLCellEvent.updateURL(controller.text));
context.pop();
},
);
},
).then((_) => controller.dispose());
}
}