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/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),
|
||||||
|
// )
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()],
|
||||||
|
),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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/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';
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue