editor reorganization + first changes
* added toolbar with buttons * bold button works * added input formatter for user convieniance
This commit is contained in:
parent
85108d8965
commit
ce41b7e18c
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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),
|
||||
// )
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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';
|
||||
|
||||
|
|
Loading…
Reference in New Issue