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/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<CreatePostStore>().body);
final body = ObserverBuilder<CreatePostStore>(
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<CreatePostStore>().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<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;
},
if (!context.read<CreatePostStore>().isEdit) ...const [
CreatePostInstancePicker(),
CreatePostCommunityPicker(),
],
CreatePostUrlField(titleFocusNode),
title,
body,
Row(
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>(
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),
// )
],
),
),
);

View File

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

View File

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

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/logged_in_action.dart';
import '../l10n/l10n.dart';
import 'editor.dart';
import 'editor/editor.dart';
import 'markdown_mode_icon.dart';
import 'markdown_text.dart';