CR Small Changes
* regex fix http?s -> https? * file rename: formatter.dart -> markdown_formatter.dart to match the class name inside * add + as continuoable list * rename: listContinuation -> unorderedListContinuation numberedListContinuation -> orderedListContinuation * fix typo: convenience * fix: doc instead of comment * rename for readability: startingIndex -> from * function & arg rename: lineBefore(int endingIndex) -> lineUpTo(int characterIndex) * parse -> tryParse * localize user picker & commmunity picker * HookWidget -> StatelessWidget where needed * Toolbar -> EditorToolbar for less ambiguity * fix typo: surroungs -> surrounds * remove debug logA * more localization stuff * title -> label on add link dialog * Reformat -> _Reformat * use store when in scope instead of context.read * remove useless Stack (oops)
This commit is contained in:
parent
821558314e
commit
cda72a1174
|
@ -415,5 +415,29 @@
|
||||||
"insert_text_here_placeholder": "[write text here]",
|
"insert_text_here_placeholder": "[write text here]",
|
||||||
"@insert_text_here_placeholder": {
|
"@insert_text_here_placeholder": {
|
||||||
"description": "placeholder for text in markdown editor when inserting stuff like * for italics or ** for bold, etc."
|
"description": "placeholder for text in markdown editor when inserting stuff like * for italics or ** for bold, etc."
|
||||||
|
},
|
||||||
|
"select_user": "Select User",
|
||||||
|
"@select_user": {
|
||||||
|
"description": "Title on a popup that lets a user search and select another user"
|
||||||
|
},
|
||||||
|
"select_community": "Select Community",
|
||||||
|
"@select_community": {
|
||||||
|
"description": "Title on a popup that lets a user search and select a community"
|
||||||
|
},
|
||||||
|
"add_link": "Add link",
|
||||||
|
"@add_link": {
|
||||||
|
"description": "title on top of a link insertion popup in a markdown editor"
|
||||||
|
},
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"@cancel": {
|
||||||
|
"description": "Cancel button on popup"
|
||||||
|
},
|
||||||
|
"editor_add_link_label": "label",
|
||||||
|
"@editor_add_link_label": {
|
||||||
|
"description": "palceholder for link label on an Add link popup in markdown editor"
|
||||||
|
},
|
||||||
|
"failed_to_upload_image": "Failed to upload image",
|
||||||
|
"@failed_to_upload_image": {
|
||||||
|
"description": "shows up on a snackbar when the image upload failed (duh)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
extension Utilities on String {
|
extension Utilities on String {
|
||||||
int getBeginningOfTheLine(int startingIndex) {
|
int getBeginningOfTheLine(int from) {
|
||||||
if (startingIndex <= 0) return 0;
|
if (from <= 0) return 0;
|
||||||
for (var i = startingIndex; i >= 0; i--) {
|
for (var i = from; i >= 0; i--) {
|
||||||
if (this[i] == '\n') return i + 1;
|
if (this[i] == '\n') return i + 1;
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
|
@ -17,9 +17,9 @@ extension Utilities on String {
|
||||||
return length - 1;
|
return length - 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// returns the line that ends at endingIndex
|
/// returns the line that ends at endingIndex
|
||||||
String lineBefore(int endingIndex) {
|
String lineUpTo(int characterIndex) {
|
||||||
return substring(getBeginningOfTheLine(endingIndex), endingIndex + 1);
|
return substring(getBeginningOfTheLine(characterIndex), characterIndex + 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,7 +39,7 @@ extension on TextEditingValue {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Provides convinience formatting in markdown text fields
|
/// Provides convenience formatting in markdown text fields
|
||||||
class MarkdownFormatter extends TextInputFormatter {
|
class MarkdownFormatter extends TextInputFormatter {
|
||||||
@override
|
@override
|
||||||
TextEditingValue formatEditUpdate(
|
TextEditingValue formatEditUpdate(
|
||||||
|
@ -52,9 +52,10 @@ class MarkdownFormatter extends TextInputFormatter {
|
||||||
|
|
||||||
if (char == '\n') {
|
if (char == '\n') {
|
||||||
final lineBefore =
|
final lineBefore =
|
||||||
newValue.text.lineBefore(newValue.selection.baseOffset - 2);
|
newValue.text.lineUpTo(newValue.selection.baseOffset - 2);
|
||||||
|
|
||||||
TextEditingValue listContinuation(String listChar, TextEditingValue tev) {
|
TextEditingValue unorderedListContinuation(
|
||||||
|
String listChar, TextEditingValue tev) {
|
||||||
final regex = RegExp(r'(\s*)' '${RegExp.escape(listChar)} ');
|
final regex = RegExp(r'(\s*)' '${RegExp.escape(listChar)} ');
|
||||||
final match = regex.matchAsPrefix(lineBefore);
|
final match = regex.matchAsPrefix(lineBefore);
|
||||||
if (match == null) {
|
if (match == null) {
|
||||||
|
@ -65,7 +66,7 @@ class MarkdownFormatter extends TextInputFormatter {
|
||||||
return tev.append('$indent$listChar ');
|
return tev.append('$indent$listChar ');
|
||||||
}
|
}
|
||||||
|
|
||||||
TextEditingValue numberedListContinuation(
|
TextEditingValue orderedListContinuation(
|
||||||
String afterNumberChar, TextEditingValue tev) {
|
String afterNumberChar, TextEditingValue tev) {
|
||||||
final regex =
|
final regex =
|
||||||
RegExp(r'(\s*)(\d+)' '${RegExp.escape(afterNumberChar)} ');
|
RegExp(r'(\s*)(\d+)' '${RegExp.escape(afterNumberChar)} ');
|
||||||
|
@ -74,15 +75,16 @@ class MarkdownFormatter extends TextInputFormatter {
|
||||||
return tev;
|
return tev;
|
||||||
}
|
}
|
||||||
final indent = match.group(1);
|
final indent = match.group(1);
|
||||||
final number = int.parse(match.group(2)!) + 1;
|
final number = int.tryParse(match.group(2)!) ?? 0 + 1;
|
||||||
|
|
||||||
return tev.append('$indent$number$afterNumberChar ');
|
return tev.append('$indent$number$afterNumberChar ');
|
||||||
}
|
}
|
||||||
|
|
||||||
newVal = listContinuation('-', newVal);
|
newVal = unorderedListContinuation('-', newVal);
|
||||||
newVal = listContinuation('*', newVal);
|
newVal = unorderedListContinuation('*', newVal);
|
||||||
newVal = numberedListContinuation('.', newVal);
|
newVal = unorderedListContinuation('+', newVal);
|
||||||
newVal = numberedListContinuation(')', newVal);
|
newVal = orderedListContinuation('.', newVal);
|
||||||
|
newVal = orderedListContinuation(')', newVal);
|
||||||
}
|
}
|
||||||
|
|
||||||
return newVal;
|
return newVal;
|
|
@ -134,7 +134,7 @@ class CreatePostPage extends HookWidget {
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
Toolbar.safeArea,
|
EditorToolbar.safeArea,
|
||||||
].spaced(6),
|
].spaced(6),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -145,7 +145,7 @@ class CreatePostPage extends HookWidget {
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
Toolbar(
|
EditorToolbar(
|
||||||
controller: bodyController,
|
controller: bodyController,
|
||||||
instanceHost: context.read<CreatePostStore>().instanceHost,
|
instanceHost: context.read<CreatePostStore>().instanceHost,
|
||||||
),
|
),
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
|
||||||
import '../../formatter.dart';
|
import '../../markdown_formatter.dart';
|
||||||
import '../markdown_text.dart';
|
import '../markdown_text.dart';
|
||||||
|
|
||||||
export 'editor_toolbar.dart';
|
export 'editor_toolbar.dart';
|
||||||
|
@ -52,22 +52,18 @@ class Editor extends HookWidget {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Stack(
|
return TextField(
|
||||||
children: [
|
controller: actualController,
|
||||||
TextField(
|
focusNode: focusNode,
|
||||||
controller: actualController,
|
autofocus: autofocus,
|
||||||
focusNode: focusNode,
|
keyboardType: TextInputType.multiline,
|
||||||
autofocus: autofocus,
|
textCapitalization: TextCapitalization.sentences,
|
||||||
keyboardType: TextInputType.multiline,
|
onChanged: onChanged,
|
||||||
textCapitalization: TextCapitalization.sentences,
|
onSubmitted: onSubmitted,
|
||||||
onChanged: onChanged,
|
maxLines: maxLines,
|
||||||
onSubmitted: onSubmitted,
|
minLines: minLines,
|
||||||
maxLines: maxLines,
|
decoration: InputDecoration(labelText: labelText),
|
||||||
minLines: minLines,
|
inputFormatters: [MarkdownFormatter()],
|
||||||
decoration: InputDecoration(labelText: labelText),
|
|
||||||
inputFormatters: [MarkdownFormatter()],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
|
||||||
import 'package:flutter_typeahead/flutter_typeahead.dart';
|
import 'package:flutter_typeahead/flutter_typeahead.dart';
|
||||||
import 'package:lemmy_api_client/v3.dart';
|
import 'package:lemmy_api_client/v3.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
import '../../../util/extensions/api.dart';
|
import '../../../util/extensions/api.dart';
|
||||||
import '../../../widgets/avatar.dart';
|
import '../../../widgets/avatar.dart';
|
||||||
|
import '../../l10n/l10n.dart';
|
||||||
import '../../stores/accounts_store.dart';
|
import '../../stores/accounts_store.dart';
|
||||||
import 'editor_toolbar_store.dart';
|
import 'editor_toolbar_store.dart';
|
||||||
|
|
||||||
class PickPersonDialog extends HookWidget {
|
class PickPersonDialog extends StatelessWidget {
|
||||||
final EditorToolbarStore store;
|
final EditorToolbarStore store;
|
||||||
|
|
||||||
const PickPersonDialog._(this.store);
|
const PickPersonDialog._(this.store);
|
||||||
|
@ -20,7 +20,7 @@ class PickPersonDialog extends HookWidget {
|
||||||
context.read<AccountsStore>().defaultUserDataFor(store.instanceHost);
|
context.read<AccountsStore>().defaultUserDataFor(store.instanceHost);
|
||||||
|
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
title: const Text('Select User'),
|
title: Text(L10n.of(context).select_user),
|
||||||
content: TypeAheadField<PersonViewSafe>(
|
content: TypeAheadField<PersonViewSafe>(
|
||||||
suggestionsCallback: (pattern) async {
|
suggestionsCallback: (pattern) async {
|
||||||
if (pattern.trim().isEmpty) return const Iterable.empty();
|
if (pattern.trim().isEmpty) return const Iterable.empty();
|
||||||
|
@ -70,7 +70,7 @@ class PickPersonDialog extends HookWidget {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class PickCommunityDialog extends HookWidget {
|
class PickCommunityDialog extends StatelessWidget {
|
||||||
final EditorToolbarStore store;
|
final EditorToolbarStore store;
|
||||||
|
|
||||||
const PickCommunityDialog._(this.store);
|
const PickCommunityDialog._(this.store);
|
||||||
|
@ -81,7 +81,7 @@ class PickCommunityDialog extends HookWidget {
|
||||||
context.read<AccountsStore>().defaultUserDataFor(store.instanceHost);
|
context.read<AccountsStore>().defaultUserDataFor(store.instanceHost);
|
||||||
|
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
title: const Text('Select Community'),
|
title: Text(L10n.of(context).select_community),
|
||||||
content: TypeAheadField<CommunityView>(
|
content: TypeAheadField<CommunityView>(
|
||||||
suggestionsCallback: (pattern) async {
|
suggestionsCallback: (pattern) async {
|
||||||
if (pattern.trim().isEmpty) return const Iterable.empty();
|
if (pattern.trim().isEmpty) return const Iterable.empty();
|
||||||
|
|
|
@ -1,11 +1,9 @@
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:logging/logging.dart';
|
|
||||||
|
|
||||||
import '../../formatter.dart';
|
|
||||||
import '../../hooks/logged_in_action.dart';
|
import '../../hooks/logged_in_action.dart';
|
||||||
import '../../l10n/l10n.dart';
|
import '../../l10n/l10n.dart';
|
||||||
|
import '../../markdown_formatter.dart';
|
||||||
import '../../resources/links.dart';
|
import '../../resources/links.dart';
|
||||||
import '../../url_launcher.dart';
|
import '../../url_launcher.dart';
|
||||||
import '../../util/async_store_listener.dart';
|
import '../../util/async_store_listener.dart';
|
||||||
|
@ -18,127 +16,17 @@ import '../../util/text_lines_iterator.dart';
|
||||||
import 'editor_picking_dialog.dart';
|
import 'editor_picking_dialog.dart';
|
||||||
import 'editor_toolbar_store.dart';
|
import 'editor_toolbar_store.dart';
|
||||||
|
|
||||||
class Reformat {
|
class _Reformat {
|
||||||
final String text;
|
final String text;
|
||||||
final int selectionBeginningShift;
|
final int selectionBeginningShift;
|
||||||
final int selectionEndingShift;
|
final int selectionEndingShift;
|
||||||
Reformat({
|
_Reformat({
|
||||||
required this.text,
|
required this.text,
|
||||||
this.selectionBeginningShift = 0,
|
this.selectionBeginningShift = 0,
|
||||||
this.selectionEndingShift = 0,
|
this.selectionEndingShift = 0,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
/// surroungs 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 {
|
|
||||||
if (text.isEmpty) return '';
|
|
||||||
return text.substring(text.getBeginningOfTheLine(selection.start - 1),
|
|
||||||
text.getEndOfTheLine(selection.end));
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
|
||||||
if (lines.current.startsWith(RegExp.escape(s))) {
|
|
||||||
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) {
|
|
||||||
if (!lines.current.startsWith(RegExp.escape(s))) {
|
|
||||||
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));
|
|
||||||
}
|
|
||||||
|
|
||||||
enum HeaderLevel {
|
enum HeaderLevel {
|
||||||
h1(1),
|
h1(1),
|
||||||
h2(2),
|
h2(2),
|
||||||
|
@ -151,21 +39,21 @@ enum HeaderLevel {
|
||||||
final int value;
|
final int value;
|
||||||
}
|
}
|
||||||
|
|
||||||
class Toolbar extends HookWidget {
|
class EditorToolbar extends StatelessWidget {
|
||||||
final TextEditingController controller;
|
final TextEditingController controller;
|
||||||
final String instanceHost;
|
final String instanceHost;
|
||||||
final EditorToolbarStore store;
|
final EditorToolbarStore store;
|
||||||
static const _height = 50.0;
|
static const _height = 50.0;
|
||||||
|
|
||||||
Toolbar({
|
EditorToolbar({
|
||||||
required this.controller,
|
required this.controller,
|
||||||
required this.instanceHost,
|
required this.instanceHost,
|
||||||
}) : store = EditorToolbarStore(instanceHost);
|
}) : store = EditorToolbarStore(instanceHost);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return MobxProvider.value(
|
return MobxProvider(
|
||||||
value: store,
|
create: (context) => store,
|
||||||
child: AsyncStoreListener(
|
child: AsyncStoreListener(
|
||||||
asyncStore: store.imageUploadState,
|
asyncStore: store.imageUploadState,
|
||||||
child: Container(
|
child: Container(
|
||||||
|
@ -235,22 +123,19 @@ class _ToolbarBody extends HookWidget {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
// FIXME: for some reason it doesn't go past this line on iOS. idk why
|
|
||||||
final pic = await pickImage();
|
final pic = await pickImage();
|
||||||
// pic is null when the picker was cancelled
|
// pic is null when the picker was cancelled
|
||||||
|
|
||||||
if (pic != null) {
|
if (pic != null) {
|
||||||
final picUrl = await context
|
final picUrl = await store.uploadImage(pic.path, token);
|
||||||
.read<EditorToolbarStore>()
|
|
||||||
.uploadImage(pic.path, token);
|
|
||||||
|
|
||||||
if (picUrl != null) {
|
if (picUrl != null) {
|
||||||
controller.reformatSimple('![]($picUrl)');
|
controller.reformatSimple('![]($picUrl)');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} on Exception catch (_) {
|
} on Exception catch (_) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||||
const SnackBar(content: Text('Failed to upload image')));
|
content: Text(L10n.of(context).failed_to_upload_image)));
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
icon: store.imageUploadState.isLoading
|
icon: store.imageUploadState.isLoading
|
||||||
|
@ -366,9 +251,7 @@ class _ToolbarBody extends HookWidget {
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
controller.reformat((selection) {
|
controller.reformat((selection) {
|
||||||
final insides = selection.isNotEmpty ? selection : '___';
|
final insides = selection.isNotEmpty ? selection : '___';
|
||||||
Logger.root
|
return _Reformat(
|
||||||
.info([21, 21 + insides.length, insides, insides.length]);
|
|
||||||
return Reformat(
|
|
||||||
text: '\n::: spoiler spoiler\n$insides\n:::\n',
|
text: '\n::: spoiler spoiler\n$insides\n:::\n',
|
||||||
selectionBeginningShift: 21,
|
selectionBeginningShift: 21,
|
||||||
selectionEndingShift: 21 + insides.length - selection.length,
|
selectionEndingShift: 21 + insides.length - selection.length,
|
||||||
|
@ -391,31 +274,31 @@ class _ToolbarBody extends HookWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class AddLinkDialog extends HookWidget {
|
class AddLinkDialog extends HookWidget {
|
||||||
final String title;
|
final String label;
|
||||||
final String url;
|
final String url;
|
||||||
final String selection;
|
final String selection;
|
||||||
|
|
||||||
static final _websiteRegex = RegExp(r'https?:\/\/', caseSensitive: false);
|
static final _websiteRegex = RegExp(r'https?:\/\/', caseSensitive: false);
|
||||||
|
|
||||||
AddLinkDialog(this.selection)
|
AddLinkDialog(this.selection)
|
||||||
: title = selection.startsWith(_websiteRegex) ? '' : selection,
|
: label = selection.startsWith(_websiteRegex) ? '' : selection,
|
||||||
url = selection.startsWith(_websiteRegex) ? selection : '';
|
url = selection.startsWith(_websiteRegex) ? selection : '';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final titleController = useTextEditingController(text: title);
|
final labelController = useTextEditingController(text: label);
|
||||||
final urlController = useTextEditingController(text: url);
|
final urlController = useTextEditingController(text: url);
|
||||||
|
|
||||||
void submit() {
|
void submit() {
|
||||||
final link = () {
|
final link = () {
|
||||||
if (urlController.text.startsWith('http?s://')) {
|
if (urlController.text.startsWith(RegExp('https?://'))) {
|
||||||
return urlController.text;
|
return urlController.text;
|
||||||
} else {
|
} else {
|
||||||
return 'https://${urlController.text}';
|
return 'https://${urlController.text}';
|
||||||
}
|
}
|
||||||
}();
|
}();
|
||||||
final finalString = '(${titleController.text})[$link]';
|
final finalString = '[${labelController.text}]($link)';
|
||||||
Navigator.of(context).pop(Reformat(
|
Navigator.of(context).pop(_Reformat(
|
||||||
text: finalString,
|
text: finalString,
|
||||||
selectionBeginningShift: finalString.length,
|
selectionBeginningShift: finalString.length,
|
||||||
selectionEndingShift: finalString.length - selection.length,
|
selectionEndingShift: finalString.length - selection.length,
|
||||||
|
@ -423,13 +306,14 @@ class AddLinkDialog extends HookWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
title: const Text('Add link'),
|
title: Text(L10n.of(context).add_link),
|
||||||
content: Column(
|
content: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
TextField(
|
TextField(
|
||||||
controller: titleController,
|
controller: labelController,
|
||||||
decoration: const InputDecoration(hintText: 'title'),
|
decoration: InputDecoration(
|
||||||
|
hintText: L10n.of(context).editor_add_link_label),
|
||||||
textInputAction: TextInputAction.next,
|
textInputAction: TextInputAction.next,
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
),
|
),
|
||||||
|
@ -444,19 +328,129 @@ class AddLinkDialog extends HookWidget {
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
child: const Text('Cancel')),
|
child: Text(L10n.of(context).cancel)),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: submit,
|
onPressed: submit,
|
||||||
child: const Text('Add link'),
|
child: Text(L10n.of(context).add_link),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<Reformat?> show(BuildContext context, String selection) async {
|
static Future<_Reformat?> show(BuildContext context, String selection) async {
|
||||||
return showDialog(
|
return showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => AddLinkDialog(selection),
|
builder: (context) => AddLinkDialog(selection),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
if (text.isEmpty) return '';
|
||||||
|
return text.substring(text.getBeginningOfTheLine(selection.start - 1),
|
||||||
|
text.getEndOfTheLine(selection.end));
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
if (lines.current.startsWith(RegExp.escape(s))) {
|
||||||
|
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) {
|
||||||
|
if (!lines.current.startsWith(RegExp.escape(s))) {
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
|
@ -132,7 +132,7 @@ class WriteComment extends HookWidget {
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
Toolbar.safeArea,
|
EditorToolbar.safeArea,
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
SafeArea(
|
SafeArea(
|
||||||
|
@ -140,7 +140,7 @@ class WriteComment extends HookWidget {
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
Toolbar(
|
EditorToolbar(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
instanceHost: post.instanceHost,
|
instanceHost: post.instanceHost,
|
||||||
),
|
),
|
||||||
|
|
Loading…
Reference in New Issue