editor reorganization + first changes

* added toolbar with buttons
* bold button works
* added input formatter for user convieniance
This commit is contained in:
Filip Krawczyk 2022-06-26 23:43:36 +02:00
parent 85108d8965
commit ce41b7e18c
6 changed files with 227 additions and 62 deletions

54
lib/formatter.dart Normal file
View File

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

View File

@ -11,7 +11,7 @@ import '../../util/async_store_listener.dart';
import '../../util/extensions/spaced.dart'; import '../../util/extensions/spaced.dart';
import '../../util/mobx_provider.dart'; import '../../util/mobx_provider.dart';
import '../../util/observer_consumers.dart'; import '../../util/observer_consumers.dart';
import '../../widgets/editor.dart'; import '../../widgets/editor/editor.dart';
import '../../widgets/markdown_mode_icon.dart'; import '../../widgets/markdown_mode_icon.dart';
import 'create_post_community_picker.dart'; import 'create_post_community_picker.dart';
import 'create_post_instance_picker.dart'; import 'create_post_instance_picker.dart';
@ -55,9 +55,12 @@ class CreatePostPage extends HookWidget {
), ),
); );
final bodyController =
useTextEditingController(text: context.read<CreatePostStore>().body);
final body = ObserverBuilder<CreatePostStore>( final body = ObserverBuilder<CreatePostStore>(
builder: (context, store) => Editor( builder: (context, store) => Editor(
initialValue: store.body, controller: bodyController,
focusNode: bodyFocusNode, focusNode: bodyFocusNode,
onChanged: (body) => store.body = body, onChanged: (body) => store.body = body,
labelText: L10n.of(context).body, labelText: L10n.of(context).body,
@ -82,59 +85,79 @@ class CreatePostPage extends HookWidget {
), ),
], ],
), ),
body: SafeArea( body: Stack(
child: SingleChildScrollView( children: [
padding: const EdgeInsets.all(5), SafeArea(
child: Form( child: SingleChildScrollView(
key: formKey, physics: const AlwaysScrollableScrollPhysics(),
child: Column( padding: const EdgeInsets.all(5),
children: [ child: Form(
if (!context.read<CreatePostStore>().isEdit) ...const [ key: formKey,
CreatePostInstancePicker(), child: Column(
CreatePostCommunityPicker(),
],
CreatePostUrlField(titleFocusNode),
title,
body,
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
ObserverBuilder<CreatePostStore>( if (!context.read<CreatePostStore>().isEdit) ...const [
builder: (context, store) => GestureDetector( CreatePostInstancePicker(),
onTap: () => store.nsfw = !store.nsfw, CreatePostCommunityPicker(),
child: Row( ],
children: [ CreatePostUrlField(titleFocusNode),
Checkbox( title,
value: store.nsfw, body,
onChanged: (val) { Row(
if (val != null) store.nsfw = val; mainAxisAlignment: MainAxisAlignment.spaceBetween,
}, children: [
ObserverBuilder<CreatePostStore>(
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<CreatePostStore>(
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<CreatePostStore>( ].spaced(6),
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), ),
), ),
), ),
), 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),
// )
],
), ),
), ),
); );

View File

@ -13,7 +13,7 @@ import '../util/pictrs.dart';
import '../widgets/bottom_modal.dart'; import '../widgets/bottom_modal.dart';
import '../widgets/bottom_safe.dart'; import '../widgets/bottom_safe.dart';
import '../widgets/cached_network_image.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 /// Page for managing things like username, email, avatar etc
/// This page will assume the manage account is logged in and /// This page will assume the manage account is logged in and

View File

@ -1,7 +1,10 @@
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 'markdown_text.dart'; import '../../formatter.dart';
import '../markdown_text.dart';
export 'toolbar.dart';
/// A text field with added functionality for ease of editing /// A text field with added functionality for ease of editing
class Editor extends HookWidget { class Editor extends HookWidget {
@ -49,17 +52,22 @@ class Editor extends HookWidget {
); );
} }
return TextField( return Stack(
controller: actualController, children: [
focusNode: focusNode, TextField(
autofocus: autofocus, controller: actualController,
keyboardType: TextInputType.multiline, focusNode: focusNode,
textCapitalization: TextCapitalization.sentences, autofocus: autofocus,
onChanged: onChanged, keyboardType: TextInputType.multiline,
onSubmitted: onSubmitted, textCapitalization: TextCapitalization.sentences,
maxLines: maxLines, onChanged: onChanged,
minLines: minLines, onSubmitted: onSubmitted,
decoration: InputDecoration(labelText: labelText), maxLines: maxLines,
minLines: minLines,
decoration: InputDecoration(labelText: labelText),
inputFormatters: [MarkdownFormatter()],
),
],
); );
} }
} }

View File

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

View File

@ -5,7 +5,7 @@ import 'package:lemmy_api_client/v3.dart';
import '../hooks/delayed_loading.dart'; import '../hooks/delayed_loading.dart';
import '../hooks/logged_in_action.dart'; import '../hooks/logged_in_action.dart';
import '../l10n/l10n.dart'; import '../l10n/l10n.dart';
import 'editor.dart'; import 'editor/editor.dart';
import 'markdown_mode_icon.dart'; import 'markdown_mode_icon.dart';
import 'markdown_text.dart'; import 'markdown_text.dart';