From ce41b7e18cf8c3b664a90a67dbd45affd5fc5e0f Mon Sep 17 00:00:00 2001 From: Filip Krawczyk Date: Sun, 26 Jun 2022 23:43:36 +0200 Subject: [PATCH] editor reorganization + first changes * added toolbar with buttons * bold button works * added input formatter for user convieniance --- lib/formatter.dart | 54 +++++++++++ lib/pages/create_post/create_post.dart | 119 +++++++++++++++---------- lib/pages/manage_account.dart | 2 +- lib/widgets/{ => editor}/editor.dart | 32 ++++--- lib/widgets/editor/toolbar.dart | 80 +++++++++++++++++ lib/widgets/write_comment.dart | 2 +- 6 files changed, 227 insertions(+), 62 deletions(-) create mode 100644 lib/formatter.dart rename lib/widgets/{ => editor}/editor.dart (67%) create mode 100644 lib/widgets/editor/toolbar.dart diff --git a/lib/formatter.dart b/lib/formatter.dart new file mode 100644 index 0000000..70bafad --- /dev/null +++ b/lib/formatter.dart @@ -0,0 +1,54 @@ +import 'package:flutter/services.dart'; + +extension on String { + int getBeginningOfPreviousLine(int startingIndex) { + for (var i = startingIndex; i >= 0; i--) { + if (this[i] == '\n') return i + 1; + } + return 0; + } + + // returns the line that ends at endingIndex + String lineBefore(int endingIndex) { + return substring(getBeginningOfPreviousLine(endingIndex), endingIndex + 1); + } +} + +extension on TextEditingValue { + /// Append a string after the cursor + TextEditingValue append(String s) { + final beg = text.substring(0, selection.baseOffset); + final end = text.substring(selection.baseOffset); + + return copyWith( + text: '$beg$s$end', + selection: selection.copyWith( + baseOffset: selection.baseOffset + s.length, + extentOffset: selection.extentOffset + s.length, + ), + ); + } +} + +/// Provides convinience formatting in markdown text fields +class MarkdownFormatter extends TextInputFormatter { + @override + TextEditingValue formatEditUpdate( + TextEditingValue oldValue, TextEditingValue newValue) { + if (oldValue.text.length > newValue.text.length) return newValue; + + final char = newValue.text[newValue.selection.baseOffset - 1]; + if (char == '\n') { + final lineBefore = + newValue.text.lineBefore(newValue.selection.baseOffset - 2); + if (lineBefore.startsWith('- ')) { + return newValue.append('- '); + } + if (lineBefore.startsWith('* ')) { + return newValue.append('* '); + } + } + + return newValue; + } +} diff --git a/lib/pages/create_post/create_post.dart b/lib/pages/create_post/create_post.dart index eb016ec..c215b69 100644 --- a/lib/pages/create_post/create_post.dart +++ b/lib/pages/create_post/create_post.dart @@ -11,7 +11,7 @@ import '../../util/async_store_listener.dart'; import '../../util/extensions/spaced.dart'; import '../../util/mobx_provider.dart'; import '../../util/observer_consumers.dart'; -import '../../widgets/editor.dart'; +import '../../widgets/editor/editor.dart'; import '../../widgets/markdown_mode_icon.dart'; import 'create_post_community_picker.dart'; import 'create_post_instance_picker.dart'; @@ -55,9 +55,12 @@ class CreatePostPage extends HookWidget { ), ); + final bodyController = + useTextEditingController(text: context.read().body); + final body = ObserverBuilder( builder: (context, store) => Editor( - initialValue: store.body, + controller: bodyController, focusNode: bodyFocusNode, onChanged: (body) => store.body = body, labelText: L10n.of(context).body, @@ -82,59 +85,79 @@ class CreatePostPage extends HookWidget { ), ], ), - body: SafeArea( - child: SingleChildScrollView( - padding: const EdgeInsets.all(5), - child: Form( - key: formKey, - child: Column( - children: [ - if (!context.read().isEdit) ...const [ - CreatePostInstancePicker(), - CreatePostCommunityPicker(), - ], - CreatePostUrlField(titleFocusNode), - title, - body, - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + body: Stack( + children: [ + SafeArea( + child: SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + padding: const EdgeInsets.all(5), + child: Form( + key: formKey, + child: Column( children: [ - ObserverBuilder( - builder: (context, store) => GestureDetector( - onTap: () => store.nsfw = !store.nsfw, - child: Row( - children: [ - Checkbox( - value: store.nsfw, - onChanged: (val) { - if (val != null) store.nsfw = val; - }, + if (!context.read().isEdit) ...const [ + CreatePostInstancePicker(), + CreatePostCommunityPicker(), + ], + CreatePostUrlField(titleFocusNode), + title, + body, + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + ObserverBuilder( + builder: (context, store) => GestureDetector( + onTap: () => store.nsfw = !store.nsfw, + child: Row( + children: [ + Checkbox( + value: store.nsfw, + onChanged: (val) { + if (val != null) store.nsfw = val; + }, + ), + Text(L10n.of(context).nsfw) + ], ), - Text(L10n.of(context).nsfw) - ], + ), ), - ), + ObserverBuilder( + builder: (context, store) => TextButton( + onPressed: store.submitState.isLoading + ? () {} + : loggedInAction(handleSubmit), + child: store.submitState.isLoading + ? const CircularProgressIndicator.adaptive() + : Text( + store.isEdit + ? L10n.of(context).edit + : L10n.of(context).post, + ), + ), + ) + ], ), - ObserverBuilder( - builder: (context, store) => TextButton( - onPressed: store.submitState.isLoading - ? () {} - : loggedInAction(handleSubmit), - child: store.submitState.isLoading - ? const CircularProgressIndicator.adaptive() - : Text( - store.isEdit - ? L10n.of(context).edit - : L10n.of(context).post, - ), - ), - ) - ], + ].spaced(6), ), - ].spaced(6), + ), ), ), - ), + SafeArea( + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Spacer(), + Toolbar(bodyController), + ], + ), + ), + // Positioned( + // bottom: MediaQuery.of(context).viewInsets.bottom, + // left: 0, + // right: 0, + // child: Toolbar(bodyController), + // ) + ], ), ), ); diff --git a/lib/pages/manage_account.dart b/lib/pages/manage_account.dart index e9a5121..a3408f1 100644 --- a/lib/pages/manage_account.dart +++ b/lib/pages/manage_account.dart @@ -13,7 +13,7 @@ import '../util/pictrs.dart'; import '../widgets/bottom_modal.dart'; import '../widgets/bottom_safe.dart'; import '../widgets/cached_network_image.dart'; -import '../widgets/editor.dart'; +import '../widgets/editor/editor.dart'; /// Page for managing things like username, email, avatar etc /// This page will assume the manage account is logged in and diff --git a/lib/widgets/editor.dart b/lib/widgets/editor/editor.dart similarity index 67% rename from lib/widgets/editor.dart rename to lib/widgets/editor/editor.dart index 145ab94..3eaad4a 100644 --- a/lib/widgets/editor.dart +++ b/lib/widgets/editor/editor.dart @@ -1,7 +1,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; -import 'markdown_text.dart'; +import '../../formatter.dart'; +import '../markdown_text.dart'; + +export 'toolbar.dart'; /// A text field with added functionality for ease of editing class Editor extends HookWidget { @@ -49,17 +52,22 @@ class Editor extends HookWidget { ); } - return TextField( - controller: actualController, - focusNode: focusNode, - autofocus: autofocus, - keyboardType: TextInputType.multiline, - textCapitalization: TextCapitalization.sentences, - onChanged: onChanged, - onSubmitted: onSubmitted, - maxLines: maxLines, - minLines: minLines, - decoration: InputDecoration(labelText: labelText), + return Stack( + children: [ + TextField( + controller: actualController, + focusNode: focusNode, + autofocus: autofocus, + keyboardType: TextInputType.multiline, + textCapitalization: TextCapitalization.sentences, + onChanged: onChanged, + onSubmitted: onSubmitted, + maxLines: maxLines, + minLines: minLines, + decoration: InputDecoration(labelText: labelText), + inputFormatters: [MarkdownFormatter()], + ), + ], ); } } diff --git a/lib/widgets/editor/toolbar.dart b/lib/widgets/editor/toolbar.dart new file mode 100644 index 0000000..7788124 --- /dev/null +++ b/lib/widgets/editor/toolbar.dart @@ -0,0 +1,80 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; + +extension on TextEditingController { + /// surroungs selection with given strings. If nothing is selected, placeholder is used in the middle + void surround(String before, String after, + [String placeholder = '[write text here]']) { + final beg = text.substring(0, selection.baseOffset); + final mid = text.substring(selection.baseOffset, selection.extentOffset); + final end = text.substring(selection.extentOffset); + + if (mid.isEmpty) { + value = value.copyWith( + text: '$beg$before$placeholder$after$end', + selection: selection.copyWith( + baseOffset: selection.baseOffset + before.length, + extentOffset: + selection.baseOffset + before.length + placeholder.length, + ), + ); + } else { + value = value.copyWith( + text: '$beg$before$mid$after$end', + selection: selection.copyWith( + baseOffset: selection.baseOffset + before.length, + extentOffset: selection.extentOffset + before.length, + )); + } + } +} + +class Toolbar extends HookWidget { + final TextEditingController controller; + + const Toolbar(this.controller); + + @override + Widget build(BuildContext context) { + return Container( + height: 50, + width: double.infinity, + color: Theme.of(context).cardColor, + child: Material( + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: [ + IconButton( + onPressed: () { + controller.surround('**', '**'); + }, + icon: const Icon(Icons.format_bold)), + IconButton( + onPressed: () {}, icon: const Icon(Icons.format_italic)), + IconButton(onPressed: () {}, icon: const Icon(Icons.link)), + IconButton(onPressed: () {}, icon: const Icon(Icons.image)), + IconButton( + onPressed: () {}, icon: const Icon(Icons.h_mobiledata)), + IconButton( + onPressed: () {}, + icon: const Icon(Icons.format_strikethrough)), + IconButton( + onPressed: () {}, icon: const Icon(Icons.format_quote)), + IconButton( + onPressed: () {}, + icon: const Icon(Icons.format_list_bulleted)), + IconButton(onPressed: () {}, icon: const Icon(Icons.code)), + IconButton(onPressed: () {}, icon: const Icon(Icons.subscript)), + IconButton(onPressed: () {}, icon: const Icon(Icons.superscript)), + // IconButton(onPressed: () {}, icon: const Icon(Icons.warning)),//spoiler + IconButton(onPressed: () {}, icon: const Icon(Icons.superscript)), + IconButton( + onPressed: () {}, icon: const Icon(Icons.question_mark)), + ], + ), + ), + ), + ); + } +} diff --git a/lib/widgets/write_comment.dart b/lib/widgets/write_comment.dart index f7f7a5e..4870975 100644 --- a/lib/widgets/write_comment.dart +++ b/lib/widgets/write_comment.dart @@ -5,7 +5,7 @@ import 'package:lemmy_api_client/v3.dart'; import '../hooks/delayed_loading.dart'; import '../hooks/logged_in_action.dart'; import '../l10n/l10n.dart'; -import 'editor.dart'; +import 'editor/editor.dart'; import 'markdown_mode_icon.dart'; import 'markdown_text.dart';