Compare commits

...

8 Commits

Author SHA1 Message Date
Filip Krawczyk 43fb2a8ceb add placeholder text to l10n 2022-08-21 23:49:03 +02:00
Filip Krawczyk 4cd8b9855c add toolbar to comments 2022-08-21 23:09:25 +02:00
Filip Krawczyk 1fcc95d6b9 make sure link is a link 2022-08-21 23:08:00 +02:00
Filip Krawczyk cd1f7a3be3 add tooltips 2022-08-21 22:57:56 +02:00
Filip Krawczyk 579b4e1d5d cleanup 2022-08-21 22:23:25 +02:00
Filip Krawczyk 09f1f54c05 implement more buttons
* header
* quote
2022-08-21 21:52:31 +02:00
Filip Krawczyk 462ce5df76 added functionality to:
* info button
* spoiler button
2022-08-21 20:47:30 +02:00
Filip Krawczyk 63032ebae1 add selecting of users and communities
also made simplified version of reformat
2022-08-21 19:03:22 +02:00
6 changed files with 412 additions and 79 deletions

View File

@ -351,5 +351,69 @@
"no_communities_found": "No communities found",
"@no_communities_found": {},
"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."
}
}

View File

@ -152,12 +152,6 @@ class CreatePostPage extends HookWidget {
],
),
),
// Positioned(
// bottom: MediaQuery.of(context).viewInsets.bottom,
// left: 0,
// right: 0,
// child: Toolbar(bodyController),
// )
],
),
),

View File

@ -1,3 +1,5 @@
const lemmurRepositoryUrl = 'https://github.com/LemmurOrg/lemmur';
const patreonUrl = 'https://patreon.com/lemmur';
const buyMeACoffeeUrl = 'https://buymeacoff.ee/lemmur';
const markdownGuide =
'https://join-lemmy.org/docs/en/about/guide.html#using-markdown';

View File

@ -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),
);
}
}

View File

@ -1,14 +1,21 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:logging/logging.dart';
import '../../formatter.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/extensions/api.dart';
import '../../util/extensions/spaced.dart';
import '../../util/files.dart';
import '../../util/mobx_provider.dart';
import '../../util/observer_consumers.dart';
import '../../util/text_lines_iterator.dart';
import 'editor_picking_dialog.dart';
import 'editor_toolbar_store.dart';
class Reformat {
@ -29,11 +36,13 @@ extension on TextEditingController {
String get afterSelectionText => text.substring(selection.extentOffset);
/// surroungs selection with given strings. If nothing is selected, placeholder is used in the middle
void surround(
String before, [
void surround({
required String before,
required String placeholder,
/// after = before if null
String? after,
String placeholder = '[write text here]',
]) {
}) {
after ??= before;
final beg = text.substring(0, selection.baseOffset);
final mid = () {
@ -57,6 +66,18 @@ extension on TextEditingController {
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;
@ -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 {
@ -168,12 +204,18 @@ class _ToolbarBody extends HookWidget {
return Row(
children: [
IconButton(
onPressed: () => controller.surround('**'),
onPressed: () => controller.surround(
before: '**',
placeholder: L10n.of(context).insert_text_here_placeholder),
icon: const Icon(Icons.format_bold),
tooltip: L10n.of(context).editor_bold,
),
IconButton(
onPressed: () => controller.surround('*'),
onPressed: () => controller.surround(
before: '*',
placeholder: L10n.of(context).insert_text_here_placeholder),
icon: const Icon(Icons.format_italic),
tooltip: L10n.of(context).editor_italics,
),
IconButton(
onPressed: () async {
@ -182,6 +224,7 @@ class _ToolbarBody extends HookWidget {
if (r != null) controller.reformat((_) => r);
},
icon: const Icon(Icons.link),
tooltip: L10n.of(context).editor_link,
),
// Insert image
ObserverBuilder<EditorToolbarStore>(
@ -202,8 +245,7 @@ class _ToolbarBody extends HookWidget {
.uploadImage(pic.path, token);
if (picUrl != null) {
controller.reformat(
(selection) => Reformat(text: '![]($picUrl)'));
controller.reformatSimple('![]($picUrl)');
}
}
} on Exception catch (_) {
@ -214,59 +256,134 @@ class _ToolbarBody extends HookWidget {
icon: store.imageUploadState.isLoading
? const CircularProgressIndicator.adaptive()
: const Icon(Icons.image),
tooltip: L10n.of(context).editor_image,
);
},
),
IconButton(
onPressed: () {},
icon: const Icon(Icons.person),
),
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;
onPressed: () async {
final person = await PickPersonDialog.show(context);
if (line.startsWith(RegExp.escape('* '))) {
controller.removeAtBeginningOfEverySelectedLine('* ');
} else if (line.startsWith('- ')) {
controller.removeAtBeginningOfEverySelectedLine('- ');
} else {
controller.insertAtBeginningOfEverySelectedLine('- ');
}
},
icon: const Icon(Icons.format_list_bulleted)),
if (person != null) {
final name =
'@${person.person.name}@${person.person.originInstanceHost}';
final link = person.person.actorId;
controller.reformatSimple('[$name]($link)');
}
},
icon: const Icon(Icons.person),
tooltip: L10n.of(context).editor_user,
),
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),
tooltip: L10n.of(context).editor_code,
),
IconButton(
onPressed: () => controller.surround('~'),
onPressed: () => controller.surround(
before: '~',
placeholder: L10n.of(context).insert_text_here_placeholder,
),
icon: const Icon(Icons.subscript),
tooltip: L10n.of(context).editor_subscript,
),
IconButton(
onPressed: () => controller.surround('^'),
onPressed: () => controller.surround(
before: '^',
placeholder: L10n.of(context).insert_text_here_placeholder,
),
icon: const Icon(Icons.superscript),
tooltip: L10n.of(context).editor_superscript,
),
//spoiler
IconButton(onPressed: () {}, icon: const Icon(Icons.warning)),
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),
tooltip: L10n.of(context).editor_help,
),
],
);
@ -290,7 +407,14 @@ class AddLinkDialog extends HookWidget {
final urlController = useTextEditingController(text: url);
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(
text: finalString,
selectionBeginningShift: finalString.length,

View File

@ -99,37 +99,54 @@ class WriteComment extends HookWidget {
),
],
),
body: ListView(
body: Stack(
children: [
ConstrainedBox(
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,
ListView(
children: [
TextButton(
onPressed:
delayed.pending ? () {} : loggedInAction(handleSubmit),
child: delayed.loading
? const CircularProgressIndicator.adaptive()
: Text(_isEdit
? L10n.of(context).edit
: L10n.of(context).post),
)
ConstrainedBox(
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: [
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,
),
],
),
),
],
),
);