2022-06-26 23:43:36 +02:00
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
|
|
|
|
2022-08-21 16:00:54 +02:00
|
|
|
import '../../hooks/logged_in_action.dart';
|
2022-08-21 22:57:56 +02:00
|
|
|
import '../../l10n/l10n.dart';
|
2022-08-23 00:34:10 +02:00
|
|
|
import '../../markdown_formatter.dart';
|
2022-08-21 20:47:30 +02:00
|
|
|
import '../../resources/links.dart';
|
|
|
|
import '../../url_launcher.dart';
|
2022-08-21 16:00:54 +02:00
|
|
|
import '../../util/async_store_listener.dart';
|
2022-08-21 19:03:22 +02:00
|
|
|
import '../../util/extensions/api.dart';
|
2022-06-28 00:40:37 +02:00
|
|
|
import '../../util/extensions/spaced.dart';
|
2022-08-21 16:00:54 +02:00
|
|
|
import '../../util/files.dart';
|
|
|
|
import '../../util/mobx_provider.dart';
|
|
|
|
import '../../util/observer_consumers.dart';
|
2022-07-06 11:28:06 +02:00
|
|
|
import '../../util/text_lines_iterator.dart';
|
2022-08-25 23:46:43 +02:00
|
|
|
import 'editor.dart';
|
2022-08-21 19:03:22 +02:00
|
|
|
import 'editor_picking_dialog.dart';
|
2022-08-21 16:00:54 +02:00
|
|
|
import 'editor_toolbar_store.dart';
|
2022-06-28 00:40:37 +02:00
|
|
|
|
2022-08-23 00:34:10 +02:00
|
|
|
class _Reformat {
|
2022-06-28 00:40:37 +02:00
|
|
|
final String text;
|
|
|
|
final int selectionBeginningShift;
|
|
|
|
final int selectionEndingShift;
|
2022-08-23 00:34:10 +02:00
|
|
|
_Reformat({
|
2022-06-28 00:40:37 +02:00
|
|
|
required this.text,
|
|
|
|
this.selectionBeginningShift = 0,
|
|
|
|
this.selectionEndingShift = 0,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2022-08-21 21:52:31 +02:00
|
|
|
enum HeaderLevel {
|
|
|
|
h1(1),
|
|
|
|
h2(2),
|
|
|
|
h3(3),
|
|
|
|
h4(4),
|
|
|
|
h5(5),
|
|
|
|
h6(6);
|
|
|
|
|
|
|
|
const HeaderLevel(this.value);
|
|
|
|
final int value;
|
|
|
|
}
|
|
|
|
|
2022-08-23 19:29:01 +02:00
|
|
|
class EditorToolbar extends HookWidget {
|
2022-08-25 23:46:43 +02:00
|
|
|
final EditorController controller;
|
|
|
|
|
2022-07-04 17:20:41 +02:00
|
|
|
static const _height = 50.0;
|
2022-08-21 16:00:54 +02:00
|
|
|
|
2022-08-25 23:46:43 +02:00
|
|
|
const EditorToolbar(this.controller);
|
2022-06-26 23:43:36 +02:00
|
|
|
|
|
|
|
@override
|
|
|
|
Widget build(BuildContext context) {
|
2022-08-25 23:46:43 +02:00
|
|
|
final visible = useListenable(controller.focusNode).hasFocus;
|
2022-08-23 19:29:01 +02:00
|
|
|
|
2022-08-23 00:34:10 +02:00
|
|
|
return MobxProvider(
|
2022-08-25 23:46:43 +02:00
|
|
|
create: (context) => EditorToolbarStore(controller.instanceHost),
|
2022-08-25 21:56:49 +02:00
|
|
|
child: Builder(builder: (context) {
|
2022-08-25 18:22:14 +02:00
|
|
|
return AsyncStoreListener(
|
2022-08-25 21:56:49 +02:00
|
|
|
asyncStore: context.read<EditorToolbarStore>().imageUploadState,
|
2022-08-25 18:22:14 +02:00
|
|
|
child: AnimatedSwitcher(
|
|
|
|
duration: kThemeAnimationDuration,
|
|
|
|
transitionBuilder: (child, animation) {
|
|
|
|
final offsetAnimation =
|
|
|
|
Tween<Offset>(begin: const Offset(0, 1.5), end: Offset.zero)
|
|
|
|
.animate(animation);
|
|
|
|
|
|
|
|
return SlideTransition(
|
|
|
|
position: offsetAnimation,
|
|
|
|
child: child,
|
|
|
|
);
|
|
|
|
},
|
|
|
|
child: visible
|
2022-08-25 23:21:28 +02:00
|
|
|
? Material(
|
2022-08-25 18:22:14 +02:00
|
|
|
color: Theme.of(context).canvasColor,
|
2022-08-25 23:56:11 +02:00
|
|
|
child: SafeArea(
|
|
|
|
child: SizedBox(
|
|
|
|
height: _height,
|
|
|
|
width: double.infinity,
|
|
|
|
child: SingleChildScrollView(
|
|
|
|
scrollDirection: Axis.horizontal,
|
|
|
|
child: _ToolbarBody(
|
|
|
|
controller: controller.textEditingController,
|
|
|
|
instanceHost: controller.instanceHost,
|
|
|
|
),
|
2022-08-25 23:21:28 +02:00
|
|
|
),
|
2022-08-25 18:22:14 +02:00
|
|
|
),
|
2022-08-23 19:29:01 +02:00
|
|
|
),
|
2022-08-25 18:22:14 +02:00
|
|
|
)
|
|
|
|
: const SizedBox.shrink(),
|
|
|
|
),
|
|
|
|
);
|
|
|
|
}),
|
2022-06-26 23:43:36 +02:00
|
|
|
);
|
|
|
|
}
|
2022-07-04 17:20:41 +02:00
|
|
|
|
2022-08-25 17:27:40 +02:00
|
|
|
static const safeArea = SizedBox(height: _height);
|
|
|
|
}
|
|
|
|
|
|
|
|
class BottomSticky extends StatelessWidget {
|
|
|
|
final Widget child;
|
|
|
|
const BottomSticky({required this.child});
|
|
|
|
|
|
|
|
@override
|
2022-08-25 23:56:11 +02:00
|
|
|
Widget build(BuildContext context) => Positioned(
|
|
|
|
bottom: 0,
|
|
|
|
child: SizedBox(
|
|
|
|
width: MediaQuery.of(context).size.width,
|
|
|
|
child: child,
|
2022-08-25 17:27:40 +02:00
|
|
|
),
|
|
|
|
);
|
2022-06-26 23:43:36 +02:00
|
|
|
}
|
2022-06-28 00:40:37 +02:00
|
|
|
|
2022-08-21 16:00:54 +02:00
|
|
|
class _ToolbarBody extends HookWidget {
|
|
|
|
const _ToolbarBody({
|
|
|
|
required this.controller,
|
|
|
|
required this.instanceHost,
|
|
|
|
});
|
|
|
|
|
|
|
|
final TextEditingController controller;
|
|
|
|
final String instanceHost;
|
|
|
|
|
|
|
|
@override
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
final loggedInAction = useLoggedInAction(instanceHost);
|
|
|
|
return Row(
|
|
|
|
children: [
|
|
|
|
IconButton(
|
2022-08-21 23:49:03 +02:00
|
|
|
onPressed: () => controller.surround(
|
|
|
|
before: '**',
|
|
|
|
placeholder: L10n.of(context).insert_text_here_placeholder),
|
2022-08-21 16:00:54 +02:00
|
|
|
icon: const Icon(Icons.format_bold),
|
2022-08-21 22:57:56 +02:00
|
|
|
tooltip: L10n.of(context).editor_bold,
|
2022-08-21 16:00:54 +02:00
|
|
|
),
|
|
|
|
IconButton(
|
2022-08-21 23:49:03 +02:00
|
|
|
onPressed: () => controller.surround(
|
|
|
|
before: '*',
|
|
|
|
placeholder: L10n.of(context).insert_text_here_placeholder),
|
2022-08-21 16:00:54 +02:00
|
|
|
icon: const Icon(Icons.format_italic),
|
2022-08-21 22:57:56 +02:00
|
|
|
tooltip: L10n.of(context).editor_italics,
|
2022-08-21 16:00:54 +02:00
|
|
|
),
|
|
|
|
IconButton(
|
|
|
|
onPressed: () async {
|
|
|
|
final r =
|
|
|
|
await AddLinkDialog.show(context, controller.selectionText);
|
|
|
|
if (r != null) controller.reformat((_) => r);
|
|
|
|
},
|
|
|
|
icon: const Icon(Icons.link),
|
2022-08-21 22:57:56 +02:00
|
|
|
tooltip: L10n.of(context).editor_link,
|
2022-08-21 16:00:54 +02:00
|
|
|
),
|
|
|
|
// Insert image
|
|
|
|
ObserverBuilder<EditorToolbarStore>(
|
|
|
|
builder: (context, store) {
|
|
|
|
return IconButton(
|
|
|
|
onPressed: loggedInAction((token) async {
|
|
|
|
if (store.imageUploadState.isLoading) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
try {
|
|
|
|
final pic = await pickImage();
|
|
|
|
// pic is null when the picker was cancelled
|
|
|
|
|
|
|
|
if (pic != null) {
|
2022-08-23 00:34:10 +02:00
|
|
|
final picUrl = await store.uploadImage(pic.path, token);
|
2022-08-21 16:00:54 +02:00
|
|
|
|
|
|
|
if (picUrl != null) {
|
2022-08-21 19:03:22 +02:00
|
|
|
controller.reformatSimple('![]($picUrl)');
|
2022-08-21 16:00:54 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
} on Exception catch (_) {
|
2022-08-23 00:34:10 +02:00
|
|
|
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
|
|
|
content: Text(L10n.of(context).failed_to_upload_image)));
|
2022-08-21 16:00:54 +02:00
|
|
|
}
|
|
|
|
}),
|
|
|
|
icon: store.imageUploadState.isLoading
|
|
|
|
? const CircularProgressIndicator.adaptive()
|
|
|
|
: const Icon(Icons.image),
|
2022-08-21 22:57:56 +02:00
|
|
|
tooltip: L10n.of(context).editor_image,
|
2022-08-21 16:00:54 +02:00
|
|
|
);
|
|
|
|
},
|
|
|
|
),
|
|
|
|
IconButton(
|
2022-08-21 19:03:22 +02:00
|
|
|
onPressed: () async {
|
|
|
|
final person = await PickPersonDialog.show(context);
|
|
|
|
|
|
|
|
if (person != null) {
|
|
|
|
final name =
|
|
|
|
'@${person.person.name}@${person.person.originInstanceHost}';
|
|
|
|
final link = person.person.actorId;
|
|
|
|
|
|
|
|
controller.reformatSimple('[$name]($link)');
|
|
|
|
}
|
|
|
|
},
|
2022-08-21 16:00:54 +02:00
|
|
|
icon: const Icon(Icons.person),
|
2022-08-21 22:57:56 +02:00
|
|
|
tooltip: L10n.of(context).editor_user,
|
2022-08-21 16:00:54 +02:00
|
|
|
),
|
|
|
|
IconButton(
|
2022-08-21 19:03:22 +02:00
|
|
|
onPressed: () async {
|
|
|
|
final community = await PickCommunityDialog.show(context);
|
|
|
|
if (community != null) {
|
|
|
|
final name =
|
|
|
|
'!${community.community.name}@${community.community.originInstanceHost}';
|
|
|
|
final link = community.community.actorId;
|
|
|
|
|
|
|
|
controller.reformatSimple('[$name]($link)');
|
|
|
|
}
|
|
|
|
},
|
2022-08-21 16:00:54 +02:00
|
|
|
icon: const Icon(Icons.home),
|
2022-08-21 22:57:56 +02:00
|
|
|
tooltip: L10n.of(context).editor_community,
|
2022-08-21 16:00:54 +02:00
|
|
|
),
|
2022-08-21 21:52:31 +02:00
|
|
|
PopupMenuButton<HeaderLevel>(
|
|
|
|
itemBuilder: (context) => [
|
|
|
|
for (final h in HeaderLevel.values)
|
|
|
|
PopupMenuItem(
|
|
|
|
value: h,
|
2022-08-22 23:59:56 +02:00
|
|
|
child: Text(h.name.toUpperCase()),
|
2022-08-25 21:56:49 +02:00
|
|
|
onTap: () {
|
|
|
|
final header = '${'#' * h.value} ';
|
|
|
|
|
|
|
|
if (!controller.firstSelectedLine.startsWith(header)) {
|
|
|
|
controller.insertAtBeginningOfFirstSelectedLine(header);
|
|
|
|
}
|
|
|
|
},
|
2022-08-21 21:52:31 +02:00
|
|
|
),
|
|
|
|
],
|
2022-08-21 22:57:56 +02:00
|
|
|
tooltip: L10n.of(context).editor_header,
|
2022-08-21 21:52:31 +02:00
|
|
|
child: const Icon(Icons.h_mobiledata),
|
2022-08-21 16:00:54 +02:00
|
|
|
),
|
|
|
|
IconButton(
|
2022-08-21 23:49:03 +02:00
|
|
|
onPressed: () => controller.surround(
|
|
|
|
before: '~~',
|
|
|
|
placeholder: L10n.of(context).insert_text_here_placeholder,
|
|
|
|
),
|
2022-08-21 16:00:54 +02:00
|
|
|
icon: const Icon(Icons.format_strikethrough),
|
2022-08-21 22:57:56 +02:00
|
|
|
tooltip: L10n.of(context).editor_strikethrough,
|
2022-08-21 16:00:54 +02:00
|
|
|
),
|
|
|
|
IconButton(
|
2022-08-21 21:52:31 +02:00
|
|
|
onPressed: () {
|
|
|
|
controller.insertAtBeginningOfEverySelectedLine('> ');
|
|
|
|
},
|
2022-08-21 16:00:54 +02:00
|
|
|
icon: const Icon(Icons.format_quote),
|
2022-08-21 22:57:56 +02:00
|
|
|
tooltip: L10n.of(context).editor_quote,
|
2022-08-21 16:00:54 +02:00
|
|
|
),
|
|
|
|
IconButton(
|
2022-08-21 22:57:56 +02:00
|
|
|
onPressed: () {
|
|
|
|
final line = controller.firstSelectedLine;
|
|
|
|
|
2022-08-25 17:24:05 +02:00
|
|
|
// if theres a list in place, remove it
|
|
|
|
final listRemoved = () {
|
|
|
|
for (final c in unorderedListTypes) {
|
|
|
|
if (line.startsWith('$c ')) {
|
|
|
|
controller.removeAtBeginningOfEverySelectedLine('$c ');
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}();
|
|
|
|
|
|
|
|
// if no list, then let's add one
|
|
|
|
if (!listRemoved) {
|
|
|
|
controller.insertAtBeginningOfEverySelectedLine(
|
|
|
|
'${unorderedListTypes.last} ');
|
2022-08-21 22:57:56 +02:00
|
|
|
}
|
|
|
|
},
|
|
|
|
icon: const Icon(Icons.format_list_bulleted),
|
|
|
|
tooltip: L10n.of(context).editor_list,
|
|
|
|
),
|
2022-08-21 16:00:54 +02:00
|
|
|
IconButton(
|
2022-08-21 23:49:03 +02:00
|
|
|
onPressed: () => controller.surround(
|
|
|
|
before: '`',
|
|
|
|
placeholder: L10n.of(context).insert_text_here_placeholder,
|
|
|
|
),
|
2022-08-21 16:00:54 +02:00
|
|
|
icon: const Icon(Icons.code),
|
2022-08-21 22:57:56 +02:00
|
|
|
tooltip: L10n.of(context).editor_code,
|
2022-08-21 16:00:54 +02:00
|
|
|
),
|
|
|
|
IconButton(
|
2022-08-21 23:49:03 +02:00
|
|
|
onPressed: () => controller.surround(
|
|
|
|
before: '~',
|
|
|
|
placeholder: L10n.of(context).insert_text_here_placeholder,
|
|
|
|
),
|
2022-08-21 16:00:54 +02:00
|
|
|
icon: const Icon(Icons.subscript),
|
2022-08-21 22:57:56 +02:00
|
|
|
tooltip: L10n.of(context).editor_subscript,
|
2022-08-21 16:00:54 +02:00
|
|
|
),
|
|
|
|
IconButton(
|
2022-08-21 23:49:03 +02:00
|
|
|
onPressed: () => controller.surround(
|
|
|
|
before: '^',
|
|
|
|
placeholder: L10n.of(context).insert_text_here_placeholder,
|
|
|
|
),
|
2022-08-21 16:00:54 +02:00
|
|
|
icon: const Icon(Icons.superscript),
|
2022-08-21 22:57:56 +02:00
|
|
|
tooltip: L10n.of(context).editor_superscript,
|
2022-08-21 16:00:54 +02:00
|
|
|
),
|
|
|
|
//spoiler
|
|
|
|
IconButton(
|
2022-08-21 22:57:56 +02:00
|
|
|
onPressed: () {
|
|
|
|
controller.reformat((selection) {
|
2022-08-25 17:50:06 +02:00
|
|
|
const textBeg = '\n::: spoiler spoiler\n';
|
|
|
|
final textMid = selection.isNotEmpty ? selection : '___';
|
|
|
|
const textEnd = '\n:::\n';
|
|
|
|
|
2022-08-23 00:34:10 +02:00
|
|
|
return _Reformat(
|
2022-08-25 17:50:06 +02:00
|
|
|
text: textBeg + textMid + textEnd,
|
|
|
|
selectionBeginningShift: textBeg.length,
|
|
|
|
selectionEndingShift:
|
|
|
|
textBeg.length + textMid.length - selection.length,
|
2022-08-21 22:57:56 +02:00
|
|
|
);
|
|
|
|
});
|
|
|
|
},
|
|
|
|
icon: const Icon(Icons.warning),
|
|
|
|
tooltip: L10n.of(context).editor_spoiler,
|
|
|
|
),
|
2022-08-21 20:47:30 +02:00
|
|
|
IconButton(
|
|
|
|
onPressed: () {
|
|
|
|
launchLink(link: markdownGuide, context: context);
|
|
|
|
},
|
2022-08-21 16:00:54 +02:00
|
|
|
icon: const Icon(Icons.question_mark),
|
2022-08-21 22:57:56 +02:00
|
|
|
tooltip: L10n.of(context).editor_help,
|
2022-08-21 16:00:54 +02:00
|
|
|
),
|
|
|
|
],
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-06-28 00:40:37 +02:00
|
|
|
class AddLinkDialog extends HookWidget {
|
2022-08-23 00:34:10 +02:00
|
|
|
final String label;
|
2022-06-28 00:40:37 +02:00
|
|
|
final String url;
|
|
|
|
final String selection;
|
|
|
|
|
2022-07-04 17:18:38 +02:00
|
|
|
static final _websiteRegex = RegExp(r'https?:\/\/', caseSensitive: false);
|
|
|
|
|
2022-06-28 00:40:37 +02:00
|
|
|
AddLinkDialog(this.selection)
|
2022-08-23 00:34:10 +02:00
|
|
|
: label = selection.startsWith(_websiteRegex) ? '' : selection,
|
2022-07-04 17:18:38 +02:00
|
|
|
url = selection.startsWith(_websiteRegex) ? selection : '';
|
2022-06-28 00:40:37 +02:00
|
|
|
|
|
|
|
@override
|
|
|
|
Widget build(BuildContext context) {
|
2022-08-23 00:34:10 +02:00
|
|
|
final labelController = useTextEditingController(text: label);
|
2022-06-28 00:40:37 +02:00
|
|
|
final urlController = useTextEditingController(text: url);
|
|
|
|
|
|
|
|
void submit() {
|
2022-08-21 23:08:00 +02:00
|
|
|
final link = () {
|
2022-08-23 00:34:10 +02:00
|
|
|
if (urlController.text.startsWith(RegExp('https?://'))) {
|
2022-08-21 23:08:00 +02:00
|
|
|
return urlController.text;
|
|
|
|
} else {
|
|
|
|
return 'https://${urlController.text}';
|
|
|
|
}
|
|
|
|
}();
|
2022-08-23 00:34:10 +02:00
|
|
|
final finalString = '[${labelController.text}]($link)';
|
|
|
|
Navigator.of(context).pop(_Reformat(
|
2022-06-28 00:40:37 +02:00
|
|
|
text: finalString,
|
|
|
|
selectionBeginningShift: finalString.length,
|
|
|
|
selectionEndingShift: finalString.length - selection.length,
|
|
|
|
));
|
|
|
|
}
|
|
|
|
|
|
|
|
return AlertDialog(
|
2022-08-23 00:34:10 +02:00
|
|
|
title: Text(L10n.of(context).add_link),
|
2022-06-28 00:40:37 +02:00
|
|
|
content: Column(
|
|
|
|
mainAxisSize: MainAxisSize.min,
|
|
|
|
children: [
|
|
|
|
TextField(
|
2022-08-23 00:34:10 +02:00
|
|
|
controller: labelController,
|
|
|
|
decoration: InputDecoration(
|
|
|
|
hintText: L10n.of(context).editor_add_link_label),
|
2022-06-28 00:40:37 +02:00
|
|
|
textInputAction: TextInputAction.next,
|
|
|
|
autofocus: true,
|
|
|
|
),
|
|
|
|
TextField(
|
|
|
|
controller: urlController,
|
|
|
|
decoration: const InputDecoration(hintText: 'https://example.com'),
|
|
|
|
onEditingComplete: submit,
|
|
|
|
autocorrect: false,
|
|
|
|
),
|
|
|
|
].spaced(10),
|
|
|
|
),
|
|
|
|
actions: [
|
|
|
|
TextButton(
|
|
|
|
onPressed: () => Navigator.of(context).pop(),
|
2022-08-23 00:34:10 +02:00
|
|
|
child: Text(L10n.of(context).cancel)),
|
2022-06-28 00:40:37 +02:00
|
|
|
ElevatedButton(
|
|
|
|
onPressed: submit,
|
2022-08-23 00:34:10 +02:00
|
|
|
child: Text(L10n.of(context).add_link),
|
2022-06-28 00:40:37 +02:00
|
|
|
)
|
|
|
|
],
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2022-08-23 00:34:10 +02:00
|
|
|
static Future<_Reformat?> show(BuildContext context, String selection) async {
|
2022-06-28 00:40:37 +02:00
|
|
|
return showDialog(
|
|
|
|
context: context,
|
|
|
|
builder: (context) => AddLinkDialog(selection),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
2022-08-23 00:34:10 +02:00
|
|
|
|
|
|
|
extension on TextEditingController {
|
|
|
|
String get selectionText =>
|
|
|
|
text.substring(selection.baseOffset, selection.extentOffset);
|
|
|
|
String get beforeSelectionText => text.substring(0, selection.baseOffset);
|
|
|
|
String get afterSelectionText => text.substring(selection.extentOffset);
|
|
|
|
|
|
|
|
/// surrounds selection with given strings. If nothing is selected, placeholder is used in the middle
|
|
|
|
void surround({
|
|
|
|
required String before,
|
|
|
|
required String placeholder,
|
|
|
|
|
|
|
|
/// after = before if null
|
|
|
|
String? after,
|
|
|
|
}) {
|
|
|
|
after ??= before;
|
|
|
|
final beg = text.substring(0, selection.baseOffset);
|
|
|
|
final mid = () {
|
|
|
|
final m = text.substring(selection.baseOffset, selection.extentOffset);
|
|
|
|
if (m.isEmpty) return placeholder;
|
|
|
|
return m;
|
|
|
|
}();
|
|
|
|
final end = text.substring(selection.extentOffset);
|
|
|
|
|
|
|
|
value = value.copyWith(
|
|
|
|
text: '$beg$before$mid$after$end',
|
|
|
|
selection: selection.copyWith(
|
|
|
|
baseOffset: selection.baseOffset + before.length,
|
|
|
|
extentOffset: selection.baseOffset + before.length + mid.length,
|
|
|
|
));
|
|
|
|
}
|
|
|
|
|
|
|
|
String get firstSelectedLine {
|
2022-08-25 21:56:49 +02:00
|
|
|
if (text.isEmpty) {
|
|
|
|
return '';
|
|
|
|
}
|
|
|
|
final val = text.substring(text.getBeginningOfTheLine(selection.start - 1),
|
|
|
|
text.getEndOfTheLine(selection.end) - 1);
|
|
|
|
return val;
|
2022-08-23 00:34:10 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
void insertAtBeginningOfFirstSelectedLine(String s) {
|
|
|
|
final lines = TextLinesIterator.fromController(this)..moveNext();
|
|
|
|
lines.current = s + lines.current;
|
|
|
|
value = value.copyWith(
|
|
|
|
text: lines.text,
|
|
|
|
selection: selection.copyWith(
|
|
|
|
baseOffset: selection.baseOffset + s.length,
|
|
|
|
extentOffset: selection.extentOffset + s.length,
|
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
void removeAtBeginningOfEverySelectedLine(String s) {
|
|
|
|
final lines = TextLinesIterator.fromController(this);
|
|
|
|
var linesCount = 0;
|
|
|
|
while (lines.moveNext()) {
|
|
|
|
if (lines.isWithinSelection) {
|
2022-08-23 00:36:31 +02:00
|
|
|
if (lines.current.startsWith(s)) {
|
2022-08-23 00:34:10 +02:00
|
|
|
lines.current = lines.current.substring(s.length);
|
|
|
|
linesCount++;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
value = value.copyWith(
|
|
|
|
text: lines.text,
|
|
|
|
selection: selection.copyWith(
|
|
|
|
baseOffset: selection.baseOffset - s.length,
|
|
|
|
extentOffset: selection.extentOffset - s.length * linesCount,
|
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
void insertAtBeginningOfEverySelectedLine(String s) {
|
|
|
|
final lines = TextLinesIterator.fromController(this);
|
|
|
|
var linesCount = 0;
|
|
|
|
while (lines.moveNext()) {
|
|
|
|
if (lines.isWithinSelection) {
|
2022-08-23 00:36:31 +02:00
|
|
|
if (!lines.current.startsWith(s)) {
|
2022-08-23 00:34:10 +02:00
|
|
|
lines.current = '$s${lines.current}';
|
|
|
|
linesCount++;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
value = value.copyWith(
|
|
|
|
text: lines.text,
|
|
|
|
selection: selection.copyWith(
|
|
|
|
baseOffset: selection.baseOffset + s.length,
|
|
|
|
extentOffset: selection.extentOffset + s.length * linesCount,
|
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
void reformat(_Reformat Function(String selection) reformatter) {
|
|
|
|
final beg = beforeSelectionText;
|
|
|
|
final mid = selectionText;
|
|
|
|
final end = afterSelectionText;
|
|
|
|
|
|
|
|
final r = reformatter(mid);
|
|
|
|
value = value.copyWith(
|
|
|
|
text: '$beg${r.text}$end',
|
|
|
|
selection: selection.copyWith(
|
|
|
|
baseOffset: selection.baseOffset + r.selectionBeginningShift,
|
|
|
|
extentOffset: selection.extentOffset + r.selectionEndingShift,
|
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
void reformatSimple(String text) =>
|
|
|
|
reformat((selection) => _Reformat(text: text));
|
|
|
|
}
|