Compare commits

...

10 Commits

Author SHA1 Message Date
Filip Krawczyk 8aefdfbf27 remove list item if empty + enter pressed 2022-08-25 18:46:32 +02:00
Filip Krawczyk b2ef0883e3 remove store from being a property. also better animation 2022-08-25 18:22:14 +02:00
Filip Krawczyk e3b561835b remove magic number in favor of calculating it on the fly 2022-08-25 17:50:06 +02:00
Filip Krawczyk 9ec5410273 remove unneed FocusScope & Focus 2022-08-25 17:45:22 +02:00
Filip Krawczyk d11a46393b remove unneeded material 2022-08-25 17:42:31 +02:00
Filip Krawczyk c83e93c755 add transition for toolbar appearing and disappearing 2022-08-25 17:40:37 +02:00
Filip Krawczyk 2cc82e6a45 fix 2022-08-25 17:30:02 +02:00
Filip Krawczyk 90553794b2
Update lib/widgets/editor/editor_toolbar.dart
Co-authored-by: Marcin Wojnarowski <xmarcinmarcin@gmail.com>
2022-08-25 17:29:28 +02:00
Filip Krawczyk b39d6b06d7 create widget for stuff that sticks to bottom 2022-08-25 17:27:40 +02:00
Filip Krawczyk 6729a040ea make list completion more reusable 2022-08-25 17:24:05 +02:00
6 changed files with 145 additions and 100 deletions

View File

@ -1,5 +1,8 @@
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
const unorderedListTypes = ['*', '+', '-'];
const orderedListTypes = [')', '.'];
extension Utilities on String { extension Utilities on String {
int getBeginningOfTheLine(int from) { int getBeginningOfTheLine(int from) {
if (from <= 0) return 0; if (from <= 0) return 0;
@ -37,6 +40,19 @@ extension on TextEditingValue {
), ),
); );
} }
/// cuts [characterCount] number of chars from before the cursor
TextEditingValue trimBeforeCursor(int characterCount) {
final beg = text.substring(0, selection.baseOffset);
final end = text.substring(selection.baseOffset);
return copyWith(
text: beg.substring(0, beg.length - characterCount - 1) + end,
selection: selection.copyWith(
baseOffset: selection.baseOffset - characterCount,
extentOffset: selection.extentOffset - characterCount,
));
}
} }
/// Provides convenience formatting in markdown text fields /// Provides convenience formatting in markdown text fields
@ -56,35 +72,52 @@ class MarkdownFormatter extends TextInputFormatter {
TextEditingValue unorderedListContinuation( TextEditingValue unorderedListContinuation(
String listChar, TextEditingValue tev) { String listChar, TextEditingValue tev) {
final regex = RegExp(r'(\s*)' '${RegExp.escape(listChar)} '); final regex = RegExp(r'(\s*)' '${RegExp.escape(listChar)} (.*)');
final match = regex.matchAsPrefix(lineBefore); final match = regex.matchAsPrefix(lineBefore);
if (match == null) { if (match == null) {
return tev; return tev;
} }
final listItemBody = match.group(2);
final indent = match.group(1); final indent = match.group(1);
if (listItemBody == null || listItemBody.isEmpty) {
return tev.trimBeforeCursor(listChar.length + (indent?.length ?? 1));
}
return tev.append('$indent$listChar '); return tev.append('$indent$listChar ');
} }
TextEditingValue orderedListContinuation( TextEditingValue orderedListContinuation(
String afterNumberChar, TextEditingValue tev) { String afterNumberChar, TextEditingValue tev) {
final regex = final regex =
RegExp(r'(\s*)(\d+)' '${RegExp.escape(afterNumberChar)} '); RegExp(r'(\s*)(\d+)' '${RegExp.escape(afterNumberChar)} (.*)');
final match = regex.matchAsPrefix(lineBefore); final match = regex.matchAsPrefix(lineBefore);
if (match == null) { if (match == null) {
return tev; return tev;
} }
final indent = match.group(1);
final number = int.tryParse(match.group(2)!) ?? 0 + 1;
final listItemBody = match.group(3)!;
final indent = match.group(1)!;
final numberStr = match.group(2)!;
if (listItemBody.isEmpty) {
return tev.trimBeforeCursor(
indent.length + numberStr.length + afterNumberChar.length + 1);
}
final number = (int.tryParse(match.group(2)!) ?? 0) + 1;
return tev.append('$indent$number$afterNumberChar '); return tev.append('$indent$number$afterNumberChar ');
} }
newVal = unorderedListContinuation('-', newVal); for (final c in unorderedListTypes) {
newVal = unorderedListContinuation('*', newVal); newVal = unorderedListContinuation(c, newVal);
newVal = unorderedListContinuation('+', newVal); }
newVal = orderedListContinuation('.', newVal); for (final c in orderedListTypes) {
newVal = orderedListContinuation(')', newVal); newVal = orderedListContinuation(c, newVal);
}
} }
return newVal; return newVal;

View File

@ -142,17 +142,11 @@ class CreatePostPage extends HookWidget {
), ),
), ),
), ),
SafeArea( BottomSticky(
child: Column( child: EditorToolbar(
mainAxisAlignment: MainAxisAlignment.spaceBetween, editorFocusNode: editorFocusNode,
children: [ controller: bodyController,
const Spacer(), instanceHost: context.read<CreatePostStore>().instanceHost,
EditorToolbar(
editorFocusNode: editorFocusNode,
controller: bodyController,
instanceHost: context.read<CreatePostStore>().instanceHost,
),
],
), ),
), ),
], ],

View File

@ -384,19 +384,13 @@ class _ManageAccount extends HookWidget {
const BottomSafe(), const BottomSafe(),
], ],
), ),
SafeArea( BottomSticky(
child: Column( child: EditorToolbar(
mainAxisAlignment: MainAxisAlignment.spaceBetween, editorFocusNode: bioFocusNode,
children: [ controller: bioController,
const Spacer(), instanceHost: user.instanceHost,
EditorToolbar(
editorFocusNode: bioFocusNode,
controller: bioController,
instanceHost: user.instanceHost,
),
],
), ),
), )
], ],
); );
} }

View File

@ -52,22 +52,18 @@ class Editor extends HookWidget {
); );
} }
return FocusScope( return TextField(
child: Focus( focusNode: focusNode,
focusNode: focusNode, controller: actualController,
child: TextField( autofocus: autofocus,
controller: actualController, keyboardType: TextInputType.multiline,
autofocus: autofocus, textCapitalization: TextCapitalization.sentences,
keyboardType: TextInputType.multiline, onChanged: onChanged,
textCapitalization: TextCapitalization.sentences, onSubmitted: onSubmitted,
onChanged: onChanged, maxLines: maxLines,
onSubmitted: onSubmitted, minLines: minLines,
maxLines: maxLines, decoration: InputDecoration(labelText: labelText),
minLines: minLines, inputFormatters: [MarkdownFormatter()],
decoration: InputDecoration(labelText: labelText),
inputFormatters: [MarkdownFormatter()],
),
),
); );
} }
} }

View File

@ -42,52 +42,73 @@ enum HeaderLevel {
class EditorToolbar extends HookWidget { class EditorToolbar extends HookWidget {
final TextEditingController controller; final TextEditingController controller;
final String instanceHost; final String instanceHost;
final EditorToolbarStore store;
final FocusNode editorFocusNode; final FocusNode editorFocusNode;
static const _height = 50.0; static const _height = 50.0;
EditorToolbar({ const EditorToolbar({
required this.controller, required this.controller,
required this.instanceHost, required this.instanceHost,
required this.editorFocusNode, required this.editorFocusNode,
}) : store = EditorToolbarStore(instanceHost); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final visible = useState(editorFocusNode.hasFocus); final visible = useListenable(editorFocusNode).hasFocus;
useEffect(() {
editorFocusNode.addListener(() {
visible.value = editorFocusNode.hasFocus;
});
return;
}, [editorFocusNode]);
return MobxProvider( return MobxProvider(
create: (context) => store, create: (context) => EditorToolbarStore(instanceHost),
child: AsyncStoreListener( child: ObserverBuilder<EditorToolbarStore>(builder: (context, store) {
asyncStore: store.imageUploadState, return AsyncStoreListener(
child: visible.value asyncStore: store.imageUploadState,
? Container( child: AnimatedSwitcher(
height: _height, duration: kThemeAnimationDuration,
width: double.infinity, transitionBuilder: (child, animation) {
color: Theme.of(context).cardColor, final offsetAnimation =
child: Material( Tween<Offset>(begin: const Offset(0, 1.5), end: Offset.zero)
child: SingleChildScrollView( .animate(animation);
scrollDirection: Axis.horizontal,
child: _ToolbarBody( return SlideTransition(
controller: controller, position: offsetAnimation,
instanceHost: instanceHost, child: child,
);
},
child: visible
? Container(
height: _height,
width: double.infinity,
color: Theme.of(context).canvasColor,
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: _ToolbarBody(
controller: controller,
instanceHost: instanceHost,
),
), ),
), )
), : const SizedBox.shrink(),
) ),
: const SizedBox.shrink(), );
), }),
); );
} }
static Widget safeArea = const SizedBox(height: _height); static const safeArea = SizedBox(height: _height);
}
class BottomSticky extends StatelessWidget {
final Widget child;
const BottomSticky({required this.child});
@override
Widget build(BuildContext context) => SafeArea(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Spacer(),
child,
],
),
);
} }
class _ToolbarBody extends HookWidget { class _ToolbarBody extends HookWidget {
@ -224,12 +245,21 @@ class _ToolbarBody extends HookWidget {
onPressed: () { onPressed: () {
final line = controller.firstSelectedLine; final line = controller.firstSelectedLine;
if (line.startsWith('* ')) { // if theres a list in place, remove it
controller.removeAtBeginningOfEverySelectedLine('* '); final listRemoved = () {
} else if (line.startsWith('- ')) { for (final c in unorderedListTypes) {
controller.removeAtBeginningOfEverySelectedLine('- '); if (line.startsWith('$c ')) {
} else { controller.removeAtBeginningOfEverySelectedLine('$c ');
controller.insertAtBeginningOfEverySelectedLine('- '); return true;
}
}
return false;
}();
// if no list, then let's add one
if (!listRemoved) {
controller.insertAtBeginningOfEverySelectedLine(
'${unorderedListTypes.last} ');
} }
}, },
icon: const Icon(Icons.format_list_bulleted), icon: const Icon(Icons.format_list_bulleted),
@ -263,11 +293,15 @@ class _ToolbarBody extends HookWidget {
IconButton( IconButton(
onPressed: () { onPressed: () {
controller.reformat((selection) { controller.reformat((selection) {
final insides = selection.isNotEmpty ? selection : '___'; const textBeg = '\n::: spoiler spoiler\n';
final textMid = selection.isNotEmpty ? selection : '___';
const textEnd = '\n:::\n';
return _Reformat( return _Reformat(
text: '\n::: spoiler spoiler\n$insides\n:::\n', text: textBeg + textMid + textEnd,
selectionBeginningShift: 21, selectionBeginningShift: textBeg.length,
selectionEndingShift: 21 + insides.length - selection.length, selectionEndingShift:
textBeg.length + textMid.length - selection.length,
); );
}); });
}, },

View File

@ -137,17 +137,11 @@ class WriteComment extends HookWidget {
EditorToolbar.safeArea, EditorToolbar.safeArea,
], ],
), ),
SafeArea( BottomSticky(
child: Column( child: EditorToolbar(
mainAxisAlignment: MainAxisAlignment.spaceBetween, editorFocusNode: editorFocusNode,
children: [ controller: controller,
const Spacer(), instanceHost: post.instanceHost,
EditorToolbar(
editorFocusNode: editorFocusNode,
controller: controller,
instanceHost: post.instanceHost,
),
],
), ),
), ),
], ],