Compare commits
8 Commits
116b0d7961
...
43fb2a8ceb
Author | SHA1 | Date |
---|---|---|
|
43fb2a8ceb | |
|
4cd8b9855c | |
|
1fcc95d6b9 | |
|
cd1f7a3be3 | |
|
579b4e1d5d | |
|
09f1f54c05 | |
|
462ce5df76 | |
|
63032ebae1 |
|
@ -351,5 +351,69 @@
|
||||||
"no_communities_found": "No communities found",
|
"no_communities_found": "No communities found",
|
||||||
"@no_communities_found": {},
|
"@no_communities_found": {},
|
||||||
"network_error": "Network error",
|
"network_error": "Network error",
|
||||||
"@network_error": {}
|
"@network_error": {},
|
||||||
|
"editor_bold": "bold",
|
||||||
|
"@editor_bold": {
|
||||||
|
"description": "tooltip for button making text bold in markdown editor toolbar"
|
||||||
|
},
|
||||||
|
"editor_italics": "italics",
|
||||||
|
"@editor_italics": {
|
||||||
|
"description": "tooltip for button making text italics in markdown editor toolbar"
|
||||||
|
},
|
||||||
|
"editor_link": "insert link",
|
||||||
|
"@editor_link": {
|
||||||
|
"description": "tooltip for button that inserts link in markdown editor toolbar"
|
||||||
|
},
|
||||||
|
"editor_image": "insert image",
|
||||||
|
"@editor_image": {
|
||||||
|
"description": "tooltip for button that inserts image in markdown editor toolbar"
|
||||||
|
},
|
||||||
|
"editor_user": "link user",
|
||||||
|
"@editor_user": {
|
||||||
|
"description": "tooltip for button that opens a popup to select user to be linked in markdown editor toolbar"
|
||||||
|
},
|
||||||
|
"editor_community": "link community",
|
||||||
|
"@editor_community": {
|
||||||
|
"description": "tooltip for button that opens a popup to select community to be linked in markdown editor toolbar"
|
||||||
|
},
|
||||||
|
"editor_header": "insert header",
|
||||||
|
"@editor_header": {
|
||||||
|
"description": "tooltip for button that inserts header in markdown editor toolbar"
|
||||||
|
},
|
||||||
|
"editor_strikethrough": "strikethrough",
|
||||||
|
"@editor_strikethrough": {
|
||||||
|
"description": "tooltip for button that makes text strikethrough in markdown editor toolbar"
|
||||||
|
},
|
||||||
|
"editor_quote": "quote",
|
||||||
|
"@editor_quote": {
|
||||||
|
"description": "tooltip for button that makes selected text into quote blocks in markdown editor toolbar"
|
||||||
|
},
|
||||||
|
"editor_list": "list",
|
||||||
|
"@editor_list": {
|
||||||
|
"description": "tooltip for button that makes selected text into list in markdown editor toolbar"
|
||||||
|
},
|
||||||
|
"editor_code": "code",
|
||||||
|
"@editor_code": {
|
||||||
|
"description": "tooltip for button that makes text into code in markdown editor toolbar"
|
||||||
|
},
|
||||||
|
"editor_subscript": "subscript",
|
||||||
|
"@editor_subscript": {
|
||||||
|
"description": "tooltip for button that makes text into subscript in markdown editor toolbar"
|
||||||
|
},
|
||||||
|
"editor_superscript": "superscript",
|
||||||
|
"@editor_superscript": {
|
||||||
|
"description": "tooltip for button that makes text into superscript in markdown editor toolbar"
|
||||||
|
},
|
||||||
|
"editor_spoiler": "spoiler",
|
||||||
|
"@editor_spoiler": {
|
||||||
|
"description": "tooltip for button that inserts spoiler in markdown editor toolbar"
|
||||||
|
},
|
||||||
|
"editor_help": "markdown guide",
|
||||||
|
"@editor_help": {
|
||||||
|
"description": "tooltip for button that goes to page containing a guide for markdown"
|
||||||
|
},
|
||||||
|
"insert_text_here_placeholder": "[write text here]",
|
||||||
|
"@insert_text_here_placeholder": {
|
||||||
|
"description": "placeholder for text in markdown editor when inserting stuff like * for italics or ** for bold, etc."
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -152,12 +152,6 @@ class CreatePostPage extends HookWidget {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// Positioned(
|
|
||||||
// bottom: MediaQuery.of(context).viewInsets.bottom,
|
|
||||||
// left: 0,
|
|
||||||
// right: 0,
|
|
||||||
// child: Toolbar(bodyController),
|
|
||||||
// )
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
const lemmurRepositoryUrl = 'https://github.com/LemmurOrg/lemmur';
|
const lemmurRepositoryUrl = 'https://github.com/LemmurOrg/lemmur';
|
||||||
const patreonUrl = 'https://patreon.com/lemmur';
|
const patreonUrl = 'https://patreon.com/lemmur';
|
||||||
const buyMeACoffeeUrl = 'https://buymeacoff.ee/lemmur';
|
const buyMeACoffeeUrl = 'https://buymeacoff.ee/lemmur';
|
||||||
|
const markdownGuide =
|
||||||
|
'https://join-lemmy.org/docs/en/about/guide.html#using-markdown';
|
||||||
|
|
|
@ -0,0 +1,132 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:flutter_typeahead/flutter_typeahead.dart';
|
||||||
|
import 'package:lemmy_api_client/v3.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
import '../../../util/extensions/api.dart';
|
||||||
|
import '../../../widgets/avatar.dart';
|
||||||
|
import '../../stores/accounts_store.dart';
|
||||||
|
import 'editor_toolbar_store.dart';
|
||||||
|
|
||||||
|
class PickPersonDialog extends HookWidget {
|
||||||
|
final EditorToolbarStore store;
|
||||||
|
|
||||||
|
const PickPersonDialog._(this.store);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final userData =
|
||||||
|
context.read<AccountsStore>().defaultUserDataFor(store.instanceHost);
|
||||||
|
|
||||||
|
return AlertDialog(
|
||||||
|
title: const Text('Select User'),
|
||||||
|
content: TypeAheadField<PersonViewSafe>(
|
||||||
|
suggestionsCallback: (pattern) async {
|
||||||
|
if (pattern.trim().isEmpty) return const Iterable.empty();
|
||||||
|
return LemmyApiV3(store.instanceHost)
|
||||||
|
.run(Search(
|
||||||
|
q: pattern,
|
||||||
|
auth: userData?.jwt.raw,
|
||||||
|
type: SearchType.users,
|
||||||
|
limit: 10,
|
||||||
|
))
|
||||||
|
.then((value) => value.users);
|
||||||
|
},
|
||||||
|
itemBuilder: (context, user) {
|
||||||
|
return ListTile(
|
||||||
|
leading: Avatar(
|
||||||
|
url: user.person.avatar,
|
||||||
|
radius: 20,
|
||||||
|
),
|
||||||
|
title: Text(user.person.originPreferredName),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onSuggestionSelected: (suggestion) =>
|
||||||
|
Navigator.of(context).pop(suggestion),
|
||||||
|
loadingBuilder: (context) => Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: const [
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.all(16),
|
||||||
|
child: CircularProgressIndicator.adaptive(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
keepSuggestionsOnLoading: false,
|
||||||
|
noItemsFoundBuilder: (context) => const SizedBox(),
|
||||||
|
hideOnEmpty: true,
|
||||||
|
textFieldConfiguration: const TextFieldConfiguration(autofocus: true),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<PersonViewSafe?> show(BuildContext context) async {
|
||||||
|
final store = context.read<EditorToolbarStore>();
|
||||||
|
return showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => PickPersonDialog._(store),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PickCommunityDialog extends HookWidget {
|
||||||
|
final EditorToolbarStore store;
|
||||||
|
|
||||||
|
const PickCommunityDialog._(this.store);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final userData =
|
||||||
|
context.read<AccountsStore>().defaultUserDataFor(store.instanceHost);
|
||||||
|
|
||||||
|
return AlertDialog(
|
||||||
|
title: const Text('Select Community'),
|
||||||
|
content: TypeAheadField<CommunityView>(
|
||||||
|
suggestionsCallback: (pattern) async {
|
||||||
|
if (pattern.trim().isEmpty) return const Iterable.empty();
|
||||||
|
return LemmyApiV3(store.instanceHost)
|
||||||
|
.run(Search(
|
||||||
|
q: pattern,
|
||||||
|
auth: userData?.jwt.raw,
|
||||||
|
type: SearchType.communities,
|
||||||
|
limit: 10,
|
||||||
|
))
|
||||||
|
.then((value) => value.communities);
|
||||||
|
},
|
||||||
|
itemBuilder: (context, community) {
|
||||||
|
return ListTile(
|
||||||
|
leading: Avatar(
|
||||||
|
url: community.community.icon,
|
||||||
|
radius: 20,
|
||||||
|
),
|
||||||
|
title: Text(community.community.originPreferredName),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onSuggestionSelected: (suggestion) =>
|
||||||
|
Navigator.of(context).pop(suggestion),
|
||||||
|
loadingBuilder: (context) => Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: const [
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.all(16),
|
||||||
|
child: CircularProgressIndicator.adaptive(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
keepSuggestionsOnLoading: false,
|
||||||
|
noItemsFoundBuilder: (context) => const SizedBox(),
|
||||||
|
hideOnEmpty: true,
|
||||||
|
textFieldConfiguration: const TextFieldConfiguration(autofocus: true),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<CommunityView?> show(BuildContext context) async {
|
||||||
|
final store = context.read<EditorToolbarStore>();
|
||||||
|
return showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => PickCommunityDialog._(store),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,14 +1,21 @@
|
||||||
|
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 '../../formatter.dart';
|
||||||
import '../../hooks/logged_in_action.dart';
|
import '../../hooks/logged_in_action.dart';
|
||||||
|
import '../../l10n/l10n.dart';
|
||||||
|
import '../../resources/links.dart';
|
||||||
|
import '../../url_launcher.dart';
|
||||||
import '../../util/async_store_listener.dart';
|
import '../../util/async_store_listener.dart';
|
||||||
|
import '../../util/extensions/api.dart';
|
||||||
import '../../util/extensions/spaced.dart';
|
import '../../util/extensions/spaced.dart';
|
||||||
import '../../util/files.dart';
|
import '../../util/files.dart';
|
||||||
import '../../util/mobx_provider.dart';
|
import '../../util/mobx_provider.dart';
|
||||||
import '../../util/observer_consumers.dart';
|
import '../../util/observer_consumers.dart';
|
||||||
import '../../util/text_lines_iterator.dart';
|
import '../../util/text_lines_iterator.dart';
|
||||||
|
import 'editor_picking_dialog.dart';
|
||||||
import 'editor_toolbar_store.dart';
|
import 'editor_toolbar_store.dart';
|
||||||
|
|
||||||
class Reformat {
|
class Reformat {
|
||||||
|
@ -29,11 +36,13 @@ extension on TextEditingController {
|
||||||
String get afterSelectionText => text.substring(selection.extentOffset);
|
String get afterSelectionText => text.substring(selection.extentOffset);
|
||||||
|
|
||||||
/// surroungs selection with given strings. If nothing is selected, placeholder is used in the middle
|
/// surroungs selection with given strings. If nothing is selected, placeholder is used in the middle
|
||||||
void surround(
|
void surround({
|
||||||
String before, [
|
required String before,
|
||||||
|
required String placeholder,
|
||||||
|
|
||||||
|
/// after = before if null
|
||||||
String? after,
|
String? after,
|
||||||
String placeholder = '[write text here]',
|
}) {
|
||||||
]) {
|
|
||||||
after ??= before;
|
after ??= before;
|
||||||
final beg = text.substring(0, selection.baseOffset);
|
final beg = text.substring(0, selection.baseOffset);
|
||||||
final mid = () {
|
final mid = () {
|
||||||
|
@ -57,6 +66,18 @@ extension on TextEditingController {
|
||||||
text.getEndOfTheLine(selection.end));
|
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) {
|
void removeAtBeginningOfEverySelectedLine(String s) {
|
||||||
final lines = TextLinesIterator.fromController(this);
|
final lines = TextLinesIterator.fromController(this);
|
||||||
var linesCount = 0;
|
var linesCount = 0;
|
||||||
|
@ -113,6 +134,21 @@ extension on TextEditingController {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void reformatSimple(String text) =>
|
||||||
|
reformat((selection) => Reformat(text: text));
|
||||||
|
}
|
||||||
|
|
||||||
|
enum HeaderLevel {
|
||||||
|
h1(1),
|
||||||
|
h2(2),
|
||||||
|
h3(3),
|
||||||
|
h4(4),
|
||||||
|
h5(5),
|
||||||
|
h6(6);
|
||||||
|
|
||||||
|
const HeaderLevel(this.value);
|
||||||
|
final int value;
|
||||||
}
|
}
|
||||||
|
|
||||||
class Toolbar extends HookWidget {
|
class Toolbar extends HookWidget {
|
||||||
|
@ -168,12 +204,18 @@ class _ToolbarBody extends HookWidget {
|
||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () => controller.surround('**'),
|
onPressed: () => controller.surround(
|
||||||
|
before: '**',
|
||||||
|
placeholder: L10n.of(context).insert_text_here_placeholder),
|
||||||
icon: const Icon(Icons.format_bold),
|
icon: const Icon(Icons.format_bold),
|
||||||
|
tooltip: L10n.of(context).editor_bold,
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () => controller.surround('*'),
|
onPressed: () => controller.surround(
|
||||||
|
before: '*',
|
||||||
|
placeholder: L10n.of(context).insert_text_here_placeholder),
|
||||||
icon: const Icon(Icons.format_italic),
|
icon: const Icon(Icons.format_italic),
|
||||||
|
tooltip: L10n.of(context).editor_italics,
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
|
@ -182,6 +224,7 @@ class _ToolbarBody extends HookWidget {
|
||||||
if (r != null) controller.reformat((_) => r);
|
if (r != null) controller.reformat((_) => r);
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.link),
|
icon: const Icon(Icons.link),
|
||||||
|
tooltip: L10n.of(context).editor_link,
|
||||||
),
|
),
|
||||||
// Insert image
|
// Insert image
|
||||||
ObserverBuilder<EditorToolbarStore>(
|
ObserverBuilder<EditorToolbarStore>(
|
||||||
|
@ -202,8 +245,7 @@ class _ToolbarBody extends HookWidget {
|
||||||
.uploadImage(pic.path, token);
|
.uploadImage(pic.path, token);
|
||||||
|
|
||||||
if (picUrl != null) {
|
if (picUrl != null) {
|
||||||
controller.reformat(
|
controller.reformatSimple('![]($picUrl)');
|
||||||
(selection) => Reformat(text: '![]($picUrl)'));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} on Exception catch (_) {
|
} on Exception catch (_) {
|
||||||
|
@ -214,59 +256,134 @@ class _ToolbarBody extends HookWidget {
|
||||||
icon: store.imageUploadState.isLoading
|
icon: store.imageUploadState.isLoading
|
||||||
? const CircularProgressIndicator.adaptive()
|
? const CircularProgressIndicator.adaptive()
|
||||||
: const Icon(Icons.image),
|
: const Icon(Icons.image),
|
||||||
|
tooltip: L10n.of(context).editor_image,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () {},
|
onPressed: () async {
|
||||||
icon: const Icon(Icons.person),
|
final person = await PickPersonDialog.show(context);
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
onPressed: () {},
|
|
||||||
icon: const Icon(Icons.home),
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
onPressed: () {},
|
|
||||||
icon: const Icon(Icons.h_mobiledata),
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
onPressed: () => controller.surround('~~'),
|
|
||||||
icon: const Icon(Icons.format_strikethrough),
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
onPressed: () {},
|
|
||||||
icon: const Icon(Icons.format_quote),
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
onPressed: () {
|
|
||||||
final line = controller.firstSelectedLine;
|
|
||||||
|
|
||||||
if (line.startsWith(RegExp.escape('* '))) {
|
if (person != null) {
|
||||||
controller.removeAtBeginningOfEverySelectedLine('* ');
|
final name =
|
||||||
} else if (line.startsWith('- ')) {
|
'@${person.person.name}@${person.person.originInstanceHost}';
|
||||||
controller.removeAtBeginningOfEverySelectedLine('- ');
|
final link = person.person.actorId;
|
||||||
} else {
|
|
||||||
controller.insertAtBeginningOfEverySelectedLine('- ');
|
controller.reformatSimple('[$name]($link)');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.format_list_bulleted)),
|
icon: const Icon(Icons.person),
|
||||||
|
tooltip: L10n.of(context).editor_user,
|
||||||
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () => controller.surround('`'),
|
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)');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.home),
|
||||||
|
tooltip: L10n.of(context).editor_community,
|
||||||
|
),
|
||||||
|
PopupMenuButton<HeaderLevel>(
|
||||||
|
itemBuilder: (context) => [
|
||||||
|
for (final h in HeaderLevel.values)
|
||||||
|
PopupMenuItem(
|
||||||
|
value: h,
|
||||||
|
child: Text(describeEnum(h).toUpperCase()),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
onSelected: (val) {
|
||||||
|
final header = '${'#' * val.value} ';
|
||||||
|
|
||||||
|
if (!controller.firstSelectedLine.startsWith(header)) {
|
||||||
|
controller.insertAtBeginningOfFirstSelectedLine(header);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tooltip: L10n.of(context).editor_header,
|
||||||
|
child: const Icon(Icons.h_mobiledata),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed: () => controller.surround(
|
||||||
|
before: '~~',
|
||||||
|
placeholder: L10n.of(context).insert_text_here_placeholder,
|
||||||
|
),
|
||||||
|
icon: const Icon(Icons.format_strikethrough),
|
||||||
|
tooltip: L10n.of(context).editor_strikethrough,
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
controller.insertAtBeginningOfEverySelectedLine('> ');
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.format_quote),
|
||||||
|
tooltip: L10n.of(context).editor_quote,
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
final line = controller.firstSelectedLine;
|
||||||
|
|
||||||
|
if (line.startsWith(RegExp.escape('* '))) {
|
||||||
|
controller.removeAtBeginningOfEverySelectedLine('* ');
|
||||||
|
} else if (line.startsWith('- ')) {
|
||||||
|
controller.removeAtBeginningOfEverySelectedLine('- ');
|
||||||
|
} else {
|
||||||
|
controller.insertAtBeginningOfEverySelectedLine('- ');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.format_list_bulleted),
|
||||||
|
tooltip: L10n.of(context).editor_list,
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed: () => controller.surround(
|
||||||
|
before: '`',
|
||||||
|
placeholder: L10n.of(context).insert_text_here_placeholder,
|
||||||
|
),
|
||||||
icon: const Icon(Icons.code),
|
icon: const Icon(Icons.code),
|
||||||
|
tooltip: L10n.of(context).editor_code,
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () => controller.surround('~'),
|
onPressed: () => controller.surround(
|
||||||
|
before: '~',
|
||||||
|
placeholder: L10n.of(context).insert_text_here_placeholder,
|
||||||
|
),
|
||||||
icon: const Icon(Icons.subscript),
|
icon: const Icon(Icons.subscript),
|
||||||
|
tooltip: L10n.of(context).editor_subscript,
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () => controller.surround('^'),
|
onPressed: () => controller.surround(
|
||||||
|
before: '^',
|
||||||
|
placeholder: L10n.of(context).insert_text_here_placeholder,
|
||||||
|
),
|
||||||
icon: const Icon(Icons.superscript),
|
icon: const Icon(Icons.superscript),
|
||||||
|
tooltip: L10n.of(context).editor_superscript,
|
||||||
),
|
),
|
||||||
//spoiler
|
//spoiler
|
||||||
IconButton(onPressed: () {}, icon: const Icon(Icons.warning)),
|
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () {},
|
onPressed: () {
|
||||||
|
controller.reformat((selection) {
|
||||||
|
final insides = selection.isNotEmpty ? selection : '___';
|
||||||
|
Logger.root
|
||||||
|
.info([21, 21 + insides.length, insides, insides.length]);
|
||||||
|
return Reformat(
|
||||||
|
text: '\n::: spoiler spoiler\n$insides\n:::\n',
|
||||||
|
selectionBeginningShift: 21,
|
||||||
|
selectionEndingShift: 21 + insides.length - selection.length,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.warning),
|
||||||
|
tooltip: L10n.of(context).editor_spoiler,
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
launchLink(link: markdownGuide, context: context);
|
||||||
|
},
|
||||||
icon: const Icon(Icons.question_mark),
|
icon: const Icon(Icons.question_mark),
|
||||||
|
tooltip: L10n.of(context).editor_help,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
@ -290,7 +407,14 @@ class AddLinkDialog extends HookWidget {
|
||||||
final urlController = useTextEditingController(text: url);
|
final urlController = useTextEditingController(text: url);
|
||||||
|
|
||||||
void submit() {
|
void submit() {
|
||||||
final finalString = '(${titleController.text})[${urlController.text}]';
|
final link = () {
|
||||||
|
if (urlController.text.startsWith('http?s://')) {
|
||||||
|
return urlController.text;
|
||||||
|
} else {
|
||||||
|
return 'https://${urlController.text}';
|
||||||
|
}
|
||||||
|
}();
|
||||||
|
final finalString = '(${titleController.text})[$link]';
|
||||||
Navigator.of(context).pop(Reformat(
|
Navigator.of(context).pop(Reformat(
|
||||||
text: finalString,
|
text: finalString,
|
||||||
selectionBeginningShift: finalString.length,
|
selectionBeginningShift: finalString.length,
|
||||||
|
|
|
@ -99,37 +99,54 @@ class WriteComment extends HookWidget {
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
body: ListView(
|
body: Stack(
|
||||||
children: [
|
children: [
|
||||||
ConstrainedBox(
|
ListView(
|
||||||
constraints: BoxConstraints(
|
|
||||||
maxHeight: MediaQuery.of(context).size.height * .35),
|
|
||||||
child: SingleChildScrollView(
|
|
||||||
padding: const EdgeInsets.all(8),
|
|
||||||
child: preview,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Divider(),
|
|
||||||
Editor(
|
|
||||||
instanceHost: post.instanceHost,
|
|
||||||
controller: controller,
|
|
||||||
autofocus: true,
|
|
||||||
fancy: showFancy.value,
|
|
||||||
),
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
|
||||||
children: [
|
children: [
|
||||||
TextButton(
|
ConstrainedBox(
|
||||||
onPressed:
|
constraints: BoxConstraints(
|
||||||
delayed.pending ? () {} : loggedInAction(handleSubmit),
|
maxHeight: MediaQuery.of(context).size.height * .35),
|
||||||
child: delayed.loading
|
child: SingleChildScrollView(
|
||||||
? const CircularProgressIndicator.adaptive()
|
padding: const EdgeInsets.all(8),
|
||||||
: Text(_isEdit
|
child: preview,
|
||||||
? L10n.of(context).edit
|
),
|
||||||
: L10n.of(context).post),
|
),
|
||||||
)
|
const Divider(),
|
||||||
|
Editor(
|
||||||
|
instanceHost: post.instanceHost,
|
||||||
|
controller: controller,
|
||||||
|
autofocus: true,
|
||||||
|
fancy: showFancy.value,
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
TextButton(
|
||||||
|
onPressed:
|
||||||
|
delayed.pending ? () {} : loggedInAction(handleSubmit),
|
||||||
|
child: delayed.loading
|
||||||
|
? const CircularProgressIndicator.adaptive()
|
||||||
|
: Text(_isEdit
|
||||||
|
? L10n.of(context).edit
|
||||||
|
: L10n.of(context).post),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Toolbar.safeArea,
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
SafeArea(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
const Spacer(),
|
||||||
|
Toolbar(
|
||||||
|
controller: controller,
|
||||||
|
instanceHost: post.instanceHost,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
Loading…
Reference in New Issue