From 11f7c0d7f6abaa5bb1cfd8b524961a23b90e1b71 Mon Sep 17 00:00:00 2001 From: dasmikko Date: Fri, 2 Aug 2019 07:59:52 +0200 Subject: [PATCH 01/19] Added app shortcuts --- lib/screens/home.dart | 26 ++++++++++++++++++++++++++ pubspec.yaml | 1 + 2 files changed, 27 insertions(+) diff --git a/lib/screens/home.dart b/lib/screens/home.dart index 63c7eb2..db0e196 100644 --- a/lib/screens/home.dart +++ b/lib/screens/home.dart @@ -2,9 +2,12 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:hive/hive.dart'; +import 'package:knocky/helpers/hiveHelper.dart'; import 'package:knocky/models/subforum.dart'; import 'package:after_layout/after_layout.dart'; import 'package:knocky/widget/Drawer.dart'; +import 'package:quick_actions/quick_actions.dart'; import 'package:scoped_model/scoped_model.dart'; import 'package:knocky/state/authentication.dart'; import 'package:knocky/state/subscriptions.dart'; @@ -33,6 +36,29 @@ class _HomeScreenState extends State void initState() { super.initState(); + + final QuickActions quickActions = new QuickActions(); + + quickActions.setShortcutItems([ + const ShortcutItem(type: 'action_subscriptions', localizedTitle: 'Subscriptions', icon: 'icon_help'), + const ShortcutItem(type: 'action_popular', localizedTitle: 'Popular threads', icon: 'icon_help'), + const ShortcutItem(type: 'action_latest', localizedTitle: 'Latest threads', icon: 'icon_help') + ]); + + quickActions.initialize((shortcutType) { + if (shortcutType == 'action_subscriptions') { + AppHiveBox.getBox().then((Box box) { + box.get('isLoggedIn', defaultValue: false).then((loginState) { + if (loginState) { + ScopedModel.of(context).setCurrentTab(1); + } + }); + }); + } + if (shortcutType == 'action_popular') ScopedModel.of(context).setCurrentTab(3); + if (shortcutType == 'action_latest') ScopedModel.of(context).setCurrentTab(2); + // More handling code... + }); } @override diff --git a/pubspec.yaml b/pubspec.yaml index 9241b18..758c621 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -47,6 +47,7 @@ dependencies: git: url: https://github.com/leisim/hive.git path: hive + quick_actions: ^0.3.2+2 flutter: sdk: flutter From f6f4c4fd0b60367f7dc2c82d51b749fc13cc31c1 Mon Sep 17 00:00:00 2001 From: dasmikko Date: Fri, 2 Aug 2019 12:11:52 +0200 Subject: [PATCH 02/19] Simple deeplink for threads --- android/app/src/main/AndroidManifest.xml | 11 ++++ lib/screens/home.dart | 77 ++++++++++++++++++++++-- lib/screens/thread.dart | 8 ++- pubspec.yaml | 1 + 4 files changed, 90 insertions(+), 7 deletions(-) diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 8252402..3ec410c 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -32,6 +32,17 @@ + + + + + + + + + diff --git a/lib/screens/home.dart b/lib/screens/home.dart index db0e196..08a5a2e 100644 --- a/lib/screens/home.dart +++ b/lib/screens/home.dart @@ -6,6 +6,7 @@ import 'package:hive/hive.dart'; import 'package:knocky/helpers/hiveHelper.dart'; import 'package:knocky/models/subforum.dart'; import 'package:after_layout/after_layout.dart'; +import 'package:knocky/screens/thread.dart'; import 'package:knocky/widget/Drawer.dart'; import 'package:quick_actions/quick_actions.dart'; import 'package:scoped_model/scoped_model.dart'; @@ -15,6 +16,12 @@ import 'package:knocky/state/appState.dart'; import 'package:knocky/widget/tab-navigator.dart'; import 'package:knocky/events.dart'; +import 'dart:async'; +import 'dart:io'; + +import 'package:uni_links/uni_links.dart'; +import 'package:flutter/services.dart' show PlatformException; + class HomeScreen extends StatefulWidget { @override _HomeScreenState createState() => _HomeScreenState(); @@ -33,16 +40,28 @@ class _HomeScreenState extends State 2: GlobalKey(), 3: GlobalKey(), }; + StreamSubscription _sub; void initState() { super.initState(); + initUniLinks(); + final QuickActions quickActions = new QuickActions(); quickActions.setShortcutItems([ - const ShortcutItem(type: 'action_subscriptions', localizedTitle: 'Subscriptions', icon: 'icon_help'), - const ShortcutItem(type: 'action_popular', localizedTitle: 'Popular threads', icon: 'icon_help'), - const ShortcutItem(type: 'action_latest', localizedTitle: 'Latest threads', icon: 'icon_help') + const ShortcutItem( + type: 'action_subscriptions', + localizedTitle: 'Subscriptions', + icon: 'icon_help'), + const ShortcutItem( + type: 'action_popular', + localizedTitle: 'Popular threads', + icon: 'icon_help'), + const ShortcutItem( + type: 'action_latest', + localizedTitle: 'Latest threads', + icon: 'icon_help') ]); quickActions.initialize((shortcutType) { @@ -55,8 +74,10 @@ class _HomeScreenState extends State }); }); } - if (shortcutType == 'action_popular') ScopedModel.of(context).setCurrentTab(3); - if (shortcutType == 'action_latest') ScopedModel.of(context).setCurrentTab(2); + if (shortcutType == 'action_popular') + ScopedModel.of(context).setCurrentTab(3); + if (shortcutType == 'action_latest') + ScopedModel.of(context).setCurrentTab(2); // More handling code... }); } @@ -75,9 +96,55 @@ class _HomeScreenState extends State @override void dispose() { _dataSub.cancel(); + _sub.cancel(); super.dispose(); } + Future initUniLinks() async { + // Platform messages may fail, so we use a try/catch PlatformException. + try { + Uri initialUri = await getInitialUri(); + print(initialUri.toString()); + if (initialUri != null) handleLink(initialUri); + // Parse the link and warn the user, if it is not correct, + // but keep in mind it could be `null`. + } on PlatformException { + // Handle exception by warning the user their action did not succeed + // return? + } + + // Attach a listener to the stream + _sub = getUriLinksStream().listen((Uri uri) { + handleLink(uri); + // Use the uri and warn the user, if it is not correct + }, onError: (err) { + // Handle exception by warning the user their action did not succeed + }); + } + + void handleLink (Uri uri) { + print(uri.toString()); + print(uri.pathSegments.length); + + // Handle thread links + if (uri.pathSegments.length > 0) { + if (uri.pathSegments[0] == 'thread') { + int threadId = int.tryParse(uri.pathSegments[1]); + + if (threadId != null) { + navigatorKeys[0].currentState.push(MaterialPageRoute( + builder: (context) => ThreadScreen( + threadId: int.parse(uri.pathSegments[1]), + ), + ),); + } + } + } + uri.pathSegments.forEach((segment) { + print(segment); + }); + } + Future _onWillPop() async { // handle login overlay if (_loginIsOpen) { diff --git a/lib/screens/thread.dart b/lib/screens/thread.dart index 31c128c..2cb4262 100644 --- a/lib/screens/thread.dart +++ b/lib/screens/thread.dart @@ -20,7 +20,7 @@ class ThreadScreen extends StatefulWidget { final int threadId; final int page; - ThreadScreen({this.title, this.page = 1, this.postCount, this.threadId}); + ThreadScreen({this.title = 'Loading thread', this.page = 1, this.postCount, this.threadId}); @override _ThreadScreenState createState() => _ThreadScreenState(); @@ -45,7 +45,10 @@ class _ThreadScreenState extends State void initState() { super.initState(); _currentPage = this.widget.page; - _totalPages = (widget.postCount / 20).ceil(); + + if (widget.postCount != null) { + _totalPages = (widget.postCount / 20).ceil(); + } prepareAnimations(); @@ -118,6 +121,7 @@ class _ThreadScreenState extends State setState(() { details = res; _isLoading = false; + _totalPages = (details.totalPosts / 20).ceil(); }); checkIfShouldMarkThreadRead(); }); diff --git a/pubspec.yaml b/pubspec.yaml index 758c621..5dfb30a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -43,6 +43,7 @@ dependencies: sticky_headers: "^0.1.8" bbob_dart: ^0.1.1 event_bus: ^1.1.0 + uni_links: ^0.2.0 hive: git: url: https://github.com/leisim/hive.git From 41a1c91e7b1c41ad5a5f2fd26698d1bc07043c79 Mon Sep 17 00:00:00 2001 From: dasmikko Date: Mon, 5 Aug 2019 14:01:51 +0200 Subject: [PATCH 03/19] Use new fp icon --- lib/widget/CategoryListItem.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/widget/CategoryListItem.dart b/lib/widget/CategoryListItem.dart index ec35a29..5573b64 100644 --- a/lib/widget/CategoryListItem.dart +++ b/lib/widget/CategoryListItem.dart @@ -89,7 +89,7 @@ class CategoryListItem extends StatelessWidget { ), Container( child: CachedNetworkImage( - imageUrl: subforum.icon, + imageUrl: !subforum.icon.contains('static') ? subforum.icon : 'https://knockout.chat' + subforum.icon, width: 40.0, ), ) From d5de974a9cbbdf7c79b1e7262b36545089dee77b Mon Sep 17 00:00:00 2001 From: dasmikko Date: Mon, 5 Aug 2019 17:01:19 +0200 Subject: [PATCH 04/19] Make slateDocumentParser take widgets as parameters to make it more flexible --- .../SlateDocumentParser.dart | 123 +++++------------- 1 file changed, 32 insertions(+), 91 deletions(-) diff --git a/lib/widget/SlateDocumentParser/SlateDocumentParser.dart b/lib/widget/SlateDocumentParser/SlateDocumentParser.dart index d9f7035..a6b3c09 100644 --- a/lib/widget/SlateDocumentParser/SlateDocumentParser.dart +++ b/lib/widget/SlateDocumentParser/SlateDocumentParser.dart @@ -1,12 +1,8 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:knocky/models/slateDocument.dart'; -import 'package:knocky/widget/Thread/PostElements/Video.dart'; -import 'package:knocky/widget/Thread/PostElements/YouTubeEmbed.dart'; -import 'package:knocky/widget/Thread/PostElements/Image.dart'; import 'package:intent/intent.dart' as Intent; import 'package:intent/action.dart' as Action; -import 'package:knocky/widget/Thread/PostElements/Embed.dart'; import 'package:knocky/widget/Thread/PostElements/UserQuote.dart'; class SlateDocumentParser extends StatelessWidget { @@ -14,9 +10,30 @@ class SlateDocumentParser extends StatelessWidget { final Function onPressSpoiler; final GlobalKey scaffoldkey; final BuildContext context; - - SlateDocumentParser( - {this.slateObject, this.onPressSpoiler, this.scaffoldkey, this.context}); + final Function imageWidgetHandler; + final Function videoWidgetHandler; + final Function youTubeWidgetHandler; + final Function twitterEmbedHandler; + final Function userQuoteHandler; + final Function bulletedListHandler; + final Function numberedListHandler; + final Function quotesHandler; + + + SlateDocumentParser({ + this.slateObject, + this.onPressSpoiler, + this.scaffoldkey, + this.context, + @required this.imageWidgetHandler, + @required this.videoWidgetHandler, + @required this.youTubeWidgetHandler, + @required this.twitterEmbedHandler, + @required this.userQuoteHandler, + @required this.bulletedListHandler, + @required this.numberedListHandler, + @required this.quotesHandler, + }); Widget paragraphToWidget(SlateNode node) { List lines = List(); @@ -148,71 +165,16 @@ class SlateDocumentParser extends StatelessWidget { Widget bulletListToWidget(SlateNode node) { List listItemsContent = List(); - List listItems = List(); - listItemsContent.addAll(handleNodes(node.nodes)); - // Handle block nodes - listItemsContent.forEach((item) { - listItems.add( - Container( - margin: EdgeInsets.only(bottom: 5.0), - child: Row( - children: [ - Container( - margin: EdgeInsets.only(right: 10.0), - height: 5.0, - width: 5.0, - decoration: new BoxDecoration( - color: Theme.of(context).textTheme.body1.color, - shape: BoxShape.circle, - ), - ), - Expanded(child: item) - ], - ), - ), - ); - }); - - return Container( - margin: EdgeInsets.only(top: 10, bottom: 10), - child: Column(children: listItems), - ); + return this.bulletedListHandler(listItemsContent); } Widget numberedListToWidget(SlateNode node) { List listItemsContent = List(); - List listItems = List(); - listItemsContent.addAll(handleNodes(node.nodes)); - // Handle block nodes - listItemsContent.forEach((item) { - listItems.add( - Container( - margin: EdgeInsets.only(bottom: 5.0), - child: Row( - children: [ - Container( - margin: EdgeInsets.only(right: 10.0), - child: Text( - (listItems.length + 1).toString(), - ), - ), - Expanded( - child: item, - ) - ], - ), - ), - ); - }); - - return Container( - margin: EdgeInsets.only(bottom: 10), - child: Column(children: listItems), - ); + return this.numberedListHandler(listItemsContent); } Widget headingToWidget(SlateNode node) { @@ -256,44 +218,23 @@ class SlateDocumentParser extends StatelessWidget { // Handle block nodes widgets.addAll(handleNodes(node.nodes, isChild: !isChild)); - return UserQuoteWidget( - username: node.data.postData.username, - children: widgets, - isChild: isChild, - ); + return this.userQuoteHandler(node.data.postData.username, widgets, isChild); } Widget youTubeToWidget(SlateNode node) { - return YoutubeVideoEmbed(url: node.data.src); + return this.youTubeWidgetHandler(node.data.src); } Widget handleQuotes(SlateNode node) { - return Container( - margin: EdgeInsets.only(bottom: 10.0), - padding: EdgeInsets.all(10.0), - decoration: BoxDecoration( - border: Border( - left: BorderSide(color: Colors.blue, width: 3.0), - ), - color: Colors.grey, - ), - child: paragraphToWidget(node), - ); + return this.quotesHandler(paragraphToWidget(node)); } Widget handleImage(SlateNode node) { - - return Container( - margin: EdgeInsets.only(top: 10.0, bottom: 10.0), - child: LimitedBox( - maxHeight: 300, - child: ImageWidget(url: node.data.src, slateObject: slateObject), - ), - ); + return this.imageWidgetHandler(node.data.src, slateObject); } Widget handleVideo(SlateNode node) { - return VideoElement(url: node.data.src, scaffoldKey: scaffoldkey); + return this.videoWidgetHandler(node.data.src); } List handleNodes(List nodes, {bool isChild = false}) { @@ -333,7 +274,7 @@ class SlateDocumentParser extends StatelessWidget { widgets.add(handleQuotes(node)); break; case 'twitter': - widgets.add(EmbedWidget(url: node.data.src)); + widgets.add(this.twitterEmbedHandler(node.data.src)); break; case 'video': widgets.add(handleVideo(node)); From 3d1862ee8f1c0c89d18c7c0584d2880dd2cb820d Mon Sep 17 00:00:00 2001 From: dasmikko Date: Mon, 5 Aug 2019 17:01:31 +0200 Subject: [PATCH 05/19] Implement new refactor --- lib/widget/Thread/ThreadPostItem.dart | 111 ++++++++++++++++++++++++++ 1 file changed, 111 insertions(+) diff --git a/lib/widget/Thread/ThreadPostItem.dart b/lib/widget/Thread/ThreadPostItem.dart index f37598e..98b0087 100644 --- a/lib/widget/Thread/ThreadPostItem.dart +++ b/lib/widget/Thread/ThreadPostItem.dart @@ -3,6 +3,11 @@ import 'package:knocky/models/thread.dart'; import 'package:knocky/widget/SlateDocumentParser/SlateDocumentParser.dart'; import 'package:knocky/helpers/icons.dart'; import 'package:cached_network_image/cached_network_image.dart'; +import 'package:knocky/widget/Thread/PostElements/Embed.dart'; +import 'package:knocky/widget/Thread/PostElements/Image.dart'; +import 'package:knocky/widget/Thread/PostElements/UserQuote.dart'; +import 'package:knocky/widget/Thread/PostElements/Video.dart'; +import 'package:knocky/widget/Thread/PostElements/YouTubeEmbed.dart'; import 'package:knocky/widget/Thread/PostHeader.dart'; import 'package:scoped_model/scoped_model.dart'; import 'package:knocky/state/authentication.dart'; @@ -190,6 +195,112 @@ class ThreadPostItem extends StatelessWidget { onPressSpoiler(context, text); }, context: context, + imageWidgetHandler: (String imageUrl, slateObject) { + return Container( + margin: EdgeInsets.only(top: 10.0, bottom: 10.0), + child: LimitedBox( + maxHeight: 300, + child: + ImageWidget(url: imageUrl, slateObject: slateObject), + ), + ); + }, + videoWidgetHandler: (String videoUrl) { + return VideoElement( + url: videoUrl, + scaffoldKey: this.scaffoldKey, + ); + }, + youTubeWidgetHandler: (String youTubeUrl) { + return YoutubeVideoEmbed( + url: youTubeUrl, + ); + }, + twitterEmbedHandler: (String embedUrl) { + return EmbedWidget( + url: embedUrl, + ); + }, + userQuoteHandler: + (String username, List widgets, bool isChild) { + return UserQuoteWidget( + username: username, + children: widgets, + isChild: isChild, + ); + }, + bulletedListHandler: (List listItemsContent) { + List listItems = List(); + // Handle block nodes + listItemsContent.forEach((item) { + listItems.add( + Container( + margin: EdgeInsets.only(bottom: 5.0), + child: Row( + children: [ + Container( + margin: EdgeInsets.only(right: 10.0), + height: 5.0, + width: 5.0, + decoration: new BoxDecoration( + color: Theme.of(context).textTheme.body1.color, + shape: BoxShape.circle, + ), + ), + Expanded(child: item) + ], + ), + ), + ); + }); + + return Container( + margin: EdgeInsets.only(top: 10, bottom: 10), + child: Column(children: listItems), + ); + }, + numberedListHandler: (List listItemsContent) { + List listItems = List(); + // Handle block nodes + listItemsContent.forEach((item) { + listItems.add( + Container( + margin: EdgeInsets.only(bottom: 5.0), + child: Row( + children: [ + Container( + margin: EdgeInsets.only(right: 10.0), + child: Text( + (listItems.length + 1).toString(), + ), + ), + Expanded( + child: item, + ) + ], + ), + ), + ); + }); + + return Container( + margin: EdgeInsets.only(bottom: 10), + child: Column(children: listItems), + ); + }, + quotesHandler: (Widget content) { + return Container( + margin: EdgeInsets.only(bottom: 10.0), + padding: EdgeInsets.all(10.0), + decoration: BoxDecoration( + border: Border( + left: BorderSide(color: Colors.blue, width: 3.0), + ), + color: Colors.grey, + ), + child: content, + ); + }, ), ), Container( From d8223d5b6462c7d04908e2e3dc637129574e8050 Mon Sep 17 00:00:00 2001 From: dasmikko Date: Wed, 7 Aug 2019 17:17:14 +0200 Subject: [PATCH 06/19] Bunch of editor changes --- lib/helpers/bbcode.dart | 287 ++------ lib/screens/editPost.dart | 497 -------------- lib/screens/newPost.dart | 626 +++++++++++++----- lib/screens/thread.dart | 3 +- lib/widget/PostEditor.dart | 220 ++++++ .../SlateDocumentController.dart | 7 + .../SlateDocumentParser.dart | 107 +-- lib/widget/Thread/ThreadPostItem.dart | 87 ++- pubspec.yaml | 1 + 9 files changed, 848 insertions(+), 987 deletions(-) delete mode 100644 lib/screens/editPost.dart create mode 100644 lib/widget/PostEditor.dart create mode 100644 lib/widget/SlateDocumentParser/SlateDocumentController.dart diff --git a/lib/helpers/bbcode.dart b/lib/helpers/bbcode.dart index de80bd4..f3dde14 100644 --- a/lib/helpers/bbcode.dart +++ b/lib/helpers/bbcode.dart @@ -1,19 +1,17 @@ import 'package:bbob_dart/bbob_dart.dart' as bbob; import 'package:knocky/models/slateDocument.dart'; -import 'package:knocky/models/thread.dart'; + class BBCodeHandler implements bbob.NodeVisitor { - SlateDocument document = SlateDocument(object: 'document', nodes: List()); + SlateNode paragraph = SlateNode(object: 'block', nodes: List()); StringBuffer _leafContentBuffer = StringBuffer(); - List _replyList = List(); - Thread _thread; SlateNode _lastElement; List _leafMarks = List(); - SlateObject parse(String text, Thread thread, List replyList) { - _thread = thread; - _replyList = replyList; + SlateNode parse(String text, {type: 'paragraph'}) { + paragraph.type = type; + var ast = bbob.parse(text); for (final node in ast) { @@ -32,50 +30,15 @@ class BBCodeHandler implements bbob.NodeVisitor { ]); // Add node - _lastElement.nodes.add(textLeafNode); + paragraph.nodes.add(textLeafNode); _leafContentBuffer = StringBuffer(); - document.nodes.add(_lastElement); } - return SlateObject(object: 'value', document: document); + return paragraph; } void visitText(bbob.Text text) { - if (_lastElement == null) { - _lastElement = SlateNode( - object: 'block', - type: 'paragraph', - data: SlateNodeData(), - nodes: List()); - } - - if (text.textContent == '\n') { - // New leaf is appearing, add old leaf to node - SlateNode textLeafNode = SlateNode(object: 'text', leaves: [ - SlateLeaf( - text: _leafContentBuffer.toString(), - marks: _leafMarks, - object: 'leaf'), - ]); - - // Reset leaf marks - _leafMarks = List(); - - // Add node - _lastElement.nodes.add(textLeafNode); - _leafContentBuffer = StringBuffer(); - - document.nodes.add(_lastElement); - - // Paragraph ended, to reset last element - _lastElement = SlateNode( - object: 'block', - type: 'paragraph', - data: SlateNodeData(), - nodes: List()); - } else { - _leafContentBuffer.write(text.textContent); - } + _leafContentBuffer.write(text.textContent); } bool visitElementBefore(bbob.Element element) { @@ -91,7 +54,7 @@ class BBCodeHandler implements bbob.NodeVisitor { // Reset leaf marks _leafMarks = List(); - _lastElement.nodes.add(textNode); + paragraph.nodes.add(textNode); _leafContentBuffer = StringBuffer(); } @@ -113,15 +76,7 @@ class BBCodeHandler implements bbob.NodeVisitor { } if (element.tag == 'url') { - if (_lastElement == null) { - _lastElement = SlateNode( - object: 'block', - type: 'paragraph', - data: SlateNodeData(), - nodes: List()); - } - - _lastElement.nodes.add(SlateNode( + paragraph.nodes.add(SlateNode( object: 'inline', type: 'link', data: SlateNodeData(href: element.children.first.textContent), @@ -137,170 +92,29 @@ class BBCodeHandler implements bbob.NodeVisitor { return false; } - if (element.tag == 'img') { - if (_lastElement != null) { - _lastElement = SlateNode( - object: 'block', - type: 'paragraph', - data: SlateNodeData(), - nodes: List()); - } - - SlateNode imgNode = SlateNode( - object: 'block', - type: 'image', - data: SlateNodeData(src: element.children.first.textContent), - nodes: List(), - ); - - document.nodes.add(imgNode); - // Do not handle children - return false; - } - - if (element.tag == 'h1') { - _lastElement = SlateNode( - object: 'block', - type: 'heading-one', - data: null, - nodes: [ - SlateNode(object: 'text', leaves: []), - ], - ); - } - - if (element.tag == 'h2') { - _lastElement = - SlateNode(object: 'block', type: 'heading-two', data: null, nodes: [ - SlateNode( - object: 'text', - leaves: [], - ) - ]); - } - - if (element.tag == 'blockquote') { - _lastElement = - SlateNode(object: 'block', type: 'block-quote', data: null, nodes: [ - SlateNode( - object: 'text', - leaves: [], - ) - ]); - } - - if (element.tag == 'youtube') { - document.nodes.add(SlateNode( - object: 'block', - type: 'youtube', - data: SlateNodeData(src: element.children.first.textContent), - nodes: [])); - return false; - } - - if (element.tag == 'video') { - document.nodes.add(SlateNode( - object: 'block', - type: 'video', - data: SlateNodeData(src: element.children.first.textContent), - nodes: [])); - return false; - } - - if (element.tag == 'userquote') { - int replyIndex = int.parse(element.children.first.textContent) - 1; - - if (_replyList[replyIndex] != null) { - ThreadPost reply = _replyList[replyIndex]; - - document.nodes.add( - SlateNode( - object: 'block', - type: 'userquote', - data: SlateNodeData( - postData: NodeDataPostData( - postId: reply.id, - threadId: _thread.id, - threadPage: _thread.currentPage, - username: reply.user.username, - ), - ), - nodes: reply.content.document.nodes), - ); - } - return false; - } - - if (element.tag == 'ul') { - _lastElement = SlateNode( - object: 'block', - type: 'bulleted-list', - data: SlateNodeData(), - nodes: []); - } - - if (element.tag == 'ol') { - _lastElement = SlateNode( - object: 'block', - type: 'numbered-list', - data: SlateNodeData(), - nodes: []); - } - - if (element.tag == 'li') { - _lastElement.nodes.add( - SlateNode( - object: 'block', - type: 'list-item', - data: SlateNodeData(), - nodes: []), - ); - } - // Handle children return true; } void visitElementAfter(bbob.Element element) { - switch (element.tag) { - case 'li': - SlateNode textNode = SlateNode(object: 'text', leaves: [ - SlateLeaf( - text: _leafContentBuffer.toString(), - marks: _leafMarks, - object: 'leaf') - ]); - - // Reset leaf marks - _leafMarks = List(); - - _lastElement.nodes.last.nodes.add(textNode); - _leafContentBuffer = StringBuffer(); - break; - case 'ul': - document.nodes.add(_lastElement); - _lastElement = null; - break; - default: - // Tag is done, add leaf - SlateNode textNode = SlateNode(object: 'text', leaves: [ - SlateLeaf( - text: _leafContentBuffer.toString(), - marks: _leafMarks, - object: 'leaf') - ]); - - // Reset leaf marks - _leafMarks = List(); - - _lastElement.nodes.add(textNode); - _leafContentBuffer = StringBuffer(); - } + // Tag is done, add leaf + SlateNode textNode = SlateNode(object: 'text', leaves: [ + SlateLeaf( + text: _leafContentBuffer.toString(), + marks: _leafMarks, + object: 'leaf') + ]); + + // Reset leaf marks + _leafMarks = List(); + + paragraph.nodes.add(textNode); + _leafContentBuffer = StringBuffer(); } - Map slateDocumentToBBCode(SlateDocument document) { - String bbcode = _handleNodes(document.nodes).trim(); - return {'bbcode': bbcode, 'userquotes': {}}; + String slateParagraphToBBCode(SlateNode node) { + String bbcode = _handleNodes(node.nodes).trim(); + return bbcode; } String _inlineHandler(SlateNode object, SlateNode node) { @@ -329,37 +143,33 @@ class BBCodeHandler implements bbob.NodeVisitor { StringBuffer content = new StringBuffer(); List contentItems = List(); - nodes.forEach((node) { - if (asList) { - print(node.type); + nodes.forEach((line) { + if (line.leaves != null) { + content.write(_leafHandler(line.leaves)); + } + + // Handle inline element + if (line.object == 'inline') { + // Handle links + if (line.type == 'link') { + line.nodes.forEach((inlineNode) { + inlineNode.leaves.forEach((leaf) { + content.write('[url]' + leaf.text + '[/url]'); + }); + }); + } else { + line.nodes.forEach((inlineNode) { + inlineNode.leaves.forEach((leaf) { + content.write(leaf.text); + }); + }); + } } // Handle blocks - switch (node.type) { + /*switch (node.type) { case 'paragraph': - node.nodes.asMap().forEach((i, line) { - if (line.leaves != null) { - content.write(_leafHandler(line.leaves)); - } - // Handle inline element - if (line.object == 'inline') { - // Handle links - if (line.type == 'link') { - line.nodes.forEach((inlineNode) { - inlineNode.leaves.forEach((leaf) { - content.write('[url]' + leaf.text + '[/url]'); - }); - }); - } else { - line.nodes.forEach((inlineNode) { - inlineNode.leaves.forEach((leaf) { - content.write(leaf.text); - }); - }); - } - } - }); content.write('\n'); break; case 'heading-one': @@ -451,6 +261,7 @@ class BBCodeHandler implements bbob.NodeVisitor { //widgets.add(handleVideo(node)); break; } + */ if (asList) { contentItems.add(content.toString()); diff --git a/lib/screens/editPost.dart b/lib/screens/editPost.dart deleted file mode 100644 index fc676c2..0000000 --- a/lib/screens/editPost.dart +++ /dev/null @@ -1,497 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:knocky/helpers/bbcode.dart'; -import 'package:knocky/models/slateDocument.dart'; -import 'package:knocky/models/thread.dart'; -import 'package:knocky/widget/SlateDocumentParser/SlateDocumentParser.dart'; -import 'package:knocky/helpers/api.dart'; -import 'package:knocky/widget/KnockoutLoadingIndicator.dart'; - -class EditPostScreen extends StatefulWidget { - final ThreadPost post; - final Thread thread; - - EditPostScreen( - {this.post, this.thread}); - - @override - _EditPostScreenState createState() => _EditPostScreenState(); -} - -class _EditPostScreenState extends State { - TextEditingController controller = TextEditingController(text: ''); - SlateObject document; - GlobalKey _scaffoldKey; - FocusNode textFocusNode = FocusNode(); - bool _isPosting = false; - List replyList = List(); - - List history = List(); - - @override - void initState() { - super.initState(); - - Map bbcodeObj = BBCodeHandler().slateDocumentToBBCode(this.widget.post.content.document); - controller.text = bbcodeObj['bbcode']; - - document = BBCodeHandler() - .parse(controller.text, this.widget.thread, replyList); - } - - void onPressPost() async { - setState(() { - _isPosting = true; - }); - await KnockoutAPI().newPost(document.toJson(), this.widget.thread.id); - Navigator.pop(context, true); - } - - void refreshPreview() { - setState(() { - document = BBCodeHandler() - .parse(controller.text, this.widget.thread, this.replyList); - }); - } - - void onPressSpoiler(BuildContext context, String content) { - showDialog( - context: context, - builder: (BuildContext context) { - // return object of type Dialog - return AlertDialog( - content: new Text(content), - actions: [ - // usually buttons at the bottom of the dialog - new FlatButton( - child: new Text("Close"), - onPressed: () { - Navigator.of(context).pop(); - }, - ), - ], - ); - }, - ); - } - - void addTagAtSelection(int start, int end, String tag) { - RegExp regExp = new RegExp( - r'(\[([^/].*?)(=(.+?))?\](.*?)\[/\2\]|\[([^/].*?)(=(.+?))?\])', - caseSensitive: false, - multiLine: false, - ); - - String newline = tag == 'h1' || tag == 'h2' ? "\n" : ''; - String selectedText = controller.text.substring(start, end); - String replaceWith = ''; - - if (regExp.hasMatch(selectedText)) { - replaceWith = selectedText.replaceAll('[${tag}]', ''); //ignore: unnecessary_brace_in_string_interps - replaceWith = replaceWith.replaceAll('[/${tag}]', ''); //ignore: unnecessary_brace_in_string_interps - } else { - replaceWith = newline + '[${tag}]' + selectedText + '[/${tag}]'; //ignore: unnecessary_brace_in_string_interps - } - controller.text = controller.text.replaceRange(start, end, replaceWith); - - refreshPreview(); - } - - void addImageDialog() async { - ClipboardData clipBoardText = await Clipboard.getData('text/plain'); - TextEditingController imgurlController = - TextEditingController(text: clipBoardText.text); - await showDialog( - context: context, - child: new AlertDialog( - contentPadding: const EdgeInsets.all(16.0), - content: new Row( - children: [ - new Expanded( - child: new TextField( - autofocus: true, - keyboardType: TextInputType.url, - controller: imgurlController, - decoration: new InputDecoration(labelText: 'Image url'), - ), - ) - ], - ), - actions: [ - new FlatButton( - child: const Text('Cancel'), - onPressed: () { - Navigator.of(context, rootNavigator: true).pop(); - }), - new FlatButton( - child: const Text('Insert'), - onPressed: () { - Navigator.of(context, rootNavigator: true).pop(); - - if(controller.text.endsWith('\n') || controller.text.isEmpty) { - controller.text = - controller.text + '[img]${imgurlController.text}[/img]'; - } else { - controller.text = - controller.text + '\n[img]${imgurlController.text}[/img]'; - } - refreshPreview(); - }) - ], - ), - ); - } - - void addLinkDialog() async { - ClipboardData clipBoardText = await Clipboard.getData('text/plain'); - TextEditingController urlController = - TextEditingController(text: clipBoardText.text); - await showDialog( - context: context, - child: new AlertDialog( - contentPadding: const EdgeInsets.all(16.0), - content: new Row( - children: [ - new Expanded( - child: new TextField( - autofocus: true, - keyboardType: TextInputType.url, - controller: urlController, - decoration: new InputDecoration(labelText: 'Url'), - ), - ) - ], - ), - actions: [ - new FlatButton( - child: const Text('Cancel'), - onPressed: () { - Navigator.of(context, rootNavigator: true).pop(); - }), - new FlatButton( - child: const Text('Insert'), - onPressed: () { - Navigator.of(context, rootNavigator: true).pop(); - - if(controller.text.endsWith('\n') || controller.text.isEmpty) { - controller.text = - controller.text + '[url]${urlController.text}[/url]'; - } else { - controller.text = - controller.text + '\n[url]${urlController.text}[/url]'; - } - - refreshPreview(); - }) - ], - ), - ); - } - - void addYoutubeVideoDialog() async { - ClipboardData clipBoardText = await Clipboard.getData('text/plain'); - TextEditingController urlController = - TextEditingController(text: clipBoardText.text); - await showDialog( - context: context, - child: new AlertDialog( - contentPadding: const EdgeInsets.all(16.0), - content: new Row( - children: [ - new Expanded( - child: new TextField( - autofocus: true, - keyboardType: TextInputType.url, - controller: urlController, - decoration: new InputDecoration(labelText: 'YouTube URL'), - ), - ) - ], - ), - actions: [ - new FlatButton( - child: const Text('Cancel'), - onPressed: () { - Navigator.of(context, rootNavigator: true).pop(); - }), - new FlatButton( - child: const Text('Insert'), - onPressed: () { - Navigator.of(context, rootNavigator: true).pop(); - - if(controller.text.endsWith('\n') || controller.text.isEmpty) { - controller.text = controller.text + - '[youtube]${urlController.text}[/youtube]'; - } else { - controller.text = controller.text + - '\n[youtube]${urlController.text}[/youtube]'; - } - refreshPreview(); - }) - ], - ), - ); - } - - void addVideoDialog() async { - ClipboardData clipBoardText = await Clipboard.getData('text/plain'); - TextEditingController urlController = - TextEditingController(text: clipBoardText.text); - await showDialog( - context: context, - child: new AlertDialog( - contentPadding: const EdgeInsets.all(16.0), - content: new Row( - children: [ - new Expanded( - child: new TextField( - autofocus: true, - keyboardType: TextInputType.url, - controller: urlController, - decoration: - new InputDecoration(labelText: 'Video URL (Webm/mp4)'), - ), - ) - ], - ), - actions: [ - new FlatButton( - child: const Text('Cancel'), - onPressed: () { - Navigator.of(context, rootNavigator: true).pop(); - }), - new FlatButton( - child: const Text('Insert'), - onPressed: () { - Navigator.of(context, rootNavigator: true).pop(); - - if(controller.text.endsWith('\n') || controller.text.isEmpty) { - controller.text = - controller.text + '[video]${urlController.text}[/video]'; - } else { - controller.text = - controller.text + '\n[video]${urlController.text}[/video]'; - } - refreshPreview(); - }) - ], - ), - ); - } - - void addUserquoteDialog(bcontext) async { - BuildContext self = bcontext; - await showDialog( - context: bcontext, - child: new AlertDialog( - title: Text('Select post'), - contentPadding: const EdgeInsets.all(16.0), - content: Container( - height: 400, - width: 200, - child: ListView.builder( - itemCount: this.replyList.length, - itemBuilder: (BuildContext context, int index) { - ThreadPost item = this.replyList[index]; - return ListTile( - title: Text(item.user.username), - onTap: () { - Navigator.of(bcontext, rootNavigator: true).pop(); - - if(controller.text.endsWith('\n') || controller.text.isEmpty) { - controller.text = - controller.text + '[userquote]${index + 1}[/userquote]'; - } else { - controller.text = - controller.text + '\n[userquote]${index + 1}[/userquote]'; - } - refreshPreview(); - }, - ); - }, - ), - ), - actions: [ - new FlatButton( - child: const Text('Cancel'), - onPressed: () { - Navigator.of(bcontext, rootNavigator: true).pop(); - }), - ], - ), - ); - } - - @override - Widget build(BuildContext wcontext) { - return DefaultTabController( - length: 2, - child: Scaffold( - key: _scaffoldKey, - appBar: AppBar( - title: Text('New post'), - actions: [ - IconButton( - onPressed: !_isPosting ? onPressPost : null, - icon: Icon(Icons.send), - ), - ], - bottom: TabBar( - tabs: [ - Tab( - text: 'Edit', - ), - Tab(text: 'Preview'), - ], - ), - ), - body: KnockoutLoadingIndicator( - show: _isPosting, - child: TabBarView( - physics: NeverScrollableScrollPhysics(), - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Expanded( - child: Container( - padding: EdgeInsets.all(25), - child: TextField( - controller: controller, - focusNode: textFocusNode, - maxLines: null, - keyboardType: TextInputType.multiline, - textCapitalization: TextCapitalization.sentences, - onChanged: (text) { - setState(() { - document = BBCodeHandler().parse(text, - this.widget.thread, this.replyList); - }); - }, - ), - ), - ), - Container( - color: Colors.grey[600], - padding: EdgeInsets.all(0), - child: Wrap( - children: [ - IconButton( - icon: Icon(Icons.format_bold), - onPressed: () { - TextSelection theSelection = controller.selection; - addTagAtSelection( - theSelection.start, theSelection.end, 'b'); - }, - ), - IconButton( - icon: Icon(Icons.format_italic), - onPressed: () { - TextSelection theSelection = controller.selection; - addTagAtSelection( - theSelection.start, theSelection.end, 'i'); - }, - ), - IconButton( - icon: Icon(Icons.format_underlined), - onPressed: () { - TextSelection theSelection = controller.selection; - addTagAtSelection( - theSelection.start, theSelection.end, 'u'); - }, - ), - IconButton( - icon: Icon(Icons.code), - onPressed: () { - TextSelection theSelection = controller.selection; - addTagAtSelection( - theSelection.start, theSelection.end, 'code'); - }, - ), - IconButton( - icon: Icon(Icons.title), - onPressed: () { - TextSelection theSelection = controller.selection; - addTagAtSelection( - theSelection.start, theSelection.end, 'h1'); - }, - ), - IconButton( - icon: Icon(Icons.format_size), - onPressed: () { - TextSelection theSelection = controller.selection; - addTagAtSelection( - theSelection.start, theSelection.end, 'h2'); - }, - ), - IconButton( - icon: Icon(Icons.format_quote), - onPressed: () { - TextSelection theSelection = controller.selection; - addTagAtSelection( - theSelection.start, theSelection.end, 'blockquote'); - }, - ), - IconButton( - icon: Icon(Icons.image), - onPressed: () { - addImageDialog(); - }, - ), - IconButton( - icon: Icon(Icons.link), - onPressed: () { - addLinkDialog(); - }, - ), - IconButton( - icon: Icon(Icons.ondemand_video), - onPressed: () { - addYoutubeVideoDialog(); - }, - ), - IconButton( - icon: Icon(Icons.videocam), - onPressed: () { - addVideoDialog(); - }, - ), - if (this.replyList.length > 0) - Builder( - builder: (BuildContext bcontext) { - return IconButton( - tooltip: 'Insert userquote', - icon: Icon(Icons.message), - onPressed: () { - addUserquoteDialog(bcontext); - }, - ); - }, - ), - ], - ), - ), - ], - ), - Container( - child: SingleChildScrollView( - child: Container( - padding: EdgeInsets.all(15), - child: SlateDocumentParser( - context: context, - scaffoldkey: _scaffoldKey, - slateObject: document, - onPressSpoiler: (content) { - onPressSpoiler(context, content); - }, - ), - ), - ), - ), - ], - ), - ), - ), - ); - } -} diff --git a/lib/screens/newPost.dart b/lib/screens/newPost.dart index 7495be5..915b6b8 100644 --- a/lib/screens/newPost.dart +++ b/lib/screens/newPost.dart @@ -1,39 +1,46 @@ +import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:knocky/helpers/bbcode.dart'; import 'package:knocky/models/slateDocument.dart'; import 'package:knocky/models/thread.dart'; +import 'package:knocky/widget/PostEditor.dart'; import 'package:knocky/widget/SlateDocumentParser/SlateDocumentParser.dart'; import 'package:knocky/helpers/api.dart'; import 'package:knocky/widget/KnockoutLoadingIndicator.dart'; +import 'package:knocky/widget/Thread/PostElements/Embed.dart'; +import 'package:knocky/widget/Thread/PostElements/Image.dart'; +import 'package:knocky/widget/Thread/PostElements/UserQuote.dart'; +import 'package:knocky/widget/Thread/PostElements/Video.dart'; +import 'package:knocky/widget/Thread/PostElements/YouTubeEmbed.dart'; +import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; class NewPostScreen extends StatefulWidget { final ThreadPost replyTo; final Thread thread; final List replyList; - NewPostScreen( - {this.replyTo, this.thread, this.replyList}); + NewPostScreen({this.replyTo, this.thread, this.replyList}); @override _NewPostScreenState createState() => _NewPostScreenState(); } class _NewPostScreenState extends State { - TextEditingController controller = TextEditingController(text: ''); - SlateObject document; + SlateObject document = new SlateObject( + document: SlateDocument(nodes: List()), + ); GlobalKey _scaffoldKey; - FocusNode textFocusNode = FocusNode(); bool _isPosting = false; - - List history = List(); + TextEditingController controller = TextEditingController(); @override void initState() { super.initState(); - document = BBCodeHandler() - .parse(controller.text, this.widget.thread, this.widget.replyList); + //document = BBCodeHandler() + // .parse(controller.text, this.widget.thread, this.widget.replyList); } void onPressPost() async { @@ -44,12 +51,7 @@ class _NewPostScreenState extends State { Navigator.pop(context, true); } - void refreshPreview() { - setState(() { - document = BBCodeHandler() - .parse(controller.text, this.widget.thread, this.widget.replyList); - }); - } + void refreshPreview() {} void onPressSpoiler(BuildContext context, String content) { showDialog( @@ -72,7 +74,8 @@ class _NewPostScreenState extends State { ); } - void addTagAtSelection(int start, int end, String tag) { + void addTagAtSelection( + TextEditingController controller, int start, int end, String tag) { RegExp regExp = new RegExp( r'(\[([^/].*?)(=(.+?))?\](.*?)\[/\2\]|\[([^/].*?)(=(.+?))?\])', caseSensitive: false, @@ -84,16 +87,25 @@ class _NewPostScreenState extends State { String replaceWith = ''; if (regExp.hasMatch(selectedText)) { - replaceWith = selectedText.replaceAll('[${tag}]', ''); //ignore: unnecessary_brace_in_string_interps - replaceWith = replaceWith.replaceAll('[/${tag}]', ''); //ignore: unnecessary_brace_in_string_interps + replaceWith = selectedText.replaceAll( + '[${tag}]', ''); //ignore: unnecessary_brace_in_string_interps + replaceWith = replaceWith.replaceAll( + '[/${tag}]', ''); //ignore: unnecessary_brace_in_string_interps } else { - replaceWith = newline + '[${tag}]' + selectedText + '[/${tag}]'; //ignore: unnecessary_brace_in_string_interps + replaceWith = newline + + '[${tag}]' + + selectedText + + '[/${tag}]'; //ignore: unnecessary_brace_in_string_interps } controller.text = controller.text.replaceRange(start, end, replaceWith); refreshPreview(); } + /** + * Toolbar callbacks + */ + void addImageDialog() async { ClipboardData clipBoardText = await Clipboard.getData('text/plain'); TextEditingController imgurlController = @@ -124,22 +136,22 @@ class _NewPostScreenState extends State { child: const Text('Insert'), onPressed: () { Navigator.of(context, rootNavigator: true).pop(); - - if(controller.text.endsWith('\n') || controller.text.isEmpty) { - controller.text = - controller.text + '[img]${imgurlController.text}[/img]'; - } else { - controller.text = - controller.text + '\n[img]${imgurlController.text}[/img]'; - } - refreshPreview(); + setState(() { + document.document.nodes.add( + SlateNode( + object: 'block', + type: 'image', + data: SlateNodeData(src: imgurlController.text), + ), + ); + }); }) ], ), ); } - void addLinkDialog() async { + void addLinkDialog(TextEditingController mainController) async { ClipboardData clipBoardText = await Clipboard.getData('text/plain'); TextEditingController urlController = TextEditingController(text: clipBoardText.text); @@ -170,12 +182,13 @@ class _NewPostScreenState extends State { onPressed: () { Navigator.of(context, rootNavigator: true).pop(); - if(controller.text.endsWith('\n') || controller.text.isEmpty) { - controller.text = - controller.text + '[url]${urlController.text}[/url]'; + if (mainController.text.endsWith('\n') || + controller.text.isEmpty) { + mainController.text = + mainController.text + '[url]${urlController.text}[/url]'; } else { - controller.text = - controller.text + '\n[url]${urlController.text}[/url]'; + mainController.text = mainController.text + + '\n[url]${urlController.text}[/url]'; } refreshPreview(); @@ -216,12 +229,12 @@ class _NewPostScreenState extends State { onPressed: () { Navigator.of(context, rootNavigator: true).pop(); - if(controller.text.endsWith('\n') || controller.text.isEmpty) { + if (controller.text.endsWith('\n') || controller.text.isEmpty) { controller.text = controller.text + - '[youtube]${urlController.text}[/youtube]'; + '[youtube]${urlController.text}[/youtube]'; } else { controller.text = controller.text + - '\n[youtube]${urlController.text}[/youtube]'; + '\n[youtube]${urlController.text}[/youtube]'; } refreshPreview(); }) @@ -262,12 +275,12 @@ class _NewPostScreenState extends State { onPressed: () { Navigator.of(context, rootNavigator: true).pop(); - if(controller.text.endsWith('\n') || controller.text.isEmpty) { + if (controller.text.endsWith('\n') || controller.text.isEmpty) { controller.text = - controller.text + '[video]${urlController.text}[/video]'; + controller.text + '[video]${urlController.text}[/video]'; } else { - controller.text = - controller.text + '\n[video]${urlController.text}[/video]'; + controller.text = controller.text + + '\n[video]${urlController.text}[/video]'; } refreshPreview(); }) @@ -294,12 +307,13 @@ class _NewPostScreenState extends State { onTap: () { Navigator.of(bcontext, rootNavigator: true).pop(); - if(controller.text.endsWith('\n') || controller.text.isEmpty) { + if (controller.text.endsWith('\n') || + controller.text.isEmpty) { controller.text = - controller.text + '[userquote]${index + 1}[/userquote]'; + controller.text + '[userquote]${index + 1}[/userquote]'; } else { - controller.text = - controller.text + '\n[userquote]${index + 1}[/userquote]'; + controller.text = controller.text + + '\n[userquote]${index + 1}[/userquote]'; } refreshPreview(); }, @@ -318,6 +332,202 @@ class _NewPostScreenState extends State { ); } + void addTextBlock() { + setState( + () { + this.document.document.nodes.add( + SlateNode( + type: 'paragraph', + object: 'block', + nodes: [ + SlateNode( + object: 'text', + leaves: [], + ), + ], + ), + ); + }, + ); + } + + void addHeadingBlock(String type) { + setState( + () { + this.document.document.nodes.add( + SlateNode( + type: 'heading-' + type, + object: 'block', + nodes: [ + SlateNode( + object: 'text', + leaves: [], + ), + ], + ), + ); + }, + ); + } + + /* + Block tap callbacks + */ + void showTextEditDialog(BuildContext context, SlateNode node) { + String bbcodeText = BBCodeHandler().slateParagraphToBBCode(node); + TextEditingController textEditingController = + TextEditingController(text: bbcodeText); + + showDialog( + context: context, + builder: (BuildContext bcontext) { + return AlertDialog( + actions: [ + FlatButton( + child: Text('Remove'), + onPressed: () { + setState(() { + int index = this.document.document.nodes.indexOf(node); + this.document.document.nodes.removeAt(index); + }); + Navigator.pop(bcontext); + }, + ), + FlatButton( + child: Text('Save'), + onPressed: () { + SlateNode newNode = BBCodeHandler() + .parse(textEditingController.text, type: node.type); + print(BBCodeHandler().slateParagraphToBBCode(newNode)); + print(newNode.toJson()); + Navigator.pop(bcontext, newNode); + }, + ) + ], + title: Text('Edit text block'), + content: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisAlignment: MainAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Container( + margin: EdgeInsets.only(bottom: 10), + child: TextField( + keyboardType: TextInputType.multiline, + maxLines: null, + textCapitalization: TextCapitalization.sentences, + controller: textEditingController, + ), + ), + Container( + color: Colors.grey[600], + padding: EdgeInsets.all(0), + child: Wrap( + children: [ + IconButton( + icon: Icon(Icons.format_bold), + onPressed: () { + TextSelection theSelection = + textEditingController.selection; + addTagAtSelection(textEditingController, + theSelection.start, theSelection.end, 'b'); + }, + ), + IconButton( + icon: Icon(Icons.format_italic), + onPressed: () { + TextSelection theSelection = + textEditingController.selection; + addTagAtSelection(textEditingController, + theSelection.start, theSelection.end, 'i'); + }, + ), + IconButton( + icon: Icon(Icons.format_underlined), + onPressed: () { + TextSelection theSelection = + textEditingController.selection; + addTagAtSelection(textEditingController, + theSelection.start, theSelection.end, 'u'); + }, + ), + IconButton( + icon: Icon(Icons.code), + onPressed: () { + TextSelection theSelection = + textEditingController.selection; + addTagAtSelection(textEditingController, + theSelection.start, theSelection.end, 'code'); + }, + ), + IconButton( + icon: Icon(Icons.link), + onPressed: () { + addLinkDialog(textEditingController); + }, + ), + ], + ), + ), + ], + ), + ); + }).then((newNode) { + setState(() { + if (newNode != null) { + int index = this.document.document.nodes.indexOf(node); + this.document.document.nodes[index] = newNode; + } + }); + }); + } + + void editImageDialog(slateObject, SlateNode node) async { + TextEditingController imgurlController = + TextEditingController(text: node.data.src); + await showDialog( + context: context, + builder: (BuildContext context) { + return new AlertDialog( + contentPadding: const EdgeInsets.all(16.0), + content: new Row( + children: [ + new Expanded( + child: new TextField( + autofocus: true, + keyboardType: TextInputType.url, + controller: imgurlController, + decoration: new InputDecoration(labelText: 'Image url'), + ), + ) + ], + ), + actions: [ + new FlatButton( + child: const Text('Remove'), + onPressed: () { + setState(() { + int index = this.document.document.nodes.indexOf(node); + this.document.document.nodes.removeAt(index); + }); + Navigator.of(context, rootNavigator: true).pop(); + }), + new FlatButton( + child: const Text('Update'), + onPressed: () { + setState(() { + int index = this.document.document.nodes.indexOf(node); + this.document.document.nodes[index].data.src = + imgurlController.text; + }); + Navigator.of(context, rootNavigator: true).pop(); + }) + ], + ); + }, + ); + } + @override Widget build(BuildContext wcontext) { return DefaultTabController( @@ -346,128 +556,27 @@ class _NewPostScreenState extends State { child: TabBarView( physics: NeverScrollableScrollPhysics(), children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Expanded( - child: Container( - padding: EdgeInsets.all(25), - child: TextField( - controller: controller, - focusNode: textFocusNode, - maxLines: null, - keyboardType: TextInputType.multiline, - textCapitalization: TextCapitalization.sentences, - onChanged: (text) { - setState(() { - document = BBCodeHandler().parse(text, - this.widget.thread, this.widget.replyList); - }); - }, - ), - ), - ), - Container( - color: Colors.grey[600], - padding: EdgeInsets.all(0), - child: Wrap( - children: [ - IconButton( - icon: Icon(Icons.format_bold), - onPressed: () { - TextSelection theSelection = controller.selection; - addTagAtSelection( - theSelection.start, theSelection.end, 'b'); - }, - ), - IconButton( - icon: Icon(Icons.format_italic), - onPressed: () { - TextSelection theSelection = controller.selection; - addTagAtSelection( - theSelection.start, theSelection.end, 'i'); - }, - ), - IconButton( - icon: Icon(Icons.format_underlined), - onPressed: () { - TextSelection theSelection = controller.selection; - addTagAtSelection( - theSelection.start, theSelection.end, 'u'); - }, - ), - IconButton( - icon: Icon(Icons.code), - onPressed: () { - TextSelection theSelection = controller.selection; - addTagAtSelection( - theSelection.start, theSelection.end, 'code'); - }, - ), - IconButton( - icon: Icon(Icons.title), - onPressed: () { - TextSelection theSelection = controller.selection; - addTagAtSelection( - theSelection.start, theSelection.end, 'h1'); - }, - ), - IconButton( - icon: Icon(Icons.format_size), - onPressed: () { - TextSelection theSelection = controller.selection; - addTagAtSelection( - theSelection.start, theSelection.end, 'h2'); - }, - ), - IconButton( - icon: Icon(Icons.format_quote), - onPressed: () { - TextSelection theSelection = controller.selection; - addTagAtSelection( - theSelection.start, theSelection.end, 'blockquote'); - }, - ), - IconButton( - icon: Icon(Icons.image), - onPressed: () { - addImageDialog(); - }, - ), - IconButton( - icon: Icon(Icons.link), - onPressed: () { - addLinkDialog(); - }, - ), - IconButton( - icon: Icon(Icons.ondemand_video), - onPressed: () { - addYoutubeVideoDialog(); - }, - ), - IconButton( - icon: Icon(Icons.videocam), - onPressed: () { - addVideoDialog(); - }, - ), - if (this.widget.replyList.length > 0) - Builder( - builder: (BuildContext bcontext) { - return IconButton( - tooltip: 'Insert userquote', - icon: Icon(Icons.message), - onPressed: () { - addUserquoteDialog(bcontext); - }, - ); - }, - ), - ], - ), - ), - ], + PostEditor( + document: document, + // Blocks + onTapTextBlock: showTextEditDialog, + onTapImageBlock: editImageDialog, + // Toolbar + onTapAddTextBlock: addTextBlock, + onTapAddHeadingOne: () => addHeadingBlock('one'), + onTapAddHeadingTwo: () => addHeadingBlock('two'), + onTapAddImage: addImageDialog, + onReorderHandler: (int oldIndex, int newIndex) { + if (oldIndex < newIndex) { + // removing the item at oldIndex will shorten the list by 1. + newIndex -= 1; + } + setState(() { + final SlateNode element = + document.document.nodes.removeAt(oldIndex); + document.document.nodes.insert(newIndex, element); + }); + }, ), Container( child: SingleChildScrollView( @@ -480,6 +589,189 @@ class _NewPostScreenState extends State { onPressSpoiler: (content) { onPressSpoiler(context, content); }, + paragraphHandler: (SlateNode node, Function leafHandler) { + List lines = List(); + + // Handle block nodes + node.nodes.forEach((line) { + if (line.leaves != null) { + lines.addAll(leafHandler(line.leaves)); + } + + // Handle inline element + if (line.object == 'inline') { + // Handle links + if (line.type == 'link') { + line.nodes.forEach((inlineNode) { + inlineNode.leaves.forEach((leaf) { + lines.add(TextSpan( + text: leaf.text, + style: TextStyle(color: Colors.blue), + )); + }); + }); + } else { + line.nodes.forEach((inlineNode) { + inlineNode.leaves.forEach((leaf) { + lines.add(TextSpan(text: leaf.text)); + }); + }); + } + } + }); + + return Container( + child: RichText( + text: TextSpan(children: lines), + ), + ); + }, + headingHandler: (SlateNode node, Function inlineHandler, + Function leafHandler) { + List lines = List(); + + // Handle block nodes + node.nodes.forEach((line) { + if (line.leaves != null) { + double headingSize = 14.0; + + if (node.type.contains('-one')) { + headingSize = 30.0; + } + + if (node.type.contains('-two')) { + headingSize = 20.0; + } + + // Handle node leaves + lines.addAll(leafHandler(line.leaves, + fontSize: headingSize)); + } + + // Handle inline element + if (line.object == 'inline') { + // Handle links + inlineHandler(node, line); + } + }); + + return Container( + margin: EdgeInsets.only(bottom: 10), + child: RichText( + text: TextSpan(children: lines), + ), + ); + }, + imageWidgetHandler: + (String imageUrl, slateObject, SlateNode node) { + return Container( + margin: EdgeInsets.only(top: 10.0, bottom: 10.0), + child: LimitedBox( + maxHeight: 300, + child: ImageWidget( + url: imageUrl, slateObject: slateObject), + ), + ); + }, + videoWidgetHandler: (String videoUrl) { + return VideoElement( + url: videoUrl, + scaffoldKey: this._scaffoldKey, + ); + }, + youTubeWidgetHandler: (String youTubeUrl) { + return YoutubeVideoEmbed( + url: youTubeUrl, + ); + }, + twitterEmbedHandler: (String embedUrl) { + return EmbedWidget( + url: embedUrl, + ); + }, + userQuoteHandler: (String username, List widgets, + bool isChild) { + return UserQuoteWidget( + username: username, + children: widgets, + isChild: isChild, + ); + }, + bulletedListHandler: (List listItemsContent) { + List listItems = List(); + // Handle block nodes + listItemsContent.forEach((item) { + listItems.add( + Container( + margin: EdgeInsets.only(bottom: 5.0), + child: Row( + children: [ + Container( + margin: EdgeInsets.only(right: 10.0), + height: 5.0, + width: 5.0, + decoration: new BoxDecoration( + color: Theme.of(context) + .textTheme + .body1 + .color, + shape: BoxShape.circle, + ), + ), + Expanded(child: item) + ], + ), + ), + ); + }); + + return Container( + margin: EdgeInsets.only(top: 10, bottom: 10), + child: Column(children: listItems), + ); + }, + numberedListHandler: (List listItemsContent) { + List listItems = List(); + // Handle block nodes + listItemsContent.forEach((item) { + listItems.add( + Container( + margin: EdgeInsets.only(bottom: 5.0), + child: Row( + children: [ + Container( + margin: EdgeInsets.only(right: 10.0), + child: Text( + (listItems.length + 1).toString(), + ), + ), + Expanded( + child: item, + ) + ], + ), + ), + ); + }); + + return Container( + margin: EdgeInsets.only(bottom: 10), + child: Column(children: listItems), + ); + }, + quotesHandler: (Widget content) { + return Container( + margin: EdgeInsets.only(bottom: 10.0), + padding: EdgeInsets.all(10.0), + decoration: BoxDecoration( + border: Border( + left: BorderSide(color: Colors.blue, width: 3.0), + ), + color: Colors.grey, + ), + child: content, + ); + }, ), ), ), diff --git a/lib/screens/thread.dart b/lib/screens/thread.dart index 2cb4262..f279a19 100644 --- a/lib/screens/thread.dart +++ b/lib/screens/thread.dart @@ -6,7 +6,6 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:knocky/helpers/api.dart'; import 'package:after_layout/after_layout.dart'; import 'package:knocky/models/thread.dart'; -import 'package:knocky/screens/editPost.dart'; import 'package:knocky/widget/Thread/ThreadPostItem.dart'; import 'package:knocky/widget/KnockoutLoadingIndicator.dart'; import 'package:numberpicker/numberpicker.dart'; @@ -495,6 +494,7 @@ class _ThreadScreenState extends State } void onTapEditPost (BuildContext context, ThreadPost post) async { + /* final result = await Navigator.push( context, MaterialPageRoute( @@ -514,6 +514,7 @@ class _ThreadScreenState extends State print('Do the scroll'); scrollController.jumpTo(scrollController.position.maxScrollExtent); } + */ } @override diff --git a/lib/widget/PostEditor.dart b/lib/widget/PostEditor.dart new file mode 100644 index 0000000..47b4c55 --- /dev/null +++ b/lib/widget/PostEditor.dart @@ -0,0 +1,220 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:knocky/models/slateDocument.dart'; +import 'package:knocky/widget/SlateDocumentParser/SlateDocumentParser.dart'; +import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; + +class PostEditor extends StatefulWidget { + final SlateObject document; + final Function onTapTextBlock; + final Function onTapImageBlock; + final Function onTapVideoBlock; + final Function onTapYouTubeBlock; + final Function onTapQuoteBlock; + final Function onTapListBlock; + final Function onTapUserQuoteBlock; + final Function onTapTwitterBlock; + + // Toolbar callbacks + final Function onTapAddTextBlock; + final Function onTapAddHeadingOne; + final Function onTapAddHeadingTwo; + final Function onTapAddImage; + final Function onTapAddQuote; + final Function onTapAddYouTubeVideo; + final Function onTapAddVideo; + final Function onReorderHandler; + + PostEditor( + {this.document, + this.onTapTextBlock, + this.onTapImageBlock, + this.onTapListBlock, + this.onTapQuoteBlock, + this.onTapTwitterBlock, + this.onTapUserQuoteBlock, + this.onTapVideoBlock, + this.onTapYouTubeBlock, + this.onTapAddHeadingOne, + this.onTapAddHeadingTwo, + this.onTapAddImage, + this.onTapAddQuote, + this.onTapAddTextBlock, + this.onTapAddVideo, + this.onTapAddYouTubeVideo, + this.onReorderHandler}); + + @override + _PostEditorState createState() => _PostEditorState(); +} + +class _PostEditorState extends State { + List editorContent() { + return SlateDocumentParser( + slateObject: this.widget.document, + context: context, + paragraphHandler: (SlateNode node, Function leafHandler) { + List lines = List(); + + // Handle block nodes + node.nodes.forEach((line) { + if (line.leaves != null) { + lines.addAll(leafHandler(line.leaves)); + } + + // Handle inline element + if (line.object == 'inline') { + // Handle links + if (line.type == 'link') { + line.nodes.forEach((inlineNode) { + inlineNode.leaves.forEach((leaf) { + lines.add(TextSpan( + text: leaf.text, + style: TextStyle(color: Colors.blue), + )); + }); + }); + } else { + line.nodes.forEach((inlineNode) { + inlineNode.leaves.forEach((leaf) { + lines.add(TextSpan(text: leaf.text)); + }); + }); + } + } + }); + + return ListTile( + leading: Icon(Icons.text_format), + onTap: () { + print(node); + this.widget.onTapTextBlock(context, node); + }, + title: Container( + margin: EdgeInsets.only(bottom: 5), + child: RichText( + text: lines.length > 0 + ? TextSpan(children: lines) + : TextSpan(text: '- empty text block -')), + ), + ); + }, + headingHandler: + (SlateNode node, Function inlineHandler, Function leafHandler) { + List lines = List(); + + // Handle block nodes + node.nodes.forEach((line) { + if (line.leaves != null) { + double headingSize = 14.0; + + if (node.type.contains('-one')) { + headingSize = 30.0; + } + + if (node.type.contains('-two')) { + headingSize = 20.0; + } + + // Handle node leaves + lines.addAll(leafHandler(line.leaves, fontSize: headingSize)); + } + + // Handle inline element + if (line.object == 'inline') { + // Handle links + inlineHandler(node, line); + } + }); + + return ListTile( + leading: Icon(node.type.contains('-one') ? MdiIcons.formatHeader1 : MdiIcons.formatHeader2), + onTap: () { + this.widget.onTapTextBlock(context, node); + }, + title: Container( + margin: EdgeInsets.only(bottom: 10), + child: RichText( + text: lines.length > 0 + ? TextSpan(children: lines) + : TextSpan(text: '- empty heading block -'), + ), + ), + ); + }, + imageWidgetHandler: (String imageUrl, slateObject, SlateNode node) { + return ListTile( + leading: Icon(Icons.image), + onTap: () { + this.widget.onTapImageBlock(slateObject, node); + }, + title: Container( + margin: EdgeInsets.only(top: 10.0, bottom: 10.0), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [Text('Image block'), Text(imageUrl)], + ), + ), + ); + }, + ) + .asWidgetList() + .map((widget) => Container( + key: ValueKey(widget), + child: widget, + )) + .toList(); + } + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Expanded( + child: ReorderableListView( + onReorder: this.widget.onReorderHandler, + children: editorContent(), + ), + ), + Container( + color: Colors.grey[600], + padding: EdgeInsets.all(0), + child: Wrap( + children: [ + IconButton( + icon: Icon(MdiIcons.textbox), + onPressed: this.widget.onTapAddTextBlock, + ), + IconButton( + icon: Icon(MdiIcons.formatHeader1), + onPressed: this.widget.onTapAddHeadingOne, + ), + IconButton( + icon: Icon(MdiIcons.formatHeader2), + onPressed: this.widget.onTapAddHeadingTwo, + ), + IconButton( + icon: Icon(Icons.format_quote), + onPressed: this.widget.onTapAddQuote, + ), + IconButton( + icon: Icon(Icons.image), + onPressed: this.widget.onTapAddImage, + ), + IconButton( + icon: Icon(Icons.ondemand_video), + onPressed: this.widget.onTapAddYouTubeVideo, + ), + IconButton( + icon: Icon(Icons.videocam), + onPressed: this.widget.onTapAddVideo, + ), + ], + ), + ), + ], + ); + } +} diff --git a/lib/widget/SlateDocumentParser/SlateDocumentController.dart b/lib/widget/SlateDocumentParser/SlateDocumentController.dart new file mode 100644 index 0000000..6f1e260 --- /dev/null +++ b/lib/widget/SlateDocumentParser/SlateDocumentController.dart @@ -0,0 +1,7 @@ +import 'package:flutter/material.dart'; + +class SlateDocumentController { + List widgetList = List(); + + SlateDocumentController({this.widgetList}); +} diff --git a/lib/widget/SlateDocumentParser/SlateDocumentParser.dart b/lib/widget/SlateDocumentParser/SlateDocumentParser.dart index a6b3c09..3de96ba 100644 --- a/lib/widget/SlateDocumentParser/SlateDocumentParser.dart +++ b/lib/widget/SlateDocumentParser/SlateDocumentParser.dart @@ -1,9 +1,7 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:knocky/models/slateDocument.dart'; -import 'package:intent/intent.dart' as Intent; -import 'package:intent/action.dart' as Action; -import 'package:knocky/widget/Thread/PostElements/UserQuote.dart'; +import 'package:knocky/widget/SlateDocumentParser/SlateDocumentController.dart'; class SlateDocumentParser extends StatelessWidget { final SlateObject slateObject; @@ -18,13 +16,17 @@ class SlateDocumentParser extends StatelessWidget { final Function bulletedListHandler; final Function numberedListHandler; final Function quotesHandler; - + final Function paragraphHandler; + final Function headingHandler; + final SlateDocumentController slateDocumentController; + final bool asListView; SlateDocumentParser({ this.slateObject, this.onPressSpoiler, this.scaffoldkey, this.context, + this.slateDocumentController, @required this.imageWidgetHandler, @required this.videoWidgetHandler, @required this.youTubeWidgetHandler, @@ -33,51 +35,13 @@ class SlateDocumentParser extends StatelessWidget { @required this.bulletedListHandler, @required this.numberedListHandler, @required this.quotesHandler, + @required this.paragraphHandler, + @required this.headingHandler, + this.asListView, }); Widget paragraphToWidget(SlateNode node) { - List lines = List(); - - // Handle block nodes - node.nodes.forEach((line) { - if (line.leaves != null) { - lines.addAll(leafHandler(line.leaves)); - } - - // Handle inline element - if (line.object == 'inline') { - // Handle links - if (line.type == 'link') { - line.nodes.forEach((inlineNode) { - inlineNode.leaves.forEach((leaf) { - lines.add(TextSpan( - text: leaf.text, - style: TextStyle(color: Colors.blue), - recognizer: TapGestureRecognizer() - ..onTap = () { - Intent.Intent() - ..setAction(Action.Action.ACTION_VIEW) - ..setData(Uri.parse(line.data.href)) - ..startActivity().catchError((e) => print(e)); - print('Clicked link: ' + line.data.href); - })); - }); - }); - } else { - line.nodes.forEach((inlineNode) { - inlineNode.leaves.forEach((leaf) { - lines.add(TextSpan(text: leaf.text)); - }); - }); - } - } - }); - - return Container( - child: RichText( - text: TextSpan(children: lines), - ), - ); + return this.paragraphHandler(node, leafHandler); } List leafHandler(List leaves, {double fontSize = 14.0}) { @@ -178,38 +142,7 @@ class SlateDocumentParser extends StatelessWidget { } Widget headingToWidget(SlateNode node) { - List lines = List(); - - // Handle block nodes - node.nodes.forEach((line) { - if (line.leaves != null) { - double headingSize = 14.0; - - if (node.type.contains('-one')) { - headingSize = 30.0; - } - - if (node.type.contains('-two')) { - headingSize = 20.0; - } - - // Handle node leaves - lines.addAll(leafHandler(line.leaves, fontSize: headingSize)); - } - - // Handle inline element - if (line.object == 'inline') { - // Handle links - inlineHandler(node, line); - } - }); - - return Container( - margin: EdgeInsets.only(bottom: 10), - child: RichText( - text: TextSpan(children: lines), - ), - ); + return this.headingHandler(node, inlineHandler, leafHandler); } Widget userquoteToWidget(SlateNode node, {bool isChild = false}) { @@ -230,7 +163,7 @@ class SlateDocumentParser extends StatelessWidget { } Widget handleImage(SlateNode node) { - return this.imageWidgetHandler(node.data.src, slateObject); + return this.imageWidgetHandler(node.data.src, slateObject, node); } Widget handleVideo(SlateNode node) { @@ -296,12 +229,20 @@ class SlateDocumentParser extends StatelessWidget { return widgets; } + List asWidgetList() { + return handleNodes(slateObject.document.nodes); + } + + + @override Widget build(BuildContext context) { return Container( - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: handleNodes(slateObject.document.nodes))); + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: handleNodes(slateObject.document.nodes), + ), + ); } } diff --git a/lib/widget/Thread/ThreadPostItem.dart b/lib/widget/Thread/ThreadPostItem.dart index 98b0087..c5f9cd0 100644 --- a/lib/widget/Thread/ThreadPostItem.dart +++ b/lib/widget/Thread/ThreadPostItem.dart @@ -1,4 +1,6 @@ +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; +import 'package:knocky/models/slateDocument.dart'; import 'package:knocky/models/thread.dart'; import 'package:knocky/widget/SlateDocumentParser/SlateDocumentParser.dart'; import 'package:knocky/helpers/icons.dart'; @@ -14,6 +16,8 @@ import 'package:knocky/state/authentication.dart'; import 'package:knocky/widget/Thread/RatePostContent.dart'; import 'package:knocky/widget/Thread/ViewUsersOfRatingsContent.dart'; import 'package:knocky/widget/Thread/PostBan.dart'; +import 'package:intent/intent.dart' as Intent; +import 'package:intent/action.dart' as Action; class ThreadPostItem extends StatelessWidget { final ThreadPost postDetails; @@ -195,7 +199,88 @@ class ThreadPostItem extends StatelessWidget { onPressSpoiler(context, text); }, context: context, - imageWidgetHandler: (String imageUrl, slateObject) { + paragraphHandler: (SlateNode node, Function leafHandler) { + List lines = List(); + + // Handle block nodes + node.nodes.forEach((line) { + if (line.leaves != null) { + lines.addAll(leafHandler(line.leaves)); + } + + // Handle inline element + if (line.object == 'inline') { + // Handle links + if (line.type == 'link') { + line.nodes.forEach((inlineNode) { + inlineNode.leaves.forEach((leaf) { + lines.add(TextSpan( + text: leaf.text, + style: TextStyle(color: Colors.blue), + recognizer: TapGestureRecognizer() + ..onTap = () { + Intent.Intent() + ..setAction(Action.Action.ACTION_VIEW) + ..setData(Uri.parse(line.data.href)) + ..startActivity() + .catchError((e) => print(e)); + print('Clicked link: ' + line.data.href); + })); + }); + }); + } else { + line.nodes.forEach((inlineNode) { + inlineNode.leaves.forEach((leaf) { + lines.add(TextSpan(text: leaf.text)); + }); + }); + } + } + }); + + return Container( + child: RichText( + text: TextSpan(children: lines), + ), + ); + }, + headingHandler: (SlateNode node, Function inlineHandler, + Function leafHandler) { + List lines = List(); + + // Handle block nodes + node.nodes.forEach((line) { + if (line.leaves != null) { + double headingSize = 14.0; + + if (node.type.contains('-one')) { + headingSize = 30.0; + } + + if (node.type.contains('-two')) { + headingSize = 20.0; + } + + // Handle node leaves + lines.addAll( + leafHandler(line.leaves, fontSize: headingSize)); + } + + // Handle inline element + if (line.object == 'inline') { + // Handle links + inlineHandler(node, line); + } + }); + + return Container( + margin: EdgeInsets.only(bottom: 10), + child: RichText( + text: TextSpan(children: lines), + ), + ); + }, + imageWidgetHandler: (String imageUrl, slateObject, SlateNode node) { return Container( margin: EdgeInsets.only(top: 10.0, bottom: 10.0), child: LimitedBox( diff --git a/pubspec.yaml b/pubspec.yaml index 5dfb30a..18792dc 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -49,6 +49,7 @@ dependencies: url: https://github.com/leisim/hive.git path: hive quick_actions: ^0.3.2+2 + material_design_icons_flutter: 3.2.3895 flutter: sdk: flutter From c646c6fa84baa4dfddce5c8a658a09153eeb1bca Mon Sep 17 00:00:00 2001 From: dasmikko Date: Thu, 8 Aug 2019 08:22:38 +0200 Subject: [PATCH 07/19] Add more handlers for the blocks --- lib/screens/newPost.dart | 189 +++++++++++++++--- lib/widget/PostEditor.dart | 67 ++++++- .../SlateDocumentParser.dart | 8 +- lib/widget/Thread/ThreadPostItem.dart | 42 +++- 4 files changed, 267 insertions(+), 39 deletions(-) diff --git a/lib/screens/newPost.dart b/lib/screens/newPost.dart index 915b6b8..01d8d03 100644 --- a/lib/screens/newPost.dart +++ b/lib/screens/newPost.dart @@ -108,8 +108,8 @@ class _NewPostScreenState extends State { void addImageDialog() async { ClipboardData clipBoardText = await Clipboard.getData('text/plain'); - TextEditingController imgurlController = - TextEditingController(text: clipBoardText.text); + TextEditingController imgurlController = TextEditingController( + text: clipBoardText != null ? clipBoardText.text : ''); await showDialog( context: context, child: new AlertDialog( @@ -227,6 +227,14 @@ class _NewPostScreenState extends State { new FlatButton( child: const Text('Insert'), onPressed: () { + setState(() { + this + .document + .document + .nodes + .add(SlateNode(type: 'youtube', data: SlateNodeData(src: urlController.text))); + }); + Navigator.of(context, rootNavigator: true).pop(); if (controller.text.endsWith('\n') || controller.text.isEmpty) { @@ -249,7 +257,7 @@ class _NewPostScreenState extends State { TextEditingController(text: clipBoardText.text); await showDialog( context: context, - child: new AlertDialog( + builder: (BuildContext context) => new AlertDialog( contentPadding: const EdgeInsets.all(16.0), content: new Row( children: [ @@ -273,16 +281,16 @@ class _NewPostScreenState extends State { new FlatButton( child: const Text('Insert'), onPressed: () { - Navigator.of(context, rootNavigator: true).pop(); + setState(() { + this.document.document.nodes.add( + SlateNode( + type: 'video', + data: SlateNodeData(src: urlController.text), + ), + ); + }); - if (controller.text.endsWith('\n') || controller.text.isEmpty) { - controller.text = - controller.text + '[video]${urlController.text}[/video]'; - } else { - controller.text = controller.text + - '\n[video]${urlController.text}[/video]'; - } - refreshPreview(); + Navigator.of(context, rootNavigator: true).pop(); }) ], ), @@ -370,6 +378,16 @@ class _NewPostScreenState extends State { ); } + void addQuoteBlock() { + setState(() { + this + .document + .document + .nodes + .add(SlateNode(type: 'block-quote', nodes: List())); + }); + } + /* Block tap callbacks */ @@ -528,6 +546,96 @@ class _NewPostScreenState extends State { ); } + void editYoutubeVideoDialog(String youTubeUrl, SlateNode node) async { + TextEditingController urlController = + TextEditingController(text: node.data.src); + await showDialog( + context: context, + builder: (BuildContext context) => AlertDialog( + contentPadding: const EdgeInsets.all(16.0), + content: new Row( + children: [ + new Expanded( + child: new TextField( + autofocus: true, + keyboardType: TextInputType.url, + controller: urlController, + decoration: new InputDecoration(labelText: 'YouTube URL'), + ), + ) + ], + ), + actions: [ + new FlatButton( + child: const Text('Remove'), + onPressed: () { + setState(() { + int index = this.document.document.nodes.indexOf(node); + this.document.document.nodes.removeAt(index); + }); + Navigator.of(context, rootNavigator: true).pop(); + }), + new FlatButton( + child: const Text('Update'), + onPressed: () { + setState(() { + int index = this.document.document.nodes.indexOf(node); + this.document.document.nodes[index].data.src = + urlController.text; + }); + Navigator.of(context, rootNavigator: true).pop(); + }) + ], + ), + ); + } + + void editVideoDialog() async { + ClipboardData clipBoardText = await Clipboard.getData('text/plain'); + TextEditingController urlController = + TextEditingController(text: clipBoardText.text); + await showDialog( + context: context, + builder: (BuildContext context) => new AlertDialog( + contentPadding: const EdgeInsets.all(16.0), + content: new Row( + children: [ + new Expanded( + child: new TextField( + autofocus: true, + keyboardType: TextInputType.url, + controller: urlController, + decoration: + new InputDecoration(labelText: 'Video URL (Webm/mp4)'), + ), + ) + ], + ), + actions: [ + new FlatButton( + child: const Text('Cancel'), + onPressed: () { + Navigator.of(context, rootNavigator: true).pop(); + }), + new FlatButton( + child: const Text('Insert'), + onPressed: () { + setState(() { + this.document.document.nodes.add( + SlateNode( + type: 'video', + data: SlateNodeData(src: urlController.text), + ), + ); + }); + + Navigator.of(context, rootNavigator: true).pop(); + }) + ], + ), + ); + } + @override Widget build(BuildContext wcontext) { return DefaultTabController( @@ -559,13 +667,18 @@ class _NewPostScreenState extends State { PostEditor( document: document, // Blocks - onTapTextBlock: showTextEditDialog, - onTapImageBlock: editImageDialog, + onTapTextBlock: this.showTextEditDialog, + onTapImageBlock: this.editImageDialog, + onTapQuoteBlock: this.showTextEditDialog, + onTapYouTubeBlock: this.editYoutubeVideoDialog, // Toolbar - onTapAddTextBlock: addTextBlock, - onTapAddHeadingOne: () => addHeadingBlock('one'), - onTapAddHeadingTwo: () => addHeadingBlock('two'), - onTapAddImage: addImageDialog, + onTapAddTextBlock: this.addTextBlock, + onTapAddHeadingOne: () => this.addHeadingBlock('one'), + onTapAddHeadingTwo: () => this.addHeadingBlock('two'), + onTapAddImage: this.addImageDialog, + onTapAddQuote: this.addQuoteBlock, + onTapAddYouTubeVideo: this.addYoutubeVideoDialog, + onTapAddVideo: this.addVideoDialog, onReorderHandler: (int oldIndex, int newIndex) { if (oldIndex < newIndex) { // removing the item at oldIndex will shorten the list by 1. @@ -673,13 +786,14 @@ class _NewPostScreenState extends State { ), ); }, - videoWidgetHandler: (String videoUrl) { + videoWidgetHandler: (SlateNode node) { return VideoElement( - url: videoUrl, + url: node.data.src, scaffoldKey: this._scaffoldKey, ); }, - youTubeWidgetHandler: (String youTubeUrl) { + youTubeWidgetHandler: + (String youTubeUrl, SlateNode node) { return YoutubeVideoEmbed( url: youTubeUrl, ); @@ -759,7 +873,34 @@ class _NewPostScreenState extends State { child: Column(children: listItems), ); }, - quotesHandler: (Widget content) { + quotesHandler: (SlateNode node, Function inlineHandler, + Function leafHandler) { + List lines = List(); + // Handle block nodes + node.nodes.forEach((line) { + if (line.leaves != null) { + double headingSize = 14.0; + + if (node.type.contains('-one')) { + headingSize = 30.0; + } + + if (node.type.contains('-two')) { + headingSize = 20.0; + } + + // Handle node leaves + lines.addAll(leafHandler(line.leaves, + fontSize: headingSize)); + } + + // Handle inline element + if (line.object == 'inline') { + // Handle links + inlineHandler(node, line); + } + }); + return Container( margin: EdgeInsets.only(bottom: 10.0), padding: EdgeInsets.all(10.0), @@ -769,7 +910,9 @@ class _NewPostScreenState extends State { ), color: Colors.grey, ), - child: content, + child: RichText( + text: TextSpan(children: lines), + ), ); }, ), diff --git a/lib/widget/PostEditor.dart b/lib/widget/PostEditor.dart index 47b4c55..b402bc3 100644 --- a/lib/widget/PostEditor.dart +++ b/lib/widget/PostEditor.dart @@ -49,6 +49,58 @@ class PostEditor extends StatefulWidget { } class _PostEditorState extends State { + Widget _quoteHandler( + SlateNode node, Function inlineHandler, Function leafHandler) { + List lines = List(); + // Handle block nodes + node.nodes.forEach((line) { + if (line.leaves != null) { + double headingSize = 14.0; + + if (node.type.contains('-one')) { + headingSize = 30.0; + } + + if (node.type.contains('-two')) { + headingSize = 20.0; + } + + // Handle node leaves + lines.addAll(leafHandler(line.leaves, fontSize: headingSize)); + } + + // Handle inline element + if (line.object == 'inline') { + // Handle links + inlineHandler(node, line); + } + }); + + return ListTile( + onTap: () => this.widget.onTapQuoteBlock(context, node), + leading: Icon(Icons.format_quote), + title: RichText( + text: TextSpan(children: lines.length > 0 ? lines : [TextSpan(text: '- empty quote')]), + ), + ); + } + + Widget _youtubeHandler (String youTubeUrl, SlateNode node) { + return ListTile( + onTap: () => this.widget.onTapYouTubeBlock(youTubeUrl, node), + leading: Icon(Icons.ondemand_video), + title: Text(youTubeUrl), + ); + } + + Widget _videoHandler (SlateNode node) { + return ListTile( + onTap: () => this.widget.onTapVideoBlock(node), + leading: Icon(Icons.videocam), + title: Text(node.data.src), + ); + } + List editorContent() { return SlateDocumentParser( slateObject: this.widget.document, @@ -128,7 +180,9 @@ class _PostEditorState extends State { }); return ListTile( - leading: Icon(node.type.contains('-one') ? MdiIcons.formatHeader1 : MdiIcons.formatHeader2), + leading: Icon(node.type.contains('-one') + ? MdiIcons.formatHeader1 + : MdiIcons.formatHeader2), onTap: () { this.widget.onTapTextBlock(context, node); }, @@ -151,13 +205,16 @@ class _PostEditorState extends State { title: Container( margin: EdgeInsets.only(top: 10.0, bottom: 10.0), child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [Text('Image block'), Text(imageUrl)], - ), + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [Text('Image block'), Text(imageUrl)], + ), ), ); }, + quotesHandler: _quoteHandler, + youTubeWidgetHandler: this._youtubeHandler, + videoWidgetHandler: this._videoHandler, ) .asWidgetList() .map((widget) => Container( diff --git a/lib/widget/SlateDocumentParser/SlateDocumentParser.dart b/lib/widget/SlateDocumentParser/SlateDocumentParser.dart index 3de96ba..8f5251f 100644 --- a/lib/widget/SlateDocumentParser/SlateDocumentParser.dart +++ b/lib/widget/SlateDocumentParser/SlateDocumentParser.dart @@ -155,11 +155,11 @@ class SlateDocumentParser extends StatelessWidget { } Widget youTubeToWidget(SlateNode node) { - return this.youTubeWidgetHandler(node.data.src); + return this.youTubeWidgetHandler(node.data.src, node); } Widget handleQuotes(SlateNode node) { - return this.quotesHandler(paragraphToWidget(node)); + return this.quotesHandler(node, inlineHandler, leafHandler); } Widget handleImage(SlateNode node) { @@ -167,7 +167,7 @@ class SlateDocumentParser extends StatelessWidget { } Widget handleVideo(SlateNode node) { - return this.videoWidgetHandler(node.data.src); + return this.videoWidgetHandler(node); } List handleNodes(List nodes, {bool isChild = false}) { @@ -233,8 +233,6 @@ class SlateDocumentParser extends StatelessWidget { return handleNodes(slateObject.document.nodes); } - - @override Widget build(BuildContext context) { return Container( diff --git a/lib/widget/Thread/ThreadPostItem.dart b/lib/widget/Thread/ThreadPostItem.dart index c5f9cd0..697675a 100644 --- a/lib/widget/Thread/ThreadPostItem.dart +++ b/lib/widget/Thread/ThreadPostItem.dart @@ -280,7 +280,8 @@ class ThreadPostItem extends StatelessWidget { ), ); }, - imageWidgetHandler: (String imageUrl, slateObject, SlateNode node) { + imageWidgetHandler: + (String imageUrl, slateObject, SlateNode node) { return Container( margin: EdgeInsets.only(top: 10.0, bottom: 10.0), child: LimitedBox( @@ -290,13 +291,13 @@ class ThreadPostItem extends StatelessWidget { ), ); }, - videoWidgetHandler: (String videoUrl) { + videoWidgetHandler: (SlateNode node) { return VideoElement( - url: videoUrl, + url: node.data.src, scaffoldKey: this.scaffoldKey, ); }, - youTubeWidgetHandler: (String youTubeUrl) { + youTubeWidgetHandler: (String youTubeUrl, SlateNode node) { return YoutubeVideoEmbed( url: youTubeUrl, ); @@ -373,7 +374,34 @@ class ThreadPostItem extends StatelessWidget { child: Column(children: listItems), ); }, - quotesHandler: (Widget content) { + quotesHandler: (SlateNode node, Function inlineHandler, + Function leafHandler) { + List lines = List(); + // Handle block nodes + node.nodes.forEach((line) { + if (line.leaves != null) { + double headingSize = 14.0; + + if (node.type.contains('-one')) { + headingSize = 30.0; + } + + if (node.type.contains('-two')) { + headingSize = 20.0; + } + + // Handle node leaves + lines.addAll( + leafHandler(line.leaves, fontSize: headingSize)); + } + + // Handle inline element + if (line.object == 'inline') { + // Handle links + inlineHandler(node, line); + } + }); + return Container( margin: EdgeInsets.only(bottom: 10.0), padding: EdgeInsets.all(10.0), @@ -383,7 +411,9 @@ class ThreadPostItem extends StatelessWidget { ), color: Colors.grey, ), - child: content, + child: RichText( + text: TextSpan(children: lines), + ), ); }, ), From 1b5f55efb37a4bc3505d8f9775c6c355b303d3c1 Mon Sep 17 00:00:00 2001 From: dasmikko Date: Thu, 8 Aug 2019 21:11:24 +0200 Subject: [PATCH 08/19] bunch of editor changes --- lib/helpers/bbcode.dart | 97 ----- lib/screens/newPost.dart | 330 ++++------------- lib/widget/ListEditor.dart | 343 ++++++++++++++++++ lib/widget/PostEditor.dart | 338 +++++++++++------ .../SlateDocumentParser.dart | 4 +- lib/widget/Thread/PostContent.dart | 245 +++++++++++++ lib/widget/Thread/ThreadPostItem.dart | 242 +----------- 7 files changed, 895 insertions(+), 704 deletions(-) create mode 100644 lib/widget/ListEditor.dart create mode 100644 lib/widget/Thread/PostContent.dart diff --git a/lib/helpers/bbcode.dart b/lib/helpers/bbcode.dart index f3dde14..b2bac19 100644 --- a/lib/helpers/bbcode.dart +++ b/lib/helpers/bbcode.dart @@ -166,103 +166,6 @@ class BBCodeHandler implements bbob.NodeVisitor { } } - // Handle blocks - /*switch (node.type) { - case 'paragraph': - - content.write('\n'); - break; - case 'heading-one': - node.nodes.forEach((line) { - content.write('[h1]'); - if (line.leaves != null) { - // Handle node leaves - content.write(_leafHandler(line.leaves)); - } - - // Handle inline element - if (line.object == 'inline') { - // Handle links - content.write(_inlineHandler(node, line)); - } - }); - content.write('[/h1]\n'); - //widgets.add(headingToWidget(node)); - break; - case 'heading-two': - node.nodes.forEach((line) { - content.write('[h2]'); - if (line.leaves != null) { - // Handle node leaves - content.write(_leafHandler(line.leaves)); - } - - // Handle inline element - if (line.object == 'inline') { - // Handle links - content.write(_inlineHandler(node, line)); - } - }); - content.write('[/h2]\n'); - break; - case 'userquote': - //widgets.add(userquoteToWidget(node, isChild: isChild)); - break; - case 'bulleted-list': - content.write('[ul]'); - List listItemsContent = List(); - listItemsContent.addAll(_handleNodes(node.nodes, asList: true)); - listItemsContent.forEach((item) { - content.write('[li]' + item + '[/li]'); - }); - content.write('[/ul]\n'); - break; - case 'numbered-list': - //widgets.add(numberedListToWidget(node)); - break; - case 'list-item': - node.nodes.asMap().forEach((i, line) { - if (line.leaves != null) { - content.write(_leafHandler(line.leaves)); - } - - // Handle inline element - if (line.object == 'inline') { - // Handle links - if (line.type == 'link') { - line.nodes.forEach((inlineNode) { - inlineNode.leaves.forEach((leaf) { - content.write('[url]' + leaf.text + '[/url]'); - }); - }); - } else { - line.nodes.forEach((inlineNode) { - inlineNode.leaves.forEach((leaf) { - content.write(leaf.text); - }); - }); - } - } - }); - break; - case 'image': - content.write('[img]' + node.data.src + '[/img]\n'); - break; - case 'youtube': - //(widgets.add(youTubeToWidget(node)); - break; - case 'block-quote': - //widgets.add(handleQuotes(node)); - break; - case 'twitter': - //widgets.add(EmbedWidget(url: node.data.src)); - break; - case 'video': - //widgets.add(handleVideo(node)); - break; - } - */ - if (asList) { contentItems.add(content.toString()); content.clear(); diff --git a/lib/screens/newPost.dart b/lib/screens/newPost.dart index 01d8d03..b7f2c87 100644 --- a/lib/screens/newPost.dart +++ b/lib/screens/newPost.dart @@ -1,20 +1,15 @@ -import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:knocky/helpers/bbcode.dart'; import 'package:knocky/models/slateDocument.dart'; import 'package:knocky/models/thread.dart'; +import 'package:knocky/widget/ListEditor.dart'; import 'package:knocky/widget/PostEditor.dart'; -import 'package:knocky/widget/SlateDocumentParser/SlateDocumentParser.dart'; import 'package:knocky/helpers/api.dart'; import 'package:knocky/widget/KnockoutLoadingIndicator.dart'; -import 'package:knocky/widget/Thread/PostElements/Embed.dart'; -import 'package:knocky/widget/Thread/PostElements/Image.dart'; -import 'package:knocky/widget/Thread/PostElements/UserQuote.dart'; -import 'package:knocky/widget/Thread/PostElements/Video.dart'; -import 'package:knocky/widget/Thread/PostElements/YouTubeEmbed.dart'; import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; +import 'package:knocky/widget/Thread/PostContent.dart'; class NewPostScreen extends StatefulWidget { final ThreadPost replyTo; @@ -228,11 +223,9 @@ class _NewPostScreenState extends State { child: const Text('Insert'), onPressed: () { setState(() { - this - .document - .document - .nodes - .add(SlateNode(type: 'youtube', data: SlateNodeData(src: urlController.text))); + this.document.document.nodes.add(SlateNode( + type: 'youtube', + data: SlateNodeData(src: urlController.text))); }); Navigator.of(context, rootNavigator: true).pop(); @@ -388,6 +381,16 @@ class _NewPostScreenState extends State { }); } + void addListBlock(String type) { + setState(() { + this + .document + .document + .nodes + .add(SlateNode(object: 'block', type: type, nodes: List())); + }); + } + /* Block tap callbacks */ @@ -567,24 +570,24 @@ class _NewPostScreenState extends State { ), actions: [ new FlatButton( - child: const Text('Remove'), - onPressed: () { - setState(() { - int index = this.document.document.nodes.indexOf(node); - this.document.document.nodes.removeAt(index); - }); - Navigator.of(context, rootNavigator: true).pop(); - }), - new FlatButton( - child: const Text('Update'), - onPressed: () { - setState(() { - int index = this.document.document.nodes.indexOf(node); - this.document.document.nodes[index].data.src = - urlController.text; - }); - Navigator.of(context, rootNavigator: true).pop(); - }) + child: const Text('Remove'), + onPressed: () { + setState(() { + int index = this.document.document.nodes.indexOf(node); + this.document.document.nodes.removeAt(index); + }); + Navigator.of(context, rootNavigator: true).pop(); + }), + new FlatButton( + child: const Text('Update'), + onPressed: () { + setState(() { + int index = this.document.document.nodes.indexOf(node); + this.document.document.nodes[index].data.src = + urlController.text; + }); + Navigator.of(context, rootNavigator: true).pop(); + }) ], ), ); @@ -636,6 +639,43 @@ class _NewPostScreenState extends State { ); } + void editList(SlateNode node) async { + List listItems = List(); + + node.nodes.forEach((listItem) { + listItems.add(listItem); + }); + + await showDialog( + context: context, + builder: (BuildContext context) => AlertDialog( + title: Text('Edit list'), + contentPadding: const EdgeInsets.all(16.0), + content: ListEditor( + oldListItems: listItems, + onListUpdated: (newListItems) { + setState(() { + int index = this.document.document.nodes.indexOf(node); + this.document.document.nodes[index].nodes = newListItems; + }); + }, + ), + actions: [ + new FlatButton( + child: const Text('Remove'), + onPressed: () { + Navigator.of(context, rootNavigator: true).pop(); + }), + new FlatButton( + child: const Text('Update'), + onPressed: () { + Navigator.of(context, rootNavigator: true).pop(); + }) + ], + ), + ); + } + @override Widget build(BuildContext wcontext) { return DefaultTabController( @@ -671,6 +711,7 @@ class _NewPostScreenState extends State { onTapImageBlock: this.editImageDialog, onTapQuoteBlock: this.showTextEditDialog, onTapYouTubeBlock: this.editYoutubeVideoDialog, + onTapListBlock: this.editList, // Toolbar onTapAddTextBlock: this.addTextBlock, onTapAddHeadingOne: () => this.addHeadingBlock('one'), @@ -679,6 +720,8 @@ class _NewPostScreenState extends State { onTapAddQuote: this.addQuoteBlock, onTapAddYouTubeVideo: this.addYoutubeVideoDialog, onTapAddVideo: this.addVideoDialog, + onTapAddBulletedList: () => this.addListBlock('bulleted-list'), + onTapAddNumberedList: () => this.addListBlock('numbered-list'), onReorderHandler: (int oldIndex, int newIndex) { if (oldIndex < newIndex) { // removing the item at oldIndex will shorten the list by 1. @@ -695,227 +738,12 @@ class _NewPostScreenState extends State { child: SingleChildScrollView( child: Container( padding: EdgeInsets.all(15), - child: SlateDocumentParser( - context: context, - scaffoldkey: _scaffoldKey, - slateObject: document, - onPressSpoiler: (content) { - onPressSpoiler(context, content); - }, - paragraphHandler: (SlateNode node, Function leafHandler) { - List lines = List(); - - // Handle block nodes - node.nodes.forEach((line) { - if (line.leaves != null) { - lines.addAll(leafHandler(line.leaves)); - } - - // Handle inline element - if (line.object == 'inline') { - // Handle links - if (line.type == 'link') { - line.nodes.forEach((inlineNode) { - inlineNode.leaves.forEach((leaf) { - lines.add(TextSpan( - text: leaf.text, - style: TextStyle(color: Colors.blue), - )); - }); - }); - } else { - line.nodes.forEach((inlineNode) { - inlineNode.leaves.forEach((leaf) { - lines.add(TextSpan(text: leaf.text)); - }); - }); - } - } - }); - - return Container( - child: RichText( - text: TextSpan(children: lines), - ), - ); - }, - headingHandler: (SlateNode node, Function inlineHandler, - Function leafHandler) { - List lines = List(); - - // Handle block nodes - node.nodes.forEach((line) { - if (line.leaves != null) { - double headingSize = 14.0; - - if (node.type.contains('-one')) { - headingSize = 30.0; - } - - if (node.type.contains('-two')) { - headingSize = 20.0; - } - - // Handle node leaves - lines.addAll(leafHandler(line.leaves, - fontSize: headingSize)); - } - - // Handle inline element - if (line.object == 'inline') { - // Handle links - inlineHandler(node, line); - } - }); - - return Container( - margin: EdgeInsets.only(bottom: 10), - child: RichText( - text: TextSpan(children: lines), - ), - ); - }, - imageWidgetHandler: - (String imageUrl, slateObject, SlateNode node) { - return Container( - margin: EdgeInsets.only(top: 10.0, bottom: 10.0), - child: LimitedBox( - maxHeight: 300, - child: ImageWidget( - url: imageUrl, slateObject: slateObject), - ), - ); - }, - videoWidgetHandler: (SlateNode node) { - return VideoElement( - url: node.data.src, - scaffoldKey: this._scaffoldKey, - ); - }, - youTubeWidgetHandler: - (String youTubeUrl, SlateNode node) { - return YoutubeVideoEmbed( - url: youTubeUrl, - ); - }, - twitterEmbedHandler: (String embedUrl) { - return EmbedWidget( - url: embedUrl, - ); - }, - userQuoteHandler: (String username, List widgets, - bool isChild) { - return UserQuoteWidget( - username: username, - children: widgets, - isChild: isChild, - ); - }, - bulletedListHandler: (List listItemsContent) { - List listItems = List(); - // Handle block nodes - listItemsContent.forEach((item) { - listItems.add( - Container( - margin: EdgeInsets.only(bottom: 5.0), - child: Row( - children: [ - Container( - margin: EdgeInsets.only(right: 10.0), - height: 5.0, - width: 5.0, - decoration: new BoxDecoration( - color: Theme.of(context) - .textTheme - .body1 - .color, - shape: BoxShape.circle, - ), - ), - Expanded(child: item) - ], - ), - ), - ); - }); - - return Container( - margin: EdgeInsets.only(top: 10, bottom: 10), - child: Column(children: listItems), - ); - }, - numberedListHandler: (List listItemsContent) { - List listItems = List(); - // Handle block nodes - listItemsContent.forEach((item) { - listItems.add( - Container( - margin: EdgeInsets.only(bottom: 5.0), - child: Row( - children: [ - Container( - margin: EdgeInsets.only(right: 10.0), - child: Text( - (listItems.length + 1).toString(), - ), - ), - Expanded( - child: item, - ) - ], - ), - ), - ); - }); - - return Container( - margin: EdgeInsets.only(bottom: 10), - child: Column(children: listItems), - ); - }, - quotesHandler: (SlateNode node, Function inlineHandler, - Function leafHandler) { - List lines = List(); - // Handle block nodes - node.nodes.forEach((line) { - if (line.leaves != null) { - double headingSize = 14.0; - - if (node.type.contains('-one')) { - headingSize = 30.0; - } - - if (node.type.contains('-two')) { - headingSize = 20.0; - } - - // Handle node leaves - lines.addAll(leafHandler(line.leaves, - fontSize: headingSize)); - } - - // Handle inline element - if (line.object == 'inline') { - // Handle links - inlineHandler(node, line); - } - }); - - return Container( - margin: EdgeInsets.only(bottom: 10.0), - padding: EdgeInsets.all(10.0), - decoration: BoxDecoration( - border: Border( - left: BorderSide(color: Colors.blue, width: 3.0), - ), - color: Colors.grey, - ), - child: RichText( - text: TextSpan(children: lines), - ), - ); - }, - ), + child: PostContent( + content: document, + onTapSpoiler: (text) { + onPressSpoiler(context, text); + }, + scaffoldKey: this._scaffoldKey), ), ), ), diff --git a/lib/widget/ListEditor.dart b/lib/widget/ListEditor.dart new file mode 100644 index 0000000..a0cb547 --- /dev/null +++ b/lib/widget/ListEditor.dart @@ -0,0 +1,343 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:knocky/helpers/bbcode.dart'; +import 'package:knocky/models/slateDocument.dart'; +import 'package:knocky/widget/SlateDocumentParser/SlateDocumentParser.dart'; + +class ListEditor extends StatefulWidget { + final List oldListItems; + final Function onTapListItem; + final Function onListUpdated; + + ListEditor({ + this.oldListItems, + this.onTapListItem, + this.onListUpdated, + }); + + @override + _ListEditorState createState() => _ListEditorState(); +} + +class _ListEditorState extends State { + List currentItemList = List(); + + @override + void initState() { + super.initState(); + currentItemList = this.widget.oldListItems; + } + + List leafHandler(List leaves, {double fontSize = 14.0}) { + List lines = List(); + leaves.forEach((leaf) { + bool isBold = leaf.marks.where((mark) => mark.type == 'bold').length > 0; + + bool isItalic = + leaf.marks.where((mark) => mark.type == 'italic').length > 0; + + bool isUnderlined = + leaf.marks.where((mark) => mark.type == 'underlined').length > 0; + + bool isCode = leaf.marks.where((mark) => mark.type == 'code').length > 0; + + bool isSpoiler = + leaf.marks.where((mark) => mark.type == 'spoiler').length > 0; + + TextStyle textStyle = Theme.of(context).textTheme.body1.copyWith( + fontSize: fontSize, + fontFamily: isCode ? 'RobotoMono' : 'Roboto', + decoration: + isUnderlined ? TextDecoration.underline : TextDecoration.none, + fontWeight: isBold ? FontWeight.bold : FontWeight.normal, + fontStyle: isItalic ? FontStyle.italic : FontStyle.normal, + ); + + TextStyle spoilerStyle = textStyle.copyWith( + background: Paint()..color = Theme.of(context).textTheme.body1.color, + color: Theme.of(context).textTheme.body1.color); + + if (isSpoiler) { + lines.add( + TextSpan( + text: leaf.text, + style: spoilerStyle, + ), + ); + } else { + lines.add( + TextSpan( + text: leaf.text, + style: textStyle, + ), + ); + } + }); + + return lines; + } + + Widget listItemTextHandler(SlateNode node) { + List lines = List(); + + // Handle block nodes + node.nodes.forEach((line) { + if (line.leaves != null) { + lines.addAll(this.leafHandler(line.leaves)); + } + + // Handle inline element + if (line.object == 'inline') { + // Handle links + if (line.type == 'link') { + line.nodes.forEach((inlineNode) { + inlineNode.leaves.forEach((leaf) { + lines.add( + TextSpan( + text: leaf.text, + style: TextStyle(color: Colors.blue), + ), + ); + }); + }); + } else { + line.nodes.forEach((inlineNode) { + inlineNode.leaves.forEach((leaf) { + lines.add(TextSpan(text: leaf.text)); + }); + }); + } + } + }); + + return Container( + child: RichText( + text: TextSpan(children: lines), + ), + ); + } + + void showTextEditDialog(BuildContext context, SlateNode node) { + String bbcodeText = BBCodeHandler().slateParagraphToBBCode(node); + TextEditingController textEditingController = + TextEditingController(text: bbcodeText); + + showDialog( + context: context, + builder: (BuildContext bcontext) { + return AlertDialog( + actions: [ + FlatButton( + child: Text('Remove'), + onPressed: () { + setState(() { + int index = this.currentItemList.indexOf(node); + this.currentItemList.removeAt(index); + this.widget.onListUpdated(this.currentItemList); + }); + Navigator.pop(bcontext); + }, + ), + FlatButton( + child: Text('Save'), + onPressed: () { + SlateNode newNode = BBCodeHandler() + .parse(textEditingController.text, type: node.type); + Navigator.pop(bcontext, newNode); + }, + ) + ], + title: Text('Edit text'), + content: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisAlignment: MainAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Container( + margin: EdgeInsets.only(bottom: 10), + child: TextField( + keyboardType: TextInputType.multiline, + maxLines: null, + textCapitalization: TextCapitalization.sentences, + controller: textEditingController, + ), + ), + Container( + color: Colors.grey[600], + padding: EdgeInsets.all(0), + child: Wrap( + children: [ + IconButton( + icon: Icon(Icons.format_bold), + onPressed: () { + TextSelection theSelection = + textEditingController.selection; + addTagAtSelection(textEditingController, + theSelection.start, theSelection.end, 'b'); + }, + ), + IconButton( + icon: Icon(Icons.format_italic), + onPressed: () { + TextSelection theSelection = + textEditingController.selection; + addTagAtSelection(textEditingController, + theSelection.start, theSelection.end, 'i'); + }, + ), + IconButton( + icon: Icon(Icons.format_underlined), + onPressed: () { + TextSelection theSelection = + textEditingController.selection; + addTagAtSelection(textEditingController, + theSelection.start, theSelection.end, 'u'); + }, + ), + IconButton( + icon: Icon(Icons.code), + onPressed: () { + TextSelection theSelection = + textEditingController.selection; + addTagAtSelection(textEditingController, + theSelection.start, theSelection.end, 'code'); + }, + ), + IconButton( + icon: Icon(Icons.link), + onPressed: () { + addLinkDialog(textEditingController); + }, + ), + ], + ), + ), + ], + ), + ); + }).then( + (newNode) { + SlateNode newNodeAsListItem = newNode; + newNodeAsListItem.type = 'list-item'; + setState(() { + int index = this.currentItemList.indexOf(node); + this.currentItemList[index] = newNodeAsListItem; + this.widget.onListUpdated(this.currentItemList); + }); + }, + ); + } + + void addLinkDialog(TextEditingController mainController) async { + ClipboardData clipBoardText = await Clipboard.getData('text/plain'); + TextEditingController urlController = + TextEditingController(text: clipBoardText.text); + await showDialog( + context: context, + child: new AlertDialog( + contentPadding: const EdgeInsets.all(16.0), + content: new Row( + children: [ + new Expanded( + child: new TextField( + autofocus: true, + keyboardType: TextInputType.url, + controller: urlController, + decoration: new InputDecoration(labelText: 'Url'), + ), + ) + ], + ), + actions: [ + new FlatButton( + child: const Text('Cancel'), + onPressed: () { + Navigator.of(context, rootNavigator: true).pop(); + }), + new FlatButton( + child: const Text('Insert'), + onPressed: () { + Navigator.of(context, rootNavigator: true).pop(); + + if (mainController.text.endsWith('\n') || + mainController.text.isEmpty) { + mainController.text = + mainController.text + '[url]${urlController.text}[/url]'; + } else { + mainController.text = mainController.text + + '\n[url]${urlController.text}[/url]'; + } + }) + ], + ), + ); + } + + void addTagAtSelection( + TextEditingController controller, int start, int end, String tag) { + RegExp regExp = new RegExp( + r'(\[([^/].*?)(=(.+?))?\](.*?)\[/\2\]|\[([^/].*?)(=(.+?))?\])', + caseSensitive: false, + multiLine: false, + ); + + String newline = tag == 'h1' || tag == 'h2' ? "\n" : ''; + String selectedText = controller.text.substring(start, end); + String replaceWith = ''; + + if (regExp.hasMatch(selectedText)) { + replaceWith = selectedText.replaceAll( + '[${tag}]', ''); //ignore: unnecessary_brace_in_string_interps + replaceWith = replaceWith.replaceAll( + '[/${tag}]', ''); //ignore: unnecessary_brace_in_string_interps + } else { + replaceWith = newline + + '[${tag}]' + + selectedText + + '[/${tag}]'; //ignore: unnecessary_brace_in_string_interps + } + controller.text = controller.text.replaceRange(start, end, replaceWith); + } + + List buildItemList() { + return currentItemList + .map((SlateNode item) => ListTile( + onTap: () => this.showTextEditDialog(context, item), + title: item.nodes.first.leaves.length > 0 + ? listItemTextHandler(item) + : Text('- empty list item -'))) + .toList(); + } + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: MainAxisSize.min, + children: [ + LimitedBox( + maxHeight: 400, + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: MainAxisSize.min, + children: buildItemList()), + ), + ), + FlatButton( + child: Text('Add list item'), + onPressed: () { + setState(() { + currentItemList.add( + SlateNode( + type: 'list-item', + nodes: [SlateNode(object: 'block', leaves: [])], + ), + ); + }); + }, + ), + ], + ); + } +} diff --git a/lib/widget/PostEditor.dart b/lib/widget/PostEditor.dart index b402bc3..d6eda40 100644 --- a/lib/widget/PostEditor.dart +++ b/lib/widget/PostEditor.dart @@ -24,31 +24,39 @@ class PostEditor extends StatefulWidget { final Function onTapAddYouTubeVideo; final Function onTapAddVideo; final Function onReorderHandler; + final Function onTapAddBulletedList; + final Function onTapAddNumberedList; + final Function onTapAddUserQuote; - PostEditor( - {this.document, - this.onTapTextBlock, - this.onTapImageBlock, - this.onTapListBlock, - this.onTapQuoteBlock, - this.onTapTwitterBlock, - this.onTapUserQuoteBlock, - this.onTapVideoBlock, - this.onTapYouTubeBlock, - this.onTapAddHeadingOne, - this.onTapAddHeadingTwo, - this.onTapAddImage, - this.onTapAddQuote, - this.onTapAddTextBlock, - this.onTapAddVideo, - this.onTapAddYouTubeVideo, - this.onReorderHandler}); + PostEditor({ + this.document, + this.onTapTextBlock, + this.onTapImageBlock, + this.onTapListBlock, + this.onTapQuoteBlock, + this.onTapTwitterBlock, + this.onTapUserQuoteBlock, + this.onTapVideoBlock, + this.onTapYouTubeBlock, + this.onTapAddHeadingOne, + this.onTapAddHeadingTwo, + this.onTapAddImage, + this.onTapAddQuote, + this.onTapAddTextBlock, + this.onTapAddVideo, + this.onTapAddYouTubeVideo, + this.onReorderHandler, + this.onTapAddBulletedList, + this.onTapAddNumberedList, + this.onTapAddUserQuote, + }); @override _PostEditorState createState() => _PostEditorState(); } class _PostEditorState extends State { + Widget _quoteHandler( SlateNode node, Function inlineHandler, Function leafHandler) { List lines = List(); @@ -80,12 +88,14 @@ class _PostEditorState extends State { onTap: () => this.widget.onTapQuoteBlock(context, node), leading: Icon(Icons.format_quote), title: RichText( - text: TextSpan(children: lines.length > 0 ? lines : [TextSpan(text: '- empty quote')]), + text: TextSpan( + children: + lines.length > 0 ? lines : [TextSpan(text: '- empty quote')]), ), ); } - Widget _youtubeHandler (String youTubeUrl, SlateNode node) { + Widget _youtubeHandler(String youTubeUrl, SlateNode node) { return ListTile( onTap: () => this.widget.onTapYouTubeBlock(youTubeUrl, node), leading: Icon(Icons.ondemand_video), @@ -93,7 +103,7 @@ class _PostEditorState extends State { ); } - Widget _videoHandler (SlateNode node) { + Widget _videoHandler(SlateNode node) { return ListTile( onTap: () => this.widget.onTapVideoBlock(node), leading: Icon(Icons.videocam), @@ -101,120 +111,200 @@ class _PostEditorState extends State { ); } - List editorContent() { - return SlateDocumentParser( - slateObject: this.widget.document, - context: context, - paragraphHandler: (SlateNode node, Function leafHandler) { - List lines = List(); - - // Handle block nodes - node.nodes.forEach((line) { - if (line.leaves != null) { - lines.addAll(leafHandler(line.leaves)); - } + Widget _bulletedListHandler(List listItemsContent, SlateNode node) { + List listItems = List(); + // Handle block nodes + listItemsContent.forEach((item) { + listItems.add( + Container( + margin: EdgeInsets.only(bottom: 5.0), + child: Row( + children: [ + Container( + margin: EdgeInsets.only(right: 10.0), + height: 5.0, + width: 5.0, + decoration: new BoxDecoration( + color: Theme.of(context).textTheme.body1.color, + shape: BoxShape.circle, + ), + ), + Expanded(child: item) + ], + ), + ), + ); + }); - // Handle inline element - if (line.object == 'inline') { - // Handle links - if (line.type == 'link') { - line.nodes.forEach((inlineNode) { - inlineNode.leaves.forEach((leaf) { - lines.add(TextSpan( - text: leaf.text, - style: TextStyle(color: Colors.blue), - )); - }); - }); - } else { - line.nodes.forEach((inlineNode) { - inlineNode.leaves.forEach((leaf) { - lines.add(TextSpan(text: leaf.text)); - }); - }); - } - } - }); + return ListTile( + onTap: () => this.widget.onTapListBlock(node), + leading: Icon(Icons.format_list_bulleted), + title: Container( + margin: EdgeInsets.only(top: 10, bottom: 10), + child: listItems.length > 0 + ? Text('${listItems.length} items') + : Text('- empty list -'), + ), + ); + } - return ListTile( - leading: Icon(Icons.text_format), - onTap: () { - print(node); - this.widget.onTapTextBlock(context, node); - }, - title: Container( - margin: EdgeInsets.only(bottom: 5), - child: RichText( - text: lines.length > 0 - ? TextSpan(children: lines) - : TextSpan(text: '- empty text block -')), + Widget _numberedListHandler(List listItemsContent, SlateNode node) { + List listItems = List(); + // Handle block nodes + listItemsContent.forEach((item) { + listItems.add( + Container( + margin: EdgeInsets.only(bottom: 5.0), + child: Row( + children: [ + Container( + margin: EdgeInsets.only(right: 10.0), + child: Text( + (listItems.length + 1).toString(), + ), + ), + Expanded( + child: item, + ) + ], ), - ); + ), + ); + }); + + return ListTile( + onTap: () => this.widget.onTapListBlock(node), + leading: Icon(Icons.format_list_numbered), + title: Container( + margin: EdgeInsets.only(top: 10, bottom: 10), + child: listItems.length > 0 + ? Text('${listItems.length} items') + : Text('- empty list -'), + ), + ); + } + + Widget _imageWidgetHandler(String imageUrl, slateObject, SlateNode node) { + return ListTile( + leading: Icon(Icons.image), + onTap: () { + this.widget.onTapImageBlock(slateObject, node); }, - headingHandler: - (SlateNode node, Function inlineHandler, Function leafHandler) { - List lines = List(); + title: Container( + margin: EdgeInsets.only(top: 10.0, bottom: 10.0), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [Text('Image block'), Text(imageUrl)], + ), + ), + ); + } - // Handle block nodes - node.nodes.forEach((line) { - if (line.leaves != null) { - double headingSize = 14.0; + Widget headingHandler( + SlateNode node, Function inlineHandler, Function leafHandler) { + List lines = List(); - if (node.type.contains('-one')) { - headingSize = 30.0; - } + // Handle block nodes + node.nodes.forEach((line) { + if (line.leaves != null) { + double headingSize = 14.0; - if (node.type.contains('-two')) { - headingSize = 20.0; - } + if (node.type.contains('-one')) { + headingSize = 30.0; + } - // Handle node leaves - lines.addAll(leafHandler(line.leaves, fontSize: headingSize)); - } + if (node.type.contains('-two')) { + headingSize = 20.0; + } - // Handle inline element - if (line.object == 'inline') { - // Handle links - inlineHandler(node, line); - } - }); + // Handle node leaves + lines.addAll(leafHandler(line.leaves, fontSize: headingSize)); + } - return ListTile( - leading: Icon(node.type.contains('-one') - ? MdiIcons.formatHeader1 - : MdiIcons.formatHeader2), - onTap: () { - this.widget.onTapTextBlock(context, node); - }, - title: Container( - margin: EdgeInsets.only(bottom: 10), - child: RichText( - text: lines.length > 0 - ? TextSpan(children: lines) - : TextSpan(text: '- empty heading block -'), - ), - ), - ); + // Handle inline element + if (line.object == 'inline') { + // Handle links + inlineHandler(node, line); + } + }); + + return ListTile( + leading: Icon(node.type.contains('-one') + ? MdiIcons.formatHeader1 + : MdiIcons.formatHeader2), + onTap: () { + this.widget.onTapTextBlock(context, node); }, - imageWidgetHandler: (String imageUrl, slateObject, SlateNode node) { - return ListTile( - leading: Icon(Icons.image), - onTap: () { - this.widget.onTapImageBlock(slateObject, node); - }, - title: Container( - margin: EdgeInsets.only(top: 10.0, bottom: 10.0), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [Text('Image block'), Text(imageUrl)], - ), - ), - ); + title: Container( + margin: EdgeInsets.only(bottom: 10), + child: RichText( + text: lines.length > 0 + ? TextSpan(children: lines) + : TextSpan(text: '- empty heading block -'), + ), + ), + ); + } + + Widget paragraphHandler(SlateNode node, Function leafHandler) { + List lines = List(); + + // Handle block nodes + node.nodes.forEach((line) { + if (line.leaves != null) { + lines.addAll(leafHandler(line.leaves)); + } + + // Handle inline element + if (line.object == 'inline') { + // Handle links + if (line.type == 'link') { + line.nodes.forEach((inlineNode) { + inlineNode.leaves.forEach((leaf) { + lines.add(TextSpan( + text: leaf.text, + style: TextStyle(color: Colors.blue), + )); + }); + }); + } else { + line.nodes.forEach((inlineNode) { + inlineNode.leaves.forEach((leaf) { + lines.add(TextSpan(text: leaf.text)); + }); + }); + } + } + }); + + return ListTile( + leading: Icon(Icons.text_format), + onTap: () { + this.widget.onTapTextBlock(context, node); }, + title: Container( + margin: EdgeInsets.only(bottom: 5), + child: RichText( + text: lines.length > 0 + ? TextSpan(children: lines) + : TextSpan(text: '- empty text block -')), + ), + ); + } + + List editorContent() { + return SlateDocumentParser( + slateObject: this.widget.document, + context: context, + paragraphHandler: this.paragraphHandler, + headingHandler: this.headingHandler, + imageWidgetHandler: this._imageWidgetHandler, quotesHandler: _quoteHandler, youTubeWidgetHandler: this._youtubeHandler, videoWidgetHandler: this._videoHandler, + bulletedListHandler: this._bulletedListHandler, + numberedListHandler: this._numberedListHandler, ) .asWidgetList() .map((widget) => Container( @@ -256,6 +346,14 @@ class _PostEditorState extends State { icon: Icon(Icons.format_quote), onPressed: this.widget.onTapAddQuote, ), + IconButton( + icon: Icon(Icons.format_list_bulleted), + onPressed: this.widget.onTapAddBulletedList, + ), + IconButton( + icon: Icon(Icons.format_list_numbered), + onPressed: this.widget.onTapAddNumberedList, + ), IconButton( icon: Icon(Icons.image), onPressed: this.widget.onTapAddImage, diff --git a/lib/widget/SlateDocumentParser/SlateDocumentParser.dart b/lib/widget/SlateDocumentParser/SlateDocumentParser.dart index 8f5251f..610e485 100644 --- a/lib/widget/SlateDocumentParser/SlateDocumentParser.dart +++ b/lib/widget/SlateDocumentParser/SlateDocumentParser.dart @@ -131,14 +131,14 @@ class SlateDocumentParser extends StatelessWidget { List listItemsContent = List(); listItemsContent.addAll(handleNodes(node.nodes)); - return this.bulletedListHandler(listItemsContent); + return this.bulletedListHandler(listItemsContent, node); } Widget numberedListToWidget(SlateNode node) { List listItemsContent = List(); listItemsContent.addAll(handleNodes(node.nodes)); - return this.numberedListHandler(listItemsContent); + return this.numberedListHandler(listItemsContent, node); } Widget headingToWidget(SlateNode node) { diff --git a/lib/widget/Thread/PostContent.dart b/lib/widget/Thread/PostContent.dart new file mode 100644 index 0000000..cc29431 --- /dev/null +++ b/lib/widget/Thread/PostContent.dart @@ -0,0 +1,245 @@ +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:knocky/models/slateDocument.dart'; +import 'package:knocky/widget/SlateDocumentParser/SlateDocumentParser.dart'; +import 'package:knocky/widget/Thread/PostElements/Embed.dart'; +import 'package:knocky/widget/Thread/PostElements/Image.dart'; +import 'package:knocky/widget/Thread/PostElements/UserQuote.dart'; +import 'package:knocky/widget/Thread/PostElements/Video.dart'; +import 'package:knocky/widget/Thread/PostElements/YouTubeEmbed.dart'; +import 'package:intent/intent.dart' as Intent; +import 'package:intent/action.dart' as Action; + +class PostContent extends StatelessWidget { + final SlateObject content; + final Function onTapSpoiler; + final GlobalKey scaffoldKey; + + PostContent({ + this.onTapSpoiler, + this.content, + this.scaffoldKey, + }); + + @override + Widget build(BuildContext context) { + return SlateDocumentParser( + slateObject: this.content, + onPressSpoiler: (text) { + this.onTapSpoiler(text); + }, + context: context, + paragraphHandler: (SlateNode node, Function leafHandler) { + List lines = List(); + + // Handle block nodes + node.nodes.forEach((line) { + if (line.leaves != null) { + lines.addAll(leafHandler(line.leaves)); + } + + // Handle inline element + if (line.object == 'inline') { + // Handle links + if (line.type == 'link') { + line.nodes.forEach((inlineNode) { + inlineNode.leaves.forEach((leaf) { + lines.add(TextSpan( + text: leaf.text, + style: TextStyle(color: Colors.blue), + recognizer: TapGestureRecognizer() + ..onTap = () { + Intent.Intent() + ..setAction(Action.Action.ACTION_VIEW) + ..setData(Uri.parse(line.data.href)) + ..startActivity().catchError((e) => print(e)); + print('Clicked link: ' + line.data.href); + })); + }); + }); + } else { + line.nodes.forEach((inlineNode) { + inlineNode.leaves.forEach((leaf) { + lines.add(TextSpan(text: leaf.text)); + }); + }); + } + } + }); + + return Container( + child: RichText( + text: TextSpan(children: lines), + ), + ); + }, + headingHandler: + (SlateNode node, Function inlineHandler, Function leafHandler) { + List lines = List(); + + // Handle block nodes + node.nodes.forEach((line) { + if (line.leaves != null) { + double headingSize = 14.0; + + if (node.type.contains('-one')) { + headingSize = 30.0; + } + + if (node.type.contains('-two')) { + headingSize = 20.0; + } + + // Handle node leaves + lines.addAll(leafHandler(line.leaves, fontSize: headingSize)); + } + + // Handle inline element + if (line.object == 'inline') { + // Handle links + inlineHandler(node, line); + } + }); + + return Container( + margin: EdgeInsets.only(bottom: 10), + child: RichText( + text: TextSpan(children: lines), + ), + ); + }, + imageWidgetHandler: (String imageUrl, slateObject, SlateNode node) { + return Container( + margin: EdgeInsets.only(top: 10.0, bottom: 10.0), + child: LimitedBox( + maxHeight: 300, + child: ImageWidget(url: imageUrl, slateObject: slateObject), + ), + ); + }, + videoWidgetHandler: (SlateNode node) { + return VideoElement( + url: node.data.src, + scaffoldKey: this.scaffoldKey, + ); + }, + youTubeWidgetHandler: (String youTubeUrl, SlateNode node) { + return YoutubeVideoEmbed( + url: youTubeUrl, + ); + }, + twitterEmbedHandler: (String embedUrl) { + return EmbedWidget( + url: embedUrl, + ); + }, + userQuoteHandler: (String username, List widgets, bool isChild) { + return UserQuoteWidget( + username: username, + children: widgets, + isChild: isChild, + ); + }, + bulletedListHandler: (List listItemsContent, node) { + List listItems = List(); + // Handle block nodes + listItemsContent.forEach((item) { + listItems.add( + Container( + margin: EdgeInsets.only(bottom: 5.0), + child: Row( + children: [ + Container( + margin: EdgeInsets.only(right: 10.0), + height: 5.0, + width: 5.0, + decoration: new BoxDecoration( + color: Theme.of(context).textTheme.body1.color, + shape: BoxShape.circle, + ), + ), + Expanded(child: item) + ], + ), + ), + ); + }); + + return Container( + margin: EdgeInsets.only(top: 10, bottom: 10), + child: Column(children: listItems), + ); + }, + numberedListHandler: (List listItemsContent, SlateNode node) { + List listItems = List(); + // Handle block nodes + listItemsContent.forEach((item) { + listItems.add( + Container( + margin: EdgeInsets.only(bottom: 5.0), + child: Row( + children: [ + Container( + margin: EdgeInsets.only(right: 10.0), + child: Text( + (listItems.length + 1).toString(), + ), + ), + Expanded( + child: item, + ) + ], + ), + ), + ); + }); + + return Container( + margin: EdgeInsets.only(bottom: 10), + child: Column(children: listItems), + ); + }, + quotesHandler: + (SlateNode node, Function inlineHandler, Function leafHandler) { + List lines = List(); + // Handle block nodes + node.nodes.forEach((line) { + if (line.leaves != null) { + double headingSize = 14.0; + + if (node.type.contains('-one')) { + headingSize = 30.0; + } + + if (node.type.contains('-two')) { + headingSize = 20.0; + } + + // Handle node leaves + lines.addAll(leafHandler(line.leaves, fontSize: headingSize)); + } + + // Handle inline element + if (line.object == 'inline') { + // Handle links + inlineHandler(node, line); + } + }); + + return Container( + margin: EdgeInsets.only(bottom: 10.0), + padding: EdgeInsets.all(10.0), + decoration: BoxDecoration( + border: Border( + left: BorderSide(color: Colors.blue, width: 3.0), + ), + color: Colors.grey, + ), + child: RichText( + text: TextSpan(children: lines), + ), + ); + }, + ); + } +} diff --git a/lib/widget/Thread/ThreadPostItem.dart b/lib/widget/Thread/ThreadPostItem.dart index 697675a..152db99 100644 --- a/lib/widget/Thread/ThreadPostItem.dart +++ b/lib/widget/Thread/ThreadPostItem.dart @@ -1,23 +1,15 @@ -import 'package:flutter/gestures.dart'; + import 'package:flutter/material.dart'; -import 'package:knocky/models/slateDocument.dart'; import 'package:knocky/models/thread.dart'; -import 'package:knocky/widget/SlateDocumentParser/SlateDocumentParser.dart'; import 'package:knocky/helpers/icons.dart'; import 'package:cached_network_image/cached_network_image.dart'; -import 'package:knocky/widget/Thread/PostElements/Embed.dart'; -import 'package:knocky/widget/Thread/PostElements/Image.dart'; -import 'package:knocky/widget/Thread/PostElements/UserQuote.dart'; -import 'package:knocky/widget/Thread/PostElements/Video.dart'; -import 'package:knocky/widget/Thread/PostElements/YouTubeEmbed.dart'; import 'package:knocky/widget/Thread/PostHeader.dart'; import 'package:scoped_model/scoped_model.dart'; import 'package:knocky/state/authentication.dart'; import 'package:knocky/widget/Thread/RatePostContent.dart'; import 'package:knocky/widget/Thread/ViewUsersOfRatingsContent.dart'; import 'package:knocky/widget/Thread/PostBan.dart'; -import 'package:intent/intent.dart' as Intent; -import 'package:intent/action.dart' as Action; +import 'package:knocky/widget/Thread/PostContent.dart'; class ThreadPostItem extends StatelessWidget { final ThreadPost postDetails; @@ -193,230 +185,12 @@ class ThreadPostItem extends StatelessWidget { ), Container( padding: EdgeInsets.only(left: 10, right: 10), - child: SlateDocumentParser( - slateObject: postDetails.content, - onPressSpoiler: (text) { - onPressSpoiler(context, text); - }, - context: context, - paragraphHandler: (SlateNode node, Function leafHandler) { - List lines = List(); - - // Handle block nodes - node.nodes.forEach((line) { - if (line.leaves != null) { - lines.addAll(leafHandler(line.leaves)); - } - - // Handle inline element - if (line.object == 'inline') { - // Handle links - if (line.type == 'link') { - line.nodes.forEach((inlineNode) { - inlineNode.leaves.forEach((leaf) { - lines.add(TextSpan( - text: leaf.text, - style: TextStyle(color: Colors.blue), - recognizer: TapGestureRecognizer() - ..onTap = () { - Intent.Intent() - ..setAction(Action.Action.ACTION_VIEW) - ..setData(Uri.parse(line.data.href)) - ..startActivity() - .catchError((e) => print(e)); - print('Clicked link: ' + line.data.href); - })); - }); - }); - } else { - line.nodes.forEach((inlineNode) { - inlineNode.leaves.forEach((leaf) { - lines.add(TextSpan(text: leaf.text)); - }); - }); - } - } - }); - - return Container( - child: RichText( - text: TextSpan(children: lines), - ), - ); - }, - headingHandler: (SlateNode node, Function inlineHandler, - Function leafHandler) { - List lines = List(); - - // Handle block nodes - node.nodes.forEach((line) { - if (line.leaves != null) { - double headingSize = 14.0; - - if (node.type.contains('-one')) { - headingSize = 30.0; - } - - if (node.type.contains('-two')) { - headingSize = 20.0; - } - - // Handle node leaves - lines.addAll( - leafHandler(line.leaves, fontSize: headingSize)); - } - - // Handle inline element - if (line.object == 'inline') { - // Handle links - inlineHandler(node, line); - } - }); - - return Container( - margin: EdgeInsets.only(bottom: 10), - child: RichText( - text: TextSpan(children: lines), - ), - ); - }, - imageWidgetHandler: - (String imageUrl, slateObject, SlateNode node) { - return Container( - margin: EdgeInsets.only(top: 10.0, bottom: 10.0), - child: LimitedBox( - maxHeight: 300, - child: - ImageWidget(url: imageUrl, slateObject: slateObject), - ), - ); - }, - videoWidgetHandler: (SlateNode node) { - return VideoElement( - url: node.data.src, - scaffoldKey: this.scaffoldKey, - ); - }, - youTubeWidgetHandler: (String youTubeUrl, SlateNode node) { - return YoutubeVideoEmbed( - url: youTubeUrl, - ); - }, - twitterEmbedHandler: (String embedUrl) { - return EmbedWidget( - url: embedUrl, - ); - }, - userQuoteHandler: - (String username, List widgets, bool isChild) { - return UserQuoteWidget( - username: username, - children: widgets, - isChild: isChild, - ); - }, - bulletedListHandler: (List listItemsContent) { - List listItems = List(); - // Handle block nodes - listItemsContent.forEach((item) { - listItems.add( - Container( - margin: EdgeInsets.only(bottom: 5.0), - child: Row( - children: [ - Container( - margin: EdgeInsets.only(right: 10.0), - height: 5.0, - width: 5.0, - decoration: new BoxDecoration( - color: Theme.of(context).textTheme.body1.color, - shape: BoxShape.circle, - ), - ), - Expanded(child: item) - ], - ), - ), - ); - }); - - return Container( - margin: EdgeInsets.only(top: 10, bottom: 10), - child: Column(children: listItems), - ); - }, - numberedListHandler: (List listItemsContent) { - List listItems = List(); - // Handle block nodes - listItemsContent.forEach((item) { - listItems.add( - Container( - margin: EdgeInsets.only(bottom: 5.0), - child: Row( - children: [ - Container( - margin: EdgeInsets.only(right: 10.0), - child: Text( - (listItems.length + 1).toString(), - ), - ), - Expanded( - child: item, - ) - ], - ), - ), - ); - }); - - return Container( - margin: EdgeInsets.only(bottom: 10), - child: Column(children: listItems), - ); - }, - quotesHandler: (SlateNode node, Function inlineHandler, - Function leafHandler) { - List lines = List(); - // Handle block nodes - node.nodes.forEach((line) { - if (line.leaves != null) { - double headingSize = 14.0; - - if (node.type.contains('-one')) { - headingSize = 30.0; - } - - if (node.type.contains('-two')) { - headingSize = 20.0; - } - - // Handle node leaves - lines.addAll( - leafHandler(line.leaves, fontSize: headingSize)); - } - - // Handle inline element - if (line.object == 'inline') { - // Handle links - inlineHandler(node, line); - } - }); - - return Container( - margin: EdgeInsets.only(bottom: 10.0), - padding: EdgeInsets.all(10.0), - decoration: BoxDecoration( - border: Border( - left: BorderSide(color: Colors.blue, width: 3.0), - ), - color: Colors.grey, - ), - child: RichText( - text: TextSpan(children: lines), - ), - ); - }, - ), + child: PostContent( + content: postDetails.content, + onTapSpoiler: (text) { + onPressSpoiler(context, text); + }, + scaffoldKey: this.scaffoldKey), ), Container( padding: From c5e98b0680727bce5010a32e3bb86a6db1bd8d20 Mon Sep 17 00:00:00 2001 From: dasmikko Date: Thu, 8 Aug 2019 21:47:38 +0200 Subject: [PATCH 09/19] Add twitter embed to editor --- lib/screens/newPost.dart | 100 ++++++++++++++++-- lib/widget/PostEditor.dart | 16 +++ .../SlateDocumentParser.dart | 2 +- lib/widget/Thread/PostContent.dart | 2 +- 4 files changed, 109 insertions(+), 11 deletions(-) diff --git a/lib/screens/newPost.dart b/lib/screens/newPost.dart index b7f2c87..e6674e0 100644 --- a/lib/screens/newPost.dart +++ b/lib/screens/newPost.dart @@ -229,15 +229,6 @@ class _NewPostScreenState extends State { }); Navigator.of(context, rootNavigator: true).pop(); - - if (controller.text.endsWith('\n') || controller.text.isEmpty) { - controller.text = controller.text + - '[youtube]${urlController.text}[/youtube]'; - } else { - controller.text = controller.text + - '\n[youtube]${urlController.text}[/youtube]'; - } - refreshPreview(); }) ], ), @@ -391,6 +382,50 @@ class _NewPostScreenState extends State { }); } + void addTwitterEmbed() async { + ClipboardData clipBoardText = await Clipboard.getData('text/plain'); + TextEditingController urlController = + TextEditingController(text: clipBoardText.text); + await showDialog( + context: context, + child: new AlertDialog( + contentPadding: const EdgeInsets.all(16.0), + content: new Row( + children: [ + new Expanded( + child: new TextField( + autofocus: true, + keyboardType: TextInputType.url, + controller: urlController, + decoration: new InputDecoration(labelText: 'Twitter URL'), + ), + ) + ], + ), + actions: [ + new FlatButton( + child: const Text('Cancel'), + onPressed: () { + Navigator.of(context, rootNavigator: true).pop(); + }), + new FlatButton( + child: const Text('Insert'), + onPressed: () { + setState(() { + this.document.document.nodes.add(SlateNode( + type: 'twitter', + data: SlateNodeData(src: urlController.text))); + }); + + Navigator.of(context, rootNavigator: true).pop(); + }) + ], + ), + ); + } + + + /* Block tap callbacks */ @@ -676,6 +711,51 @@ class _NewPostScreenState extends State { ); } + void editTwitterEmbed(String youTubeUrl, SlateNode node) async { + TextEditingController urlController = + TextEditingController(text: node.data.src); + await showDialog( + context: context, + builder: (BuildContext context) => AlertDialog( + contentPadding: const EdgeInsets.all(16.0), + content: new Row( + children: [ + new Expanded( + child: new TextField( + autofocus: true, + keyboardType: TextInputType.url, + controller: urlController, + decoration: new InputDecoration(labelText: 'Twitter URL'), + ), + ) + ], + ), + actions: [ + new FlatButton( + child: const Text('Remove'), + onPressed: () { + setState(() { + int index = this.document.document.nodes.indexOf(node); + this.document.document.nodes.removeAt(index); + }); + Navigator.of(context, rootNavigator: true).pop(); + }), + new FlatButton( + child: const Text('Update'), + onPressed: () { + setState(() { + int index = this.document.document.nodes.indexOf(node); + this.document.document.nodes[index].data.src = + urlController.text; + }); + Navigator.of(context, rootNavigator: true).pop(); + }) + ], + ), + ); + } + + @override Widget build(BuildContext wcontext) { return DefaultTabController( @@ -712,6 +792,7 @@ class _NewPostScreenState extends State { onTapQuoteBlock: this.showTextEditDialog, onTapYouTubeBlock: this.editYoutubeVideoDialog, onTapListBlock: this.editList, + onTapTwitterBlock: this.editTwitterEmbed, // Toolbar onTapAddTextBlock: this.addTextBlock, onTapAddHeadingOne: () => this.addHeadingBlock('one'), @@ -722,6 +803,7 @@ class _NewPostScreenState extends State { onTapAddVideo: this.addVideoDialog, onTapAddBulletedList: () => this.addListBlock('bulleted-list'), onTapAddNumberedList: () => this.addListBlock('numbered-list'), + onTapAddTwitterEmbed: this.addTwitterEmbed, onReorderHandler: (int oldIndex, int newIndex) { if (oldIndex < newIndex) { // removing the item at oldIndex will shorten the list by 1. diff --git a/lib/widget/PostEditor.dart b/lib/widget/PostEditor.dart index d6eda40..e11014b 100644 --- a/lib/widget/PostEditor.dart +++ b/lib/widget/PostEditor.dart @@ -27,6 +27,7 @@ class PostEditor extends StatefulWidget { final Function onTapAddBulletedList; final Function onTapAddNumberedList; final Function onTapAddUserQuote; + final Function onTapAddTwitterEmbed; PostEditor({ this.document, @@ -49,6 +50,8 @@ class PostEditor extends StatefulWidget { this.onTapAddBulletedList, this.onTapAddNumberedList, this.onTapAddUserQuote, + this.onTapAddTwitterEmbed, + }); @override @@ -293,6 +296,14 @@ class _PostEditorState extends State { ); } + Widget _twitterHandler(String twitterUrl, SlateNode node) { + return ListTile( + onTap: () => this.widget.onTapTwitterBlock(twitterUrl, node), + leading: Icon(MdiIcons.twitter), + title: Text(twitterUrl), + ); + } + List editorContent() { return SlateDocumentParser( slateObject: this.widget.document, @@ -305,6 +316,7 @@ class _PostEditorState extends State { videoWidgetHandler: this._videoHandler, bulletedListHandler: this._bulletedListHandler, numberedListHandler: this._numberedListHandler, + twitterEmbedHandler: this._twitterHandler, ) .asWidgetList() .map((widget) => Container( @@ -366,6 +378,10 @@ class _PostEditorState extends State { icon: Icon(Icons.videocam), onPressed: this.widget.onTapAddVideo, ), + IconButton( + icon: Icon(MdiIcons.twitter), + onPressed: this.widget.onTapAddTwitterEmbed, + ), ], ), ), diff --git a/lib/widget/SlateDocumentParser/SlateDocumentParser.dart b/lib/widget/SlateDocumentParser/SlateDocumentParser.dart index 610e485..5c5894b 100644 --- a/lib/widget/SlateDocumentParser/SlateDocumentParser.dart +++ b/lib/widget/SlateDocumentParser/SlateDocumentParser.dart @@ -207,7 +207,7 @@ class SlateDocumentParser extends StatelessWidget { widgets.add(handleQuotes(node)); break; case 'twitter': - widgets.add(this.twitterEmbedHandler(node.data.src)); + widgets.add(this.twitterEmbedHandler(node.data.src, node)); break; case 'video': widgets.add(handleVideo(node)); diff --git a/lib/widget/Thread/PostContent.dart b/lib/widget/Thread/PostContent.dart index cc29431..4aab3b7 100644 --- a/lib/widget/Thread/PostContent.dart +++ b/lib/widget/Thread/PostContent.dart @@ -128,7 +128,7 @@ class PostContent extends StatelessWidget { url: youTubeUrl, ); }, - twitterEmbedHandler: (String embedUrl) { + twitterEmbedHandler: (String embedUrl, SlateNode node) { return EmbedWidget( url: embedUrl, ); From a0c7074abd28382f3ce656516c35dc2508d26437 Mon Sep 17 00:00:00 2001 From: dasmikko Date: Fri, 9 Aug 2019 08:04:41 +0200 Subject: [PATCH 10/19] Added support for strawpoll embeds --- lib/helpers/api.dart | 3 + lib/screens/newPost.dart | 121 +++++++++++++++++- lib/widget/PostEditor.dart | 17 +++ .../SlateDocumentParser.dart | 5 + lib/widget/Thread/PostContent.dart | 5 + 5 files changed, 146 insertions(+), 5 deletions(-) diff --git a/lib/helpers/api.dart b/lib/helpers/api.dart index 5257af4..ff1c12a 100644 --- a/lib/helpers/api.dart +++ b/lib/helpers/api.dart @@ -170,6 +170,8 @@ class KnockoutAPI { } Future newPost(dynamic content, int threadId) async { + print(content); + print(threadId); try { await _request(type: 'post', url: 'post', data: { 'content': json.encode(content).toString(), @@ -180,6 +182,7 @@ class KnockoutAPI { }); } on DioError catch (e) { print(e); + print(e.response); } } diff --git a/lib/screens/newPost.dart b/lib/screens/newPost.dart index e6674e0..614f437 100644 --- a/lib/screens/newPost.dart +++ b/lib/screens/newPost.dart @@ -24,7 +24,8 @@ class NewPostScreen extends StatefulWidget { class _NewPostScreenState extends State { SlateObject document = new SlateObject( - document: SlateDocument(nodes: List()), + object: 'value', + document: SlateDocument(object: 'document', nodes: List()), ); GlobalKey _scaffoldKey; bool _isPosting = false; @@ -42,7 +43,11 @@ class _NewPostScreenState extends State { setState(() { _isPosting = true; }); - await KnockoutAPI().newPost(document.toJson(), this.widget.thread.id); + await KnockoutAPI() + .newPost(document.toJson(), this.widget.thread.id) + .catchError((error) { + print(error); + }); Navigator.pop(context, true); } @@ -412,9 +417,20 @@ class _NewPostScreenState extends State { child: const Text('Insert'), onPressed: () { setState(() { - this.document.document.nodes.add(SlateNode( - type: 'twitter', - data: SlateNodeData(src: urlController.text))); + this.document.document.nodes.add( + SlateNode( + type: 'twitter', + object: 'block', + data: SlateNodeData(src: urlController.text), + nodes: [ + SlateNode( + object: 'text', + leaves: [ + SlateLeaf(text: '', marks: [], object: 'leaf') + ], + ), + ]), + ); }); Navigator.of(context, rootNavigator: true).pop(); @@ -424,7 +440,58 @@ class _NewPostScreenState extends State { ); } + void addStrawpollEmbed() async { + ClipboardData clipBoardText = await Clipboard.getData('text/plain'); + TextEditingController urlController = + TextEditingController(text: clipBoardText.text); + await showDialog( + context: context, + child: new AlertDialog( + contentPadding: const EdgeInsets.all(16.0), + content: new Row( + children: [ + new Expanded( + child: new TextField( + autofocus: true, + keyboardType: TextInputType.url, + controller: urlController, + decoration: new InputDecoration(labelText: 'Strawpoll URL'), + ), + ) + ], + ), + actions: [ + new FlatButton( + child: const Text('Cancel'), + onPressed: () { + Navigator.of(context, rootNavigator: true).pop(); + }), + new FlatButton( + child: const Text('Insert'), + onPressed: () { + setState(() { + this.document.document.nodes.add( + SlateNode( + type: 'strawpoll', + object: 'block', + data: SlateNodeData(src: urlController.text), + nodes: [ + SlateNode( + object: 'text', + leaves: [ + SlateLeaf(text: '', marks: [], object: 'leaf') + ], + ), + ]), + ); + }); + Navigator.of(context, rootNavigator: true).pop(); + }) + ], + ), + ); + } /* Block tap callbacks @@ -755,6 +822,49 @@ class _NewPostScreenState extends State { ); } + void editStrawpollEmbed(SlateNode node) async { + TextEditingController urlController = + TextEditingController(text: node.data.src); + await showDialog( + context: context, + builder: (BuildContext context) => AlertDialog( + contentPadding: const EdgeInsets.all(16.0), + content: new Row( + children: [ + new Expanded( + child: new TextField( + autofocus: true, + keyboardType: TextInputType.url, + controller: urlController, + decoration: new InputDecoration(labelText: 'Strawpoll URL'), + ), + ) + ], + ), + actions: [ + new FlatButton( + child: const Text('Remove'), + onPressed: () { + setState(() { + int index = this.document.document.nodes.indexOf(node); + this.document.document.nodes.removeAt(index); + }); + Navigator.of(context, rootNavigator: true).pop(); + }), + new FlatButton( + child: const Text('Update'), + onPressed: () { + setState(() { + int index = this.document.document.nodes.indexOf(node); + this.document.document.nodes[index].data.src = + urlController.text; + }); + Navigator.of(context, rootNavigator: true).pop(); + }) + ], + ), + ); + } @override Widget build(BuildContext wcontext) { @@ -804,6 +914,7 @@ class _NewPostScreenState extends State { onTapAddBulletedList: () => this.addListBlock('bulleted-list'), onTapAddNumberedList: () => this.addListBlock('numbered-list'), onTapAddTwitterEmbed: this.addTwitterEmbed, + onTapAddStrawPollEmbed: this.addStrawpollEmbed, onReorderHandler: (int oldIndex, int newIndex) { if (oldIndex < newIndex) { // removing the item at oldIndex will shorten the list by 1. diff --git a/lib/widget/PostEditor.dart b/lib/widget/PostEditor.dart index e11014b..52bfb5c 100644 --- a/lib/widget/PostEditor.dart +++ b/lib/widget/PostEditor.dart @@ -14,6 +14,7 @@ class PostEditor extends StatefulWidget { final Function onTapListBlock; final Function onTapUserQuoteBlock; final Function onTapTwitterBlock; + final Function onTapStrawpollBlock; // Toolbar callbacks final Function onTapAddTextBlock; @@ -28,6 +29,7 @@ class PostEditor extends StatefulWidget { final Function onTapAddNumberedList; final Function onTapAddUserQuote; final Function onTapAddTwitterEmbed; + final Function onTapAddStrawPollEmbed; PostEditor({ this.document, @@ -39,6 +41,7 @@ class PostEditor extends StatefulWidget { this.onTapUserQuoteBlock, this.onTapVideoBlock, this.onTapYouTubeBlock, + this.onTapStrawpollBlock, this.onTapAddHeadingOne, this.onTapAddHeadingTwo, this.onTapAddImage, @@ -51,6 +54,7 @@ class PostEditor extends StatefulWidget { this.onTapAddNumberedList, this.onTapAddUserQuote, this.onTapAddTwitterEmbed, + this.onTapAddStrawPollEmbed, }); @@ -304,6 +308,14 @@ class _PostEditorState extends State { ); } + Widget _strawpollHandler(SlateNode node) { + return ListTile( + onTap: () => this.widget.onTapStrawpollBlock(node.data.src, node), + leading: Icon(MdiIcons.poll), + title: Text(node.data.src), + ); + } + List editorContent() { return SlateDocumentParser( slateObject: this.widget.document, @@ -317,6 +329,7 @@ class _PostEditorState extends State { bulletedListHandler: this._bulletedListHandler, numberedListHandler: this._numberedListHandler, twitterEmbedHandler: this._twitterHandler, + strawpollHandler: this._strawpollHandler, ) .asWidgetList() .map((widget) => Container( @@ -382,6 +395,10 @@ class _PostEditorState extends State { icon: Icon(MdiIcons.twitter), onPressed: this.widget.onTapAddTwitterEmbed, ), + IconButton( + icon: Icon(MdiIcons.poll), + onPressed: this.widget.onTapAddStrawPollEmbed, + ), ], ), ), diff --git a/lib/widget/SlateDocumentParser/SlateDocumentParser.dart b/lib/widget/SlateDocumentParser/SlateDocumentParser.dart index 5c5894b..3084f93 100644 --- a/lib/widget/SlateDocumentParser/SlateDocumentParser.dart +++ b/lib/widget/SlateDocumentParser/SlateDocumentParser.dart @@ -18,6 +18,7 @@ class SlateDocumentParser extends StatelessWidget { final Function quotesHandler; final Function paragraphHandler; final Function headingHandler; + final Function strawpollHandler; final SlateDocumentController slateDocumentController; final bool asListView; @@ -37,6 +38,7 @@ class SlateDocumentParser extends StatelessWidget { @required this.quotesHandler, @required this.paragraphHandler, @required this.headingHandler, + @required this.strawpollHandler, this.asListView, }); @@ -212,6 +214,9 @@ class SlateDocumentParser extends StatelessWidget { case 'video': widgets.add(handleVideo(node)); break; + case 'strawpoll': + widgets.add(this.strawpollHandler(node)); + break; default: if (node.object == 'text') { widgets.add( diff --git a/lib/widget/Thread/PostContent.dart b/lib/widget/Thread/PostContent.dart index 4aab3b7..8fe6e33 100644 --- a/lib/widget/Thread/PostContent.dart +++ b/lib/widget/Thread/PostContent.dart @@ -133,6 +133,11 @@ class PostContent extends StatelessWidget { url: embedUrl, ); }, + strawpollHandler: (SlateNode node) { + return EmbedWidget( + url: node.data.src, + ); + }, userQuoteHandler: (String username, List widgets, bool isChild) { return UserQuoteWidget( username: username, From bada39ce530b35843aeb2cee1261a392baf9bccd Mon Sep 17 00:00:00 2001 From: dasmikko Date: Fri, 9 Aug 2019 17:01:44 +0200 Subject: [PATCH 11/19] Added reply support and edit post --- lib/screens/newPost.dart | 99 ++++++++++++++----- lib/widget/PostEditor.dart | 79 ++++++++++----- .../SlateDocumentParser.dart | 2 +- lib/widget/Thread/PostContent.dart | 2 +- 4 files changed, 130 insertions(+), 52 deletions(-) diff --git a/lib/screens/newPost.dart b/lib/screens/newPost.dart index 614f437..901f8ff 100644 --- a/lib/screens/newPost.dart +++ b/lib/screens/newPost.dart @@ -34,9 +34,23 @@ class _NewPostScreenState extends State { @override void initState() { super.initState(); - - //document = BBCodeHandler() - // .parse(controller.text, this.widget.thread, this.widget.replyList); + if (this.widget.replyList.length == 1) { + ThreadPost item = this.widget.replyList.first; + this.document.document.nodes.add( + SlateNode( + object: 'block', + type: 'userquote', + data: SlateNodeData( + postData: NodeDataPostData( + postId: item.id, + threadId: this.widget.thread.id, + threadPage: this.widget.thread.currentPage, + username: item.user.username, + ), + ), + nodes: item.content.document.nodes), + ); + } } void onPressPost() async { @@ -302,17 +316,23 @@ class _NewPostScreenState extends State { return ListTile( title: Text(item.user.username), onTap: () { + setState(() { + this.document.document.nodes.add( + SlateNode( + object: 'block', + type: 'userquote', + data: SlateNodeData( + postData: NodeDataPostData( + postId: item.id, + threadId: this.widget.thread.id, + threadPage: this.widget.thread.currentPage, + username: item.user.username, + ), + ), + nodes: item.content.document.nodes), + ); + }); Navigator.of(bcontext, rootNavigator: true).pop(); - - if (controller.text.endsWith('\n') || - controller.text.isEmpty) { - controller.text = - controller.text + '[userquote]${index + 1}[/userquote]'; - } else { - controller.text = controller.text + - '\n[userquote]${index + 1}[/userquote]'; - } - refreshPreview(); }, ); }, @@ -695,10 +715,9 @@ class _NewPostScreenState extends State { ); } - void editVideoDialog() async { - ClipboardData clipBoardText = await Clipboard.getData('text/plain'); + void editVideoDialog(SlateNode node) async { TextEditingController urlController = - TextEditingController(text: clipBoardText.text); + TextEditingController(text: node.data.src); await showDialog( context: context, builder: (BuildContext context) => new AlertDialog( @@ -718,20 +737,21 @@ class _NewPostScreenState extends State { ), actions: [ new FlatButton( - child: const Text('Cancel'), + child: const Text('Remove'), onPressed: () { + setState(() { + int index = this.document.document.nodes.indexOf(node); + this.document.document.nodes.removeAt(index); + }); Navigator.of(context, rootNavigator: true).pop(); }), new FlatButton( - child: const Text('Insert'), + child: const Text('Update'), onPressed: () { setState(() { - this.document.document.nodes.add( - SlateNode( - type: 'video', - data: SlateNodeData(src: urlController.text), - ), - ); + int index = this.document.document.nodes.indexOf(node); + this.document.document.nodes[index].data.src = + urlController.text; }); Navigator.of(context, rootNavigator: true).pop(); @@ -866,6 +886,33 @@ class _NewPostScreenState extends State { ); } + void editUserQuote(SlateNode node) async { + await showDialog( + context: context, + builder: (BuildContext context) => AlertDialog( + title: Text('Remove user quote?'), + contentPadding: const EdgeInsets.all(16.0), + content: Text('Are you sure you want to remove the user quote?'), + actions: [ + new FlatButton( + child: const Text('No'), + onPressed: () { + Navigator.of(context, rootNavigator: true).pop(); + }), + new FlatButton( + child: const Text('Yes'), + onPressed: () { + setState(() { + int index = this.document.document.nodes.indexOf(node); + this.document.document.nodes.removeAt(index); + }); + Navigator.of(context, rootNavigator: true).pop(); + }), + ], + ), + ); + } + @override Widget build(BuildContext wcontext) { return DefaultTabController( @@ -896,13 +943,16 @@ class _NewPostScreenState extends State { children: [ PostEditor( document: document, + replyList: this.widget.replyList, // Blocks onTapTextBlock: this.showTextEditDialog, onTapImageBlock: this.editImageDialog, onTapQuoteBlock: this.showTextEditDialog, onTapYouTubeBlock: this.editYoutubeVideoDialog, + onTapVideoBlock: this.editVideoDialog, onTapListBlock: this.editList, onTapTwitterBlock: this.editTwitterEmbed, + onTapUserQuoteBlock: this.editUserQuote, // Toolbar onTapAddTextBlock: this.addTextBlock, onTapAddHeadingOne: () => this.addHeadingBlock('one'), @@ -915,6 +965,7 @@ class _NewPostScreenState extends State { onTapAddNumberedList: () => this.addListBlock('numbered-list'), onTapAddTwitterEmbed: this.addTwitterEmbed, onTapAddStrawPollEmbed: this.addStrawpollEmbed, + onTapAddUserQuote: () => this.addUserquoteDialog(context), onReorderHandler: (int oldIndex, int newIndex) { if (oldIndex < newIndex) { // removing the item at oldIndex will shorten the list by 1. diff --git a/lib/widget/PostEditor.dart b/lib/widget/PostEditor.dart index 52bfb5c..b19f2a0 100644 --- a/lib/widget/PostEditor.dart +++ b/lib/widget/PostEditor.dart @@ -1,6 +1,7 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:knocky/models/slateDocument.dart'; +import 'package:knocky/models/thread.dart'; import 'package:knocky/widget/SlateDocumentParser/SlateDocumentParser.dart'; import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; @@ -31,38 +32,44 @@ class PostEditor extends StatefulWidget { final Function onTapAddTwitterEmbed; final Function onTapAddStrawPollEmbed; - PostEditor({ - this.document, - this.onTapTextBlock, - this.onTapImageBlock, - this.onTapListBlock, - this.onTapQuoteBlock, - this.onTapTwitterBlock, - this.onTapUserQuoteBlock, - this.onTapVideoBlock, - this.onTapYouTubeBlock, - this.onTapStrawpollBlock, - this.onTapAddHeadingOne, - this.onTapAddHeadingTwo, - this.onTapAddImage, - this.onTapAddQuote, - this.onTapAddTextBlock, - this.onTapAddVideo, - this.onTapAddYouTubeVideo, - this.onReorderHandler, - this.onTapAddBulletedList, - this.onTapAddNumberedList, - this.onTapAddUserQuote, - this.onTapAddTwitterEmbed, - this.onTapAddStrawPollEmbed, - - }); + final List replyList; + + PostEditor( + {this.document, + this.onTapTextBlock, + this.onTapImageBlock, + this.onTapListBlock, + this.onTapQuoteBlock, + this.onTapTwitterBlock, + this.onTapUserQuoteBlock, + this.onTapVideoBlock, + this.onTapYouTubeBlock, + this.onTapStrawpollBlock, + this.onTapAddHeadingOne, + this.onTapAddHeadingTwo, + this.onTapAddImage, + this.onTapAddQuote, + this.onTapAddTextBlock, + this.onTapAddVideo, + this.onTapAddYouTubeVideo, + this.onReorderHandler, + this.onTapAddBulletedList, + this.onTapAddNumberedList, + this.onTapAddUserQuote, + this.onTapAddTwitterEmbed, + this.onTapAddStrawPollEmbed, + this.replyList}); @override _PostEditorState createState() => _PostEditorState(); } class _PostEditorState extends State { + @override + void initState() { + super.initState(); + print(this.widget.replyList); + } Widget _quoteHandler( SlateNode node, Function inlineHandler, Function leafHandler) { @@ -316,6 +323,15 @@ class _PostEditorState extends State { ); } + Widget _userquoteHandler( + String username, List widgets, bool isChild, SlateNode node) { + return ListTile( + onTap: () => this.widget.onTapUserQuoteBlock(node), + leading: Icon(Icons.message), + title: Text(username), + ); + } + List editorContent() { return SlateDocumentParser( slateObject: this.widget.document, @@ -330,6 +346,7 @@ class _PostEditorState extends State { numberedListHandler: this._numberedListHandler, twitterEmbedHandler: this._twitterHandler, strawpollHandler: this._strawpollHandler, + userQuoteHandler: this._userquoteHandler, ) .asWidgetList() .map((widget) => Container( @@ -399,6 +416,16 @@ class _PostEditorState extends State { icon: Icon(MdiIcons.poll), onPressed: this.widget.onTapAddStrawPollEmbed, ), + if (this.widget.replyList.length > 0) + Builder( + builder: (BuildContext bcontext) { + return IconButton( + tooltip: 'Insert userquote', + icon: Icon(Icons.message), + onPressed: this.widget.onTapAddUserQuote, + ); + }, + ), ], ), ), diff --git a/lib/widget/SlateDocumentParser/SlateDocumentParser.dart b/lib/widget/SlateDocumentParser/SlateDocumentParser.dart index 3084f93..01c1dd7 100644 --- a/lib/widget/SlateDocumentParser/SlateDocumentParser.dart +++ b/lib/widget/SlateDocumentParser/SlateDocumentParser.dart @@ -153,7 +153,7 @@ class SlateDocumentParser extends StatelessWidget { // Handle block nodes widgets.addAll(handleNodes(node.nodes, isChild: !isChild)); - return this.userQuoteHandler(node.data.postData.username, widgets, isChild); + return this.userQuoteHandler(node.data.postData.username, widgets, isChild, node); } Widget youTubeToWidget(SlateNode node) { diff --git a/lib/widget/Thread/PostContent.dart b/lib/widget/Thread/PostContent.dart index 8fe6e33..4ded1da 100644 --- a/lib/widget/Thread/PostContent.dart +++ b/lib/widget/Thread/PostContent.dart @@ -138,7 +138,7 @@ class PostContent extends StatelessWidget { url: node.data.src, ); }, - userQuoteHandler: (String username, List widgets, bool isChild) { + userQuoteHandler: (String username, List widgets, bool isChild, SlateNode node) { return UserQuoteWidget( username: username, children: widgets, From 5a33bbc1ec9a8343f49a26285f04b9ec963879b6 Mon Sep 17 00:00:00 2001 From: dasmikko Date: Fri, 9 Aug 2019 17:02:01 +0200 Subject: [PATCH 12/19] Added reply and edit post support --- lib/helpers/api.dart | 55 +- lib/screens/editPost.dart | 981 ++++++++++++++++++++++++++ lib/screens/thread.dart | 8 +- lib/widget/Thread/ThreadPostItem.dart | 1 - 4 files changed, 1014 insertions(+), 31 deletions(-) create mode 100644 lib/screens/editPost.dart diff --git a/lib/helpers/api.dart b/lib/helpers/api.dart index ff1c12a..b2f0c7c 100644 --- a/lib/helpers/api.dart +++ b/lib/helpers/api.dart @@ -47,7 +47,6 @@ class KnockoutAPI { String mBaseurl = await box.get('env') == 'knockout' ? KNOCKOUT_URL : QA_URL; - Dio dio = new Dio(); dio.options.baseUrl = mBaseurl; dio.options.contentType = ContentType.json; @@ -76,8 +75,8 @@ class KnockoutAPI { try { final response2 = await _request(url: 'subforum'); return response2.data['list'] - .map((json) => Subforum.fromJson(json)) - .toList(); + .map((json) => Subforum.fromJson(json)) + .toList(); } on DioError catch (e) { throw e; } @@ -86,8 +85,8 @@ class KnockoutAPI { Future getSubforumDetails(int id, {int page = 1}) async { try { final response = await _request( - url: 'subforum/' + id.toString() + '/' + page.toString()); - return SubforumDetails.fromJson(response.data); + url: 'subforum/' + id.toString() + '/' + page.toString()); + return SubforumDetails.fromJson(response.data); } on DioError catch (e) { print(e); return null; @@ -102,7 +101,8 @@ class KnockoutAPI { Future authCheck() async { try { - final response = await _request(url: 'user/authCheck', headers: {'content-format-version': 1}); + final response = await _request( + url: 'user/authCheck', headers: {'content-format-version': 1}); return response.data; } on DioError catch (e) { return e.response.data; @@ -124,8 +124,7 @@ class KnockoutAPI { Future readThreads(DateTime lastseen, int threadId) async { ReadThreads jsonToPost = new ReadThreads(lastSeen: lastseen, threadId: threadId); - await _request( - type: 'post', url: 'readThreads', data: jsonToPost.toJson()); + await _request(type: 'post', url: 'readThreads', data: jsonToPost.toJson()); } Future readThreadSubsciption(DateTime lastseen, int threadId) async { @@ -138,16 +137,15 @@ class KnockoutAPI { final response = await _request(type: 'get', url: 'events'); return response.data - .map((json) => KnockoutEvent.fromJson(json)) - .toList(); + .map((json) => KnockoutEvent.fromJson(json)) + .toList(); } Future deleteThreadAlert(int threadid) async { final response = await _request( url: 'alert', type: 'delete', data: {'threadId': threadid}); - if (response.statusCode == 200) { - } + if (response.statusCode == 200) {} } Future subscribe(DateTime lastSeen, int threadid) async { @@ -156,8 +154,7 @@ class KnockoutAPI { type: 'post', data: {'lastSeen': lastSeen.toIso8601String(), 'threadId': threadid}); - if (response.statusCode == 200) { - } + if (response.statusCode == 200) {} } Future ratePost(int postId, String rating) async { @@ -186,30 +183,34 @@ class KnockoutAPI { } } - Future updatePost(String content, int postId, int threadId) async { - await _request( - type: 'post', - url: 'post', - data: {'content': content, 'id': postId, 'thread_id': threadId}); + Future updatePost(dynamic content, int postId, int threadId) async { + try { + await _request(type: 'put', url: 'post', data: { + 'content': json.encode(content).toString(), + 'id': postId, + 'thread_id': threadId + }); + } on DioError catch (e) { + print(e); + print(e.response); + } } Future> latestThreads() async { - final response = await _request( - type: 'get', - url: 'thread/latest'); + final response = await _request(type: 'get', url: 'thread/latest'); return response.data['list'] - .map((json) => SubforumThreadLatestPopular.fromJson(json)) + .map( + (json) => SubforumThreadLatestPopular.fromJson(json)) .toList(); } Future> popularThreads() async { - final response = await _request( - type: 'get', - url: 'thread/popular'); + final response = await _request(type: 'get', url: 'thread/popular'); return response.data['list'] - .map((json) => SubforumThreadLatestPopular.fromJson(json)) + .map( + (json) => SubforumThreadLatestPopular.fromJson(json)) .toList(); } diff --git a/lib/screens/editPost.dart b/lib/screens/editPost.dart new file mode 100644 index 0000000..c25040f --- /dev/null +++ b/lib/screens/editPost.dart @@ -0,0 +1,981 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:knocky/helpers/bbcode.dart'; +import 'package:knocky/models/slateDocument.dart'; +import 'package:knocky/models/thread.dart'; +import 'package:knocky/widget/ListEditor.dart'; +import 'package:knocky/widget/PostEditor.dart'; +import 'package:knocky/helpers/api.dart'; +import 'package:knocky/widget/KnockoutLoadingIndicator.dart'; +import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; +import 'package:knocky/widget/Thread/PostContent.dart'; + +class EditPostScreen extends StatefulWidget { + final ThreadPost postToEdit; + final Thread thread; + final List replyList; + + EditPostScreen({this.postToEdit, this.thread, this.replyList}); + + @override + _EditPostScreenState createState() => _EditPostScreenState(); +} + +class _EditPostScreenState extends State { + SlateObject document; + GlobalKey _scaffoldKey; + bool _isPosting = false; + TextEditingController controller = TextEditingController(); + + @override + void initState() { + super.initState(); + this.document = this.widget.postToEdit.content; + } + + void onPressPost() async { + setState(() { + _isPosting = true; + }); + await KnockoutAPI() + .updatePost(document.toJson(), this.widget.postToEdit.id, this.widget.thread.id) + .catchError((error) { + print(error); + }); + Navigator.pop(context, true); + } + + void refreshPreview() {} + + void onPressSpoiler(BuildContext context, String content) { + showDialog( + context: context, + builder: (BuildContext context) { + // return object of type Dialog + return AlertDialog( + content: new Text(content), + actions: [ + // usually buttons at the bottom of the dialog + new FlatButton( + child: new Text("Close"), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ], + ); + }, + ); + } + + void addTagAtSelection( + TextEditingController controller, int start, int end, String tag) { + RegExp regExp = new RegExp( + r'(\[([^/].*?)(=(.+?))?\](.*?)\[/\2\]|\[([^/].*?)(=(.+?))?\])', + caseSensitive: false, + multiLine: false, + ); + + String newline = tag == 'h1' || tag == 'h2' ? "\n" : ''; + String selectedText = controller.text.substring(start, end); + String replaceWith = ''; + + if (regExp.hasMatch(selectedText)) { + replaceWith = selectedText.replaceAll( + '[${tag}]', ''); //ignore: unnecessary_brace_in_string_interps + replaceWith = replaceWith.replaceAll( + '[/${tag}]', ''); //ignore: unnecessary_brace_in_string_interps + } else { + replaceWith = newline + + '[${tag}]' + + selectedText + + '[/${tag}]'; //ignore: unnecessary_brace_in_string_interps + } + controller.text = controller.text.replaceRange(start, end, replaceWith); + + refreshPreview(); + } + + /** + * Toolbar callbacks + */ + + void addImageDialog() async { + ClipboardData clipBoardText = await Clipboard.getData('text/plain'); + TextEditingController imgurlController = TextEditingController( + text: clipBoardText != null ? clipBoardText.text : ''); + await showDialog( + context: context, + child: new AlertDialog( + contentPadding: const EdgeInsets.all(16.0), + content: new Row( + children: [ + new Expanded( + child: new TextField( + autofocus: true, + keyboardType: TextInputType.url, + controller: imgurlController, + decoration: new InputDecoration(labelText: 'Image url'), + ), + ) + ], + ), + actions: [ + new FlatButton( + child: const Text('Cancel'), + onPressed: () { + Navigator.of(context, rootNavigator: true).pop(); + }), + new FlatButton( + child: const Text('Insert'), + onPressed: () { + Navigator.of(context, rootNavigator: true).pop(); + setState(() { + document.document.nodes.add( + SlateNode( + object: 'block', + type: 'image', + data: SlateNodeData(src: imgurlController.text), + ), + ); + }); + }) + ], + ), + ); + } + + void addLinkDialog(TextEditingController mainController) async { + ClipboardData clipBoardText = await Clipboard.getData('text/plain'); + TextEditingController urlController = + TextEditingController(text: clipBoardText.text); + await showDialog( + context: context, + child: new AlertDialog( + contentPadding: const EdgeInsets.all(16.0), + content: new Row( + children: [ + new Expanded( + child: new TextField( + autofocus: true, + keyboardType: TextInputType.url, + controller: urlController, + decoration: new InputDecoration(labelText: 'Url'), + ), + ) + ], + ), + actions: [ + new FlatButton( + child: const Text('Cancel'), + onPressed: () { + Navigator.of(context, rootNavigator: true).pop(); + }), + new FlatButton( + child: const Text('Insert'), + onPressed: () { + Navigator.of(context, rootNavigator: true).pop(); + + if (mainController.text.endsWith('\n') || + controller.text.isEmpty) { + mainController.text = + mainController.text + '[url]${urlController.text}[/url]'; + } else { + mainController.text = mainController.text + + '\n[url]${urlController.text}[/url]'; + } + + refreshPreview(); + }) + ], + ), + ); + } + + void addYoutubeVideoDialog() async { + ClipboardData clipBoardText = await Clipboard.getData('text/plain'); + TextEditingController urlController = + TextEditingController(text: clipBoardText.text); + await showDialog( + context: context, + child: new AlertDialog( + contentPadding: const EdgeInsets.all(16.0), + content: new Row( + children: [ + new Expanded( + child: new TextField( + autofocus: true, + keyboardType: TextInputType.url, + controller: urlController, + decoration: new InputDecoration(labelText: 'YouTube URL'), + ), + ) + ], + ), + actions: [ + new FlatButton( + child: const Text('Cancel'), + onPressed: () { + Navigator.of(context, rootNavigator: true).pop(); + }), + new FlatButton( + child: const Text('Insert'), + onPressed: () { + setState(() { + this.document.document.nodes.add(SlateNode( + type: 'youtube', + data: SlateNodeData(src: urlController.text))); + }); + + Navigator.of(context, rootNavigator: true).pop(); + }) + ], + ), + ); + } + + void addVideoDialog() async { + ClipboardData clipBoardText = await Clipboard.getData('text/plain'); + TextEditingController urlController = + TextEditingController(text: clipBoardText.text); + await showDialog( + context: context, + builder: (BuildContext context) => new AlertDialog( + contentPadding: const EdgeInsets.all(16.0), + content: new Row( + children: [ + new Expanded( + child: new TextField( + autofocus: true, + keyboardType: TextInputType.url, + controller: urlController, + decoration: + new InputDecoration(labelText: 'Video URL (Webm/mp4)'), + ), + ) + ], + ), + actions: [ + new FlatButton( + child: const Text('Cancel'), + onPressed: () { + Navigator.of(context, rootNavigator: true).pop(); + }), + new FlatButton( + child: const Text('Insert'), + onPressed: () { + setState(() { + this.document.document.nodes.add( + SlateNode( + type: 'video', + data: SlateNodeData(src: urlController.text), + ), + ); + }); + + Navigator.of(context, rootNavigator: true).pop(); + }) + ], + ), + ); + } + + void addUserquoteDialog(bcontext) async { + await showDialog( + context: bcontext, + child: new AlertDialog( + title: Text('Select post'), + contentPadding: const EdgeInsets.all(16.0), + content: Container( + height: 400, + width: 200, + child: ListView.builder( + itemCount: this.widget.replyList.length, + itemBuilder: (BuildContext context, int index) { + ThreadPost item = this.widget.replyList[index]; + return ListTile( + title: Text(item.user.username), + onTap: () { + setState(() { + this.document.document.nodes.add( + SlateNode( + object: 'block', + type: 'userquote', + data: SlateNodeData( + postData: NodeDataPostData( + postId: item.id, + threadId: this.widget.thread.id, + threadPage: this.widget.thread.currentPage, + username: item.user.username, + ), + ), + nodes: item.content.document.nodes), + ); + }); + Navigator.of(bcontext, rootNavigator: true).pop(); + }, + ); + }, + ), + ), + actions: [ + new FlatButton( + child: const Text('Cancel'), + onPressed: () { + Navigator.of(bcontext, rootNavigator: true).pop(); + }), + ], + ), + ); + } + + void addTextBlock() { + setState( + () { + this.document.document.nodes.add( + SlateNode( + type: 'paragraph', + object: 'block', + nodes: [ + SlateNode( + object: 'text', + leaves: [], + ), + ], + ), + ); + }, + ); + } + + void addHeadingBlock(String type) { + setState( + () { + this.document.document.nodes.add( + SlateNode( + type: 'heading-' + type, + object: 'block', + nodes: [ + SlateNode( + object: 'text', + leaves: [], + ), + ], + ), + ); + }, + ); + } + + void addQuoteBlock() { + setState(() { + this + .document + .document + .nodes + .add(SlateNode(type: 'block-quote', nodes: List())); + }); + } + + void addListBlock(String type) { + setState(() { + this + .document + .document + .nodes + .add(SlateNode(object: 'block', type: type, nodes: List())); + }); + } + + void addTwitterEmbed() async { + ClipboardData clipBoardText = await Clipboard.getData('text/plain'); + TextEditingController urlController = + TextEditingController(text: clipBoardText.text); + await showDialog( + context: context, + child: new AlertDialog( + contentPadding: const EdgeInsets.all(16.0), + content: new Row( + children: [ + new Expanded( + child: new TextField( + autofocus: true, + keyboardType: TextInputType.url, + controller: urlController, + decoration: new InputDecoration(labelText: 'Twitter URL'), + ), + ) + ], + ), + actions: [ + new FlatButton( + child: const Text('Cancel'), + onPressed: () { + Navigator.of(context, rootNavigator: true).pop(); + }), + new FlatButton( + child: const Text('Insert'), + onPressed: () { + setState(() { + this.document.document.nodes.add( + SlateNode( + type: 'twitter', + object: 'block', + data: SlateNodeData(src: urlController.text), + nodes: [ + SlateNode( + object: 'text', + leaves: [ + SlateLeaf(text: '', marks: [], object: 'leaf') + ], + ), + ]), + ); + }); + + Navigator.of(context, rootNavigator: true).pop(); + }) + ], + ), + ); + } + + void addStrawpollEmbed() async { + ClipboardData clipBoardText = await Clipboard.getData('text/plain'); + TextEditingController urlController = + TextEditingController(text: clipBoardText.text); + await showDialog( + context: context, + child: new AlertDialog( + contentPadding: const EdgeInsets.all(16.0), + content: new Row( + children: [ + new Expanded( + child: new TextField( + autofocus: true, + keyboardType: TextInputType.url, + controller: urlController, + decoration: new InputDecoration(labelText: 'Strawpoll URL'), + ), + ) + ], + ), + actions: [ + new FlatButton( + child: const Text('Cancel'), + onPressed: () { + Navigator.of(context, rootNavigator: true).pop(); + }), + new FlatButton( + child: const Text('Insert'), + onPressed: () { + setState(() { + this.document.document.nodes.add( + SlateNode( + type: 'strawpoll', + object: 'block', + data: SlateNodeData(src: urlController.text), + nodes: [ + SlateNode( + object: 'text', + leaves: [ + SlateLeaf(text: '', marks: [], object: 'leaf') + ], + ), + ]), + ); + }); + + Navigator.of(context, rootNavigator: true).pop(); + }) + ], + ), + ); + } + + /* + Block tap callbacks + */ + void showTextEditDialog(BuildContext context, SlateNode node) { + String bbcodeText = BBCodeHandler().slateParagraphToBBCode(node); + TextEditingController textEditingController = + TextEditingController(text: bbcodeText); + + showDialog( + context: context, + builder: (BuildContext bcontext) { + return AlertDialog( + actions: [ + FlatButton( + child: Text('Remove'), + onPressed: () { + setState(() { + int index = this.document.document.nodes.indexOf(node); + this.document.document.nodes.removeAt(index); + }); + Navigator.pop(bcontext); + }, + ), + FlatButton( + child: Text('Save'), + onPressed: () { + SlateNode newNode = BBCodeHandler() + .parse(textEditingController.text, type: node.type); + print(BBCodeHandler().slateParagraphToBBCode(newNode)); + print(newNode.toJson()); + Navigator.pop(bcontext, newNode); + }, + ) + ], + title: Text('Edit text block'), + content: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisAlignment: MainAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Container( + margin: EdgeInsets.only(bottom: 10), + child: TextField( + keyboardType: TextInputType.multiline, + maxLines: null, + textCapitalization: TextCapitalization.sentences, + controller: textEditingController, + ), + ), + Container( + color: Colors.grey[600], + padding: EdgeInsets.all(0), + child: Wrap( + children: [ + IconButton( + icon: Icon(Icons.format_bold), + onPressed: () { + TextSelection theSelection = + textEditingController.selection; + addTagAtSelection(textEditingController, + theSelection.start, theSelection.end, 'b'); + }, + ), + IconButton( + icon: Icon(Icons.format_italic), + onPressed: () { + TextSelection theSelection = + textEditingController.selection; + addTagAtSelection(textEditingController, + theSelection.start, theSelection.end, 'i'); + }, + ), + IconButton( + icon: Icon(Icons.format_underlined), + onPressed: () { + TextSelection theSelection = + textEditingController.selection; + addTagAtSelection(textEditingController, + theSelection.start, theSelection.end, 'u'); + }, + ), + IconButton( + icon: Icon(Icons.code), + onPressed: () { + TextSelection theSelection = + textEditingController.selection; + addTagAtSelection(textEditingController, + theSelection.start, theSelection.end, 'code'); + }, + ), + IconButton( + icon: Icon(Icons.link), + onPressed: () { + addLinkDialog(textEditingController); + }, + ), + ], + ), + ), + ], + ), + ); + }).then((newNode) { + setState(() { + if (newNode != null) { + int index = this.document.document.nodes.indexOf(node); + this.document.document.nodes[index] = newNode; + } + }); + }); + } + + void editImageDialog(slateObject, SlateNode node) async { + TextEditingController imgurlController = + TextEditingController(text: node.data.src); + await showDialog( + context: context, + builder: (BuildContext context) { + return new AlertDialog( + contentPadding: const EdgeInsets.all(16.0), + content: new Row( + children: [ + new Expanded( + child: new TextField( + autofocus: true, + keyboardType: TextInputType.url, + controller: imgurlController, + decoration: new InputDecoration(labelText: 'Image url'), + ), + ) + ], + ), + actions: [ + new FlatButton( + child: const Text('Remove'), + onPressed: () { + setState(() { + int index = this.document.document.nodes.indexOf(node); + this.document.document.nodes.removeAt(index); + }); + Navigator.of(context, rootNavigator: true).pop(); + }), + new FlatButton( + child: const Text('Update'), + onPressed: () { + setState(() { + int index = this.document.document.nodes.indexOf(node); + this.document.document.nodes[index].data.src = + imgurlController.text; + }); + Navigator.of(context, rootNavigator: true).pop(); + }) + ], + ); + }, + ); + } + + void editYoutubeVideoDialog(String youTubeUrl, SlateNode node) async { + TextEditingController urlController = + TextEditingController(text: node.data.src); + await showDialog( + context: context, + builder: (BuildContext context) => AlertDialog( + contentPadding: const EdgeInsets.all(16.0), + content: new Row( + children: [ + new Expanded( + child: new TextField( + autofocus: true, + keyboardType: TextInputType.url, + controller: urlController, + decoration: new InputDecoration(labelText: 'YouTube URL'), + ), + ) + ], + ), + actions: [ + new FlatButton( + child: const Text('Remove'), + onPressed: () { + setState(() { + int index = this.document.document.nodes.indexOf(node); + this.document.document.nodes.removeAt(index); + }); + Navigator.of(context, rootNavigator: true).pop(); + }), + new FlatButton( + child: const Text('Update'), + onPressed: () { + setState(() { + int index = this.document.document.nodes.indexOf(node); + this.document.document.nodes[index].data.src = + urlController.text; + }); + Navigator.of(context, rootNavigator: true).pop(); + }) + ], + ), + ); + } + + void editVideoDialog(SlateNode node) async { + TextEditingController urlController = + TextEditingController(text: node.data.src); + await showDialog( + context: context, + builder: (BuildContext context) => new AlertDialog( + contentPadding: const EdgeInsets.all(16.0), + content: new Row( + children: [ + new Expanded( + child: new TextField( + autofocus: true, + keyboardType: TextInputType.url, + controller: urlController, + decoration: + new InputDecoration(labelText: 'Video URL (Webm/mp4)'), + ), + ) + ], + ), + actions: [ + new FlatButton( + child: const Text('Remove'), + onPressed: () { + setState(() { + int index = this.document.document.nodes.indexOf(node); + this.document.document.nodes.removeAt(index); + }); + Navigator.of(context, rootNavigator: true).pop(); + }), + new FlatButton( + child: const Text('Update'), + onPressed: () { + setState(() { + int index = this.document.document.nodes.indexOf(node); + this.document.document.nodes[index].data.src = + urlController.text; + }); + + Navigator.of(context, rootNavigator: true).pop(); + }) + ], + ), + ); + } + + void editList(SlateNode node) async { + List listItems = List(); + + node.nodes.forEach((listItem) { + listItems.add(listItem); + }); + + await showDialog( + context: context, + builder: (BuildContext context) => AlertDialog( + title: Text('Edit list'), + contentPadding: const EdgeInsets.all(16.0), + content: ListEditor( + oldListItems: listItems, + onListUpdated: (newListItems) { + setState(() { + int index = this.document.document.nodes.indexOf(node); + this.document.document.nodes[index].nodes = newListItems; + }); + }, + ), + actions: [ + new FlatButton( + child: const Text('Remove'), + onPressed: () { + Navigator.of(context, rootNavigator: true).pop(); + }), + new FlatButton( + child: const Text('Update'), + onPressed: () { + Navigator.of(context, rootNavigator: true).pop(); + }) + ], + ), + ); + } + + void editTwitterEmbed(String youTubeUrl, SlateNode node) async { + TextEditingController urlController = + TextEditingController(text: node.data.src); + await showDialog( + context: context, + builder: (BuildContext context) => AlertDialog( + contentPadding: const EdgeInsets.all(16.0), + content: new Row( + children: [ + new Expanded( + child: new TextField( + autofocus: true, + keyboardType: TextInputType.url, + controller: urlController, + decoration: new InputDecoration(labelText: 'Twitter URL'), + ), + ) + ], + ), + actions: [ + new FlatButton( + child: const Text('Remove'), + onPressed: () { + setState(() { + int index = this.document.document.nodes.indexOf(node); + this.document.document.nodes.removeAt(index); + }); + Navigator.of(context, rootNavigator: true).pop(); + }), + new FlatButton( + child: const Text('Update'), + onPressed: () { + setState(() { + int index = this.document.document.nodes.indexOf(node); + this.document.document.nodes[index].data.src = + urlController.text; + }); + Navigator.of(context, rootNavigator: true).pop(); + }) + ], + ), + ); + } + + void editStrawpollEmbed(SlateNode node) async { + TextEditingController urlController = + TextEditingController(text: node.data.src); + await showDialog( + context: context, + builder: (BuildContext context) => AlertDialog( + contentPadding: const EdgeInsets.all(16.0), + content: new Row( + children: [ + new Expanded( + child: new TextField( + autofocus: true, + keyboardType: TextInputType.url, + controller: urlController, + decoration: new InputDecoration(labelText: 'Strawpoll URL'), + ), + ) + ], + ), + actions: [ + new FlatButton( + child: const Text('Remove'), + onPressed: () { + setState(() { + int index = this.document.document.nodes.indexOf(node); + this.document.document.nodes.removeAt(index); + }); + Navigator.of(context, rootNavigator: true).pop(); + }), + new FlatButton( + child: const Text('Update'), + onPressed: () { + setState(() { + int index = this.document.document.nodes.indexOf(node); + this.document.document.nodes[index].data.src = + urlController.text; + }); + Navigator.of(context, rootNavigator: true).pop(); + }) + ], + ), + ); + } + + void editUserQuote(SlateNode node) async { + await showDialog( + context: context, + builder: (BuildContext context) => AlertDialog( + title: Text('Remove user quote?'), + contentPadding: const EdgeInsets.all(16.0), + content: Text('Are you sure you want to remove the user quote?'), + actions: [ + new FlatButton( + child: const Text('No'), + onPressed: () { + Navigator.of(context, rootNavigator: true).pop(); + }), + new FlatButton( + child: const Text('Yes'), + onPressed: () { + setState(() { + int index = this.document.document.nodes.indexOf(node); + this.document.document.nodes.removeAt(index); + }); + Navigator.of(context, rootNavigator: true).pop(); + }), + ], + ), + ); + } + + @override + Widget build(BuildContext wcontext) { + return DefaultTabController( + length: 2, + child: Scaffold( + key: _scaffoldKey, + appBar: AppBar( + title: Text('Edit post'), + actions: [ + IconButton( + onPressed: !_isPosting ? onPressPost : null, + icon: Icon(Icons.send), + ), + ], + bottom: TabBar( + tabs: [ + Tab( + text: 'Edit', + ), + Tab(text: 'Preview'), + ], + ), + ), + body: KnockoutLoadingIndicator( + show: _isPosting, + child: TabBarView( + physics: NeverScrollableScrollPhysics(), + children: [ + PostEditor( + document: document, + replyList: this.widget.replyList, + // Blocks + onTapTextBlock: this.showTextEditDialog, + onTapImageBlock: this.editImageDialog, + onTapQuoteBlock: this.showTextEditDialog, + onTapYouTubeBlock: this.editYoutubeVideoDialog, + onTapVideoBlock: this.editVideoDialog, + onTapListBlock: this.editList, + onTapTwitterBlock: this.editTwitterEmbed, + onTapUserQuoteBlock: this.editUserQuote, + // Toolbar + onTapAddTextBlock: this.addTextBlock, + onTapAddHeadingOne: () => this.addHeadingBlock('one'), + onTapAddHeadingTwo: () => this.addHeadingBlock('two'), + onTapAddImage: this.addImageDialog, + onTapAddQuote: this.addQuoteBlock, + onTapAddYouTubeVideo: this.addYoutubeVideoDialog, + onTapAddVideo: this.addVideoDialog, + onTapAddBulletedList: () => this.addListBlock('bulleted-list'), + onTapAddNumberedList: () => this.addListBlock('numbered-list'), + onTapAddTwitterEmbed: this.addTwitterEmbed, + onTapAddStrawPollEmbed: this.addStrawpollEmbed, + onTapAddUserQuote: () => this.addUserquoteDialog(context), + onReorderHandler: (int oldIndex, int newIndex) { + if (oldIndex < newIndex) { + // removing the item at oldIndex will shorten the list by 1. + newIndex -= 1; + } + setState(() { + final SlateNode element = + document.document.nodes.removeAt(oldIndex); + document.document.nodes.insert(newIndex, element); + }); + }, + ), + Container( + child: SingleChildScrollView( + child: Container( + padding: EdgeInsets.all(15), + child: PostContent( + content: document, + onTapSpoiler: (text) { + onPressSpoiler(context, text); + }, + scaffoldKey: this._scaffoldKey), + ), + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/screens/thread.dart b/lib/screens/thread.dart index f279a19..921b9cd 100644 --- a/lib/screens/thread.dart +++ b/lib/screens/thread.dart @@ -6,6 +6,7 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:knocky/helpers/api.dart'; import 'package:after_layout/after_layout.dart'; import 'package:knocky/models/thread.dart'; +import 'package:knocky/screens/editPost.dart'; import 'package:knocky/widget/Thread/ThreadPostItem.dart'; import 'package:knocky/widget/KnockoutLoadingIndicator.dart'; import 'package:numberpicker/numberpicker.dart'; @@ -494,13 +495,14 @@ class _ThreadScreenState extends State } void onTapEditPost (BuildContext context, ThreadPost post) async { - /* + final result = await Navigator.push( context, MaterialPageRoute( builder: (context) => EditPostScreen( thread: details, - post: post, + postToEdit: post, + replyList: List(), ), ), ); @@ -514,7 +516,7 @@ class _ThreadScreenState extends State print('Do the scroll'); scrollController.jumpTo(scrollController.position.maxScrollExtent); } - */ + } @override diff --git a/lib/widget/Thread/ThreadPostItem.dart b/lib/widget/Thread/ThreadPostItem.dart index 152db99..cd67223 100644 --- a/lib/widget/Thread/ThreadPostItem.dart +++ b/lib/widget/Thread/ThreadPostItem.dart @@ -120,7 +120,6 @@ class ThreadPostItem extends StatelessWidget { } List ownPostButtons(BuildContext context) { - return []; return [ FlatButton( child: Text('Edit'), From 4b05ae52584eeeba8f70850a9b850542edbfbe4d Mon Sep 17 00:00:00 2001 From: dasmikko Date: Fri, 9 Aug 2019 17:07:06 +0200 Subject: [PATCH 13/19] Use a unified posting screen for easy maintainability --- lib/screens/editPost.dart | 981 -------------------------------------- lib/screens/newPost.dart | 34 +- lib/screens/thread.dart | 6 +- 3 files changed, 29 insertions(+), 992 deletions(-) delete mode 100644 lib/screens/editPost.dart diff --git a/lib/screens/editPost.dart b/lib/screens/editPost.dart deleted file mode 100644 index c25040f..0000000 --- a/lib/screens/editPost.dart +++ /dev/null @@ -1,981 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:font_awesome_flutter/font_awesome_flutter.dart'; -import 'package:knocky/helpers/bbcode.dart'; -import 'package:knocky/models/slateDocument.dart'; -import 'package:knocky/models/thread.dart'; -import 'package:knocky/widget/ListEditor.dart'; -import 'package:knocky/widget/PostEditor.dart'; -import 'package:knocky/helpers/api.dart'; -import 'package:knocky/widget/KnockoutLoadingIndicator.dart'; -import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; -import 'package:knocky/widget/Thread/PostContent.dart'; - -class EditPostScreen extends StatefulWidget { - final ThreadPost postToEdit; - final Thread thread; - final List replyList; - - EditPostScreen({this.postToEdit, this.thread, this.replyList}); - - @override - _EditPostScreenState createState() => _EditPostScreenState(); -} - -class _EditPostScreenState extends State { - SlateObject document; - GlobalKey _scaffoldKey; - bool _isPosting = false; - TextEditingController controller = TextEditingController(); - - @override - void initState() { - super.initState(); - this.document = this.widget.postToEdit.content; - } - - void onPressPost() async { - setState(() { - _isPosting = true; - }); - await KnockoutAPI() - .updatePost(document.toJson(), this.widget.postToEdit.id, this.widget.thread.id) - .catchError((error) { - print(error); - }); - Navigator.pop(context, true); - } - - void refreshPreview() {} - - void onPressSpoiler(BuildContext context, String content) { - showDialog( - context: context, - builder: (BuildContext context) { - // return object of type Dialog - return AlertDialog( - content: new Text(content), - actions: [ - // usually buttons at the bottom of the dialog - new FlatButton( - child: new Text("Close"), - onPressed: () { - Navigator.of(context).pop(); - }, - ), - ], - ); - }, - ); - } - - void addTagAtSelection( - TextEditingController controller, int start, int end, String tag) { - RegExp regExp = new RegExp( - r'(\[([^/].*?)(=(.+?))?\](.*?)\[/\2\]|\[([^/].*?)(=(.+?))?\])', - caseSensitive: false, - multiLine: false, - ); - - String newline = tag == 'h1' || tag == 'h2' ? "\n" : ''; - String selectedText = controller.text.substring(start, end); - String replaceWith = ''; - - if (regExp.hasMatch(selectedText)) { - replaceWith = selectedText.replaceAll( - '[${tag}]', ''); //ignore: unnecessary_brace_in_string_interps - replaceWith = replaceWith.replaceAll( - '[/${tag}]', ''); //ignore: unnecessary_brace_in_string_interps - } else { - replaceWith = newline + - '[${tag}]' + - selectedText + - '[/${tag}]'; //ignore: unnecessary_brace_in_string_interps - } - controller.text = controller.text.replaceRange(start, end, replaceWith); - - refreshPreview(); - } - - /** - * Toolbar callbacks - */ - - void addImageDialog() async { - ClipboardData clipBoardText = await Clipboard.getData('text/plain'); - TextEditingController imgurlController = TextEditingController( - text: clipBoardText != null ? clipBoardText.text : ''); - await showDialog( - context: context, - child: new AlertDialog( - contentPadding: const EdgeInsets.all(16.0), - content: new Row( - children: [ - new Expanded( - child: new TextField( - autofocus: true, - keyboardType: TextInputType.url, - controller: imgurlController, - decoration: new InputDecoration(labelText: 'Image url'), - ), - ) - ], - ), - actions: [ - new FlatButton( - child: const Text('Cancel'), - onPressed: () { - Navigator.of(context, rootNavigator: true).pop(); - }), - new FlatButton( - child: const Text('Insert'), - onPressed: () { - Navigator.of(context, rootNavigator: true).pop(); - setState(() { - document.document.nodes.add( - SlateNode( - object: 'block', - type: 'image', - data: SlateNodeData(src: imgurlController.text), - ), - ); - }); - }) - ], - ), - ); - } - - void addLinkDialog(TextEditingController mainController) async { - ClipboardData clipBoardText = await Clipboard.getData('text/plain'); - TextEditingController urlController = - TextEditingController(text: clipBoardText.text); - await showDialog( - context: context, - child: new AlertDialog( - contentPadding: const EdgeInsets.all(16.0), - content: new Row( - children: [ - new Expanded( - child: new TextField( - autofocus: true, - keyboardType: TextInputType.url, - controller: urlController, - decoration: new InputDecoration(labelText: 'Url'), - ), - ) - ], - ), - actions: [ - new FlatButton( - child: const Text('Cancel'), - onPressed: () { - Navigator.of(context, rootNavigator: true).pop(); - }), - new FlatButton( - child: const Text('Insert'), - onPressed: () { - Navigator.of(context, rootNavigator: true).pop(); - - if (mainController.text.endsWith('\n') || - controller.text.isEmpty) { - mainController.text = - mainController.text + '[url]${urlController.text}[/url]'; - } else { - mainController.text = mainController.text + - '\n[url]${urlController.text}[/url]'; - } - - refreshPreview(); - }) - ], - ), - ); - } - - void addYoutubeVideoDialog() async { - ClipboardData clipBoardText = await Clipboard.getData('text/plain'); - TextEditingController urlController = - TextEditingController(text: clipBoardText.text); - await showDialog( - context: context, - child: new AlertDialog( - contentPadding: const EdgeInsets.all(16.0), - content: new Row( - children: [ - new Expanded( - child: new TextField( - autofocus: true, - keyboardType: TextInputType.url, - controller: urlController, - decoration: new InputDecoration(labelText: 'YouTube URL'), - ), - ) - ], - ), - actions: [ - new FlatButton( - child: const Text('Cancel'), - onPressed: () { - Navigator.of(context, rootNavigator: true).pop(); - }), - new FlatButton( - child: const Text('Insert'), - onPressed: () { - setState(() { - this.document.document.nodes.add(SlateNode( - type: 'youtube', - data: SlateNodeData(src: urlController.text))); - }); - - Navigator.of(context, rootNavigator: true).pop(); - }) - ], - ), - ); - } - - void addVideoDialog() async { - ClipboardData clipBoardText = await Clipboard.getData('text/plain'); - TextEditingController urlController = - TextEditingController(text: clipBoardText.text); - await showDialog( - context: context, - builder: (BuildContext context) => new AlertDialog( - contentPadding: const EdgeInsets.all(16.0), - content: new Row( - children: [ - new Expanded( - child: new TextField( - autofocus: true, - keyboardType: TextInputType.url, - controller: urlController, - decoration: - new InputDecoration(labelText: 'Video URL (Webm/mp4)'), - ), - ) - ], - ), - actions: [ - new FlatButton( - child: const Text('Cancel'), - onPressed: () { - Navigator.of(context, rootNavigator: true).pop(); - }), - new FlatButton( - child: const Text('Insert'), - onPressed: () { - setState(() { - this.document.document.nodes.add( - SlateNode( - type: 'video', - data: SlateNodeData(src: urlController.text), - ), - ); - }); - - Navigator.of(context, rootNavigator: true).pop(); - }) - ], - ), - ); - } - - void addUserquoteDialog(bcontext) async { - await showDialog( - context: bcontext, - child: new AlertDialog( - title: Text('Select post'), - contentPadding: const EdgeInsets.all(16.0), - content: Container( - height: 400, - width: 200, - child: ListView.builder( - itemCount: this.widget.replyList.length, - itemBuilder: (BuildContext context, int index) { - ThreadPost item = this.widget.replyList[index]; - return ListTile( - title: Text(item.user.username), - onTap: () { - setState(() { - this.document.document.nodes.add( - SlateNode( - object: 'block', - type: 'userquote', - data: SlateNodeData( - postData: NodeDataPostData( - postId: item.id, - threadId: this.widget.thread.id, - threadPage: this.widget.thread.currentPage, - username: item.user.username, - ), - ), - nodes: item.content.document.nodes), - ); - }); - Navigator.of(bcontext, rootNavigator: true).pop(); - }, - ); - }, - ), - ), - actions: [ - new FlatButton( - child: const Text('Cancel'), - onPressed: () { - Navigator.of(bcontext, rootNavigator: true).pop(); - }), - ], - ), - ); - } - - void addTextBlock() { - setState( - () { - this.document.document.nodes.add( - SlateNode( - type: 'paragraph', - object: 'block', - nodes: [ - SlateNode( - object: 'text', - leaves: [], - ), - ], - ), - ); - }, - ); - } - - void addHeadingBlock(String type) { - setState( - () { - this.document.document.nodes.add( - SlateNode( - type: 'heading-' + type, - object: 'block', - nodes: [ - SlateNode( - object: 'text', - leaves: [], - ), - ], - ), - ); - }, - ); - } - - void addQuoteBlock() { - setState(() { - this - .document - .document - .nodes - .add(SlateNode(type: 'block-quote', nodes: List())); - }); - } - - void addListBlock(String type) { - setState(() { - this - .document - .document - .nodes - .add(SlateNode(object: 'block', type: type, nodes: List())); - }); - } - - void addTwitterEmbed() async { - ClipboardData clipBoardText = await Clipboard.getData('text/plain'); - TextEditingController urlController = - TextEditingController(text: clipBoardText.text); - await showDialog( - context: context, - child: new AlertDialog( - contentPadding: const EdgeInsets.all(16.0), - content: new Row( - children: [ - new Expanded( - child: new TextField( - autofocus: true, - keyboardType: TextInputType.url, - controller: urlController, - decoration: new InputDecoration(labelText: 'Twitter URL'), - ), - ) - ], - ), - actions: [ - new FlatButton( - child: const Text('Cancel'), - onPressed: () { - Navigator.of(context, rootNavigator: true).pop(); - }), - new FlatButton( - child: const Text('Insert'), - onPressed: () { - setState(() { - this.document.document.nodes.add( - SlateNode( - type: 'twitter', - object: 'block', - data: SlateNodeData(src: urlController.text), - nodes: [ - SlateNode( - object: 'text', - leaves: [ - SlateLeaf(text: '', marks: [], object: 'leaf') - ], - ), - ]), - ); - }); - - Navigator.of(context, rootNavigator: true).pop(); - }) - ], - ), - ); - } - - void addStrawpollEmbed() async { - ClipboardData clipBoardText = await Clipboard.getData('text/plain'); - TextEditingController urlController = - TextEditingController(text: clipBoardText.text); - await showDialog( - context: context, - child: new AlertDialog( - contentPadding: const EdgeInsets.all(16.0), - content: new Row( - children: [ - new Expanded( - child: new TextField( - autofocus: true, - keyboardType: TextInputType.url, - controller: urlController, - decoration: new InputDecoration(labelText: 'Strawpoll URL'), - ), - ) - ], - ), - actions: [ - new FlatButton( - child: const Text('Cancel'), - onPressed: () { - Navigator.of(context, rootNavigator: true).pop(); - }), - new FlatButton( - child: const Text('Insert'), - onPressed: () { - setState(() { - this.document.document.nodes.add( - SlateNode( - type: 'strawpoll', - object: 'block', - data: SlateNodeData(src: urlController.text), - nodes: [ - SlateNode( - object: 'text', - leaves: [ - SlateLeaf(text: '', marks: [], object: 'leaf') - ], - ), - ]), - ); - }); - - Navigator.of(context, rootNavigator: true).pop(); - }) - ], - ), - ); - } - - /* - Block tap callbacks - */ - void showTextEditDialog(BuildContext context, SlateNode node) { - String bbcodeText = BBCodeHandler().slateParagraphToBBCode(node); - TextEditingController textEditingController = - TextEditingController(text: bbcodeText); - - showDialog( - context: context, - builder: (BuildContext bcontext) { - return AlertDialog( - actions: [ - FlatButton( - child: Text('Remove'), - onPressed: () { - setState(() { - int index = this.document.document.nodes.indexOf(node); - this.document.document.nodes.removeAt(index); - }); - Navigator.pop(bcontext); - }, - ), - FlatButton( - child: Text('Save'), - onPressed: () { - SlateNode newNode = BBCodeHandler() - .parse(textEditingController.text, type: node.type); - print(BBCodeHandler().slateParagraphToBBCode(newNode)); - print(newNode.toJson()); - Navigator.pop(bcontext, newNode); - }, - ) - ], - title: Text('Edit text block'), - content: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - mainAxisAlignment: MainAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Container( - margin: EdgeInsets.only(bottom: 10), - child: TextField( - keyboardType: TextInputType.multiline, - maxLines: null, - textCapitalization: TextCapitalization.sentences, - controller: textEditingController, - ), - ), - Container( - color: Colors.grey[600], - padding: EdgeInsets.all(0), - child: Wrap( - children: [ - IconButton( - icon: Icon(Icons.format_bold), - onPressed: () { - TextSelection theSelection = - textEditingController.selection; - addTagAtSelection(textEditingController, - theSelection.start, theSelection.end, 'b'); - }, - ), - IconButton( - icon: Icon(Icons.format_italic), - onPressed: () { - TextSelection theSelection = - textEditingController.selection; - addTagAtSelection(textEditingController, - theSelection.start, theSelection.end, 'i'); - }, - ), - IconButton( - icon: Icon(Icons.format_underlined), - onPressed: () { - TextSelection theSelection = - textEditingController.selection; - addTagAtSelection(textEditingController, - theSelection.start, theSelection.end, 'u'); - }, - ), - IconButton( - icon: Icon(Icons.code), - onPressed: () { - TextSelection theSelection = - textEditingController.selection; - addTagAtSelection(textEditingController, - theSelection.start, theSelection.end, 'code'); - }, - ), - IconButton( - icon: Icon(Icons.link), - onPressed: () { - addLinkDialog(textEditingController); - }, - ), - ], - ), - ), - ], - ), - ); - }).then((newNode) { - setState(() { - if (newNode != null) { - int index = this.document.document.nodes.indexOf(node); - this.document.document.nodes[index] = newNode; - } - }); - }); - } - - void editImageDialog(slateObject, SlateNode node) async { - TextEditingController imgurlController = - TextEditingController(text: node.data.src); - await showDialog( - context: context, - builder: (BuildContext context) { - return new AlertDialog( - contentPadding: const EdgeInsets.all(16.0), - content: new Row( - children: [ - new Expanded( - child: new TextField( - autofocus: true, - keyboardType: TextInputType.url, - controller: imgurlController, - decoration: new InputDecoration(labelText: 'Image url'), - ), - ) - ], - ), - actions: [ - new FlatButton( - child: const Text('Remove'), - onPressed: () { - setState(() { - int index = this.document.document.nodes.indexOf(node); - this.document.document.nodes.removeAt(index); - }); - Navigator.of(context, rootNavigator: true).pop(); - }), - new FlatButton( - child: const Text('Update'), - onPressed: () { - setState(() { - int index = this.document.document.nodes.indexOf(node); - this.document.document.nodes[index].data.src = - imgurlController.text; - }); - Navigator.of(context, rootNavigator: true).pop(); - }) - ], - ); - }, - ); - } - - void editYoutubeVideoDialog(String youTubeUrl, SlateNode node) async { - TextEditingController urlController = - TextEditingController(text: node.data.src); - await showDialog( - context: context, - builder: (BuildContext context) => AlertDialog( - contentPadding: const EdgeInsets.all(16.0), - content: new Row( - children: [ - new Expanded( - child: new TextField( - autofocus: true, - keyboardType: TextInputType.url, - controller: urlController, - decoration: new InputDecoration(labelText: 'YouTube URL'), - ), - ) - ], - ), - actions: [ - new FlatButton( - child: const Text('Remove'), - onPressed: () { - setState(() { - int index = this.document.document.nodes.indexOf(node); - this.document.document.nodes.removeAt(index); - }); - Navigator.of(context, rootNavigator: true).pop(); - }), - new FlatButton( - child: const Text('Update'), - onPressed: () { - setState(() { - int index = this.document.document.nodes.indexOf(node); - this.document.document.nodes[index].data.src = - urlController.text; - }); - Navigator.of(context, rootNavigator: true).pop(); - }) - ], - ), - ); - } - - void editVideoDialog(SlateNode node) async { - TextEditingController urlController = - TextEditingController(text: node.data.src); - await showDialog( - context: context, - builder: (BuildContext context) => new AlertDialog( - contentPadding: const EdgeInsets.all(16.0), - content: new Row( - children: [ - new Expanded( - child: new TextField( - autofocus: true, - keyboardType: TextInputType.url, - controller: urlController, - decoration: - new InputDecoration(labelText: 'Video URL (Webm/mp4)'), - ), - ) - ], - ), - actions: [ - new FlatButton( - child: const Text('Remove'), - onPressed: () { - setState(() { - int index = this.document.document.nodes.indexOf(node); - this.document.document.nodes.removeAt(index); - }); - Navigator.of(context, rootNavigator: true).pop(); - }), - new FlatButton( - child: const Text('Update'), - onPressed: () { - setState(() { - int index = this.document.document.nodes.indexOf(node); - this.document.document.nodes[index].data.src = - urlController.text; - }); - - Navigator.of(context, rootNavigator: true).pop(); - }) - ], - ), - ); - } - - void editList(SlateNode node) async { - List listItems = List(); - - node.nodes.forEach((listItem) { - listItems.add(listItem); - }); - - await showDialog( - context: context, - builder: (BuildContext context) => AlertDialog( - title: Text('Edit list'), - contentPadding: const EdgeInsets.all(16.0), - content: ListEditor( - oldListItems: listItems, - onListUpdated: (newListItems) { - setState(() { - int index = this.document.document.nodes.indexOf(node); - this.document.document.nodes[index].nodes = newListItems; - }); - }, - ), - actions: [ - new FlatButton( - child: const Text('Remove'), - onPressed: () { - Navigator.of(context, rootNavigator: true).pop(); - }), - new FlatButton( - child: const Text('Update'), - onPressed: () { - Navigator.of(context, rootNavigator: true).pop(); - }) - ], - ), - ); - } - - void editTwitterEmbed(String youTubeUrl, SlateNode node) async { - TextEditingController urlController = - TextEditingController(text: node.data.src); - await showDialog( - context: context, - builder: (BuildContext context) => AlertDialog( - contentPadding: const EdgeInsets.all(16.0), - content: new Row( - children: [ - new Expanded( - child: new TextField( - autofocus: true, - keyboardType: TextInputType.url, - controller: urlController, - decoration: new InputDecoration(labelText: 'Twitter URL'), - ), - ) - ], - ), - actions: [ - new FlatButton( - child: const Text('Remove'), - onPressed: () { - setState(() { - int index = this.document.document.nodes.indexOf(node); - this.document.document.nodes.removeAt(index); - }); - Navigator.of(context, rootNavigator: true).pop(); - }), - new FlatButton( - child: const Text('Update'), - onPressed: () { - setState(() { - int index = this.document.document.nodes.indexOf(node); - this.document.document.nodes[index].data.src = - urlController.text; - }); - Navigator.of(context, rootNavigator: true).pop(); - }) - ], - ), - ); - } - - void editStrawpollEmbed(SlateNode node) async { - TextEditingController urlController = - TextEditingController(text: node.data.src); - await showDialog( - context: context, - builder: (BuildContext context) => AlertDialog( - contentPadding: const EdgeInsets.all(16.0), - content: new Row( - children: [ - new Expanded( - child: new TextField( - autofocus: true, - keyboardType: TextInputType.url, - controller: urlController, - decoration: new InputDecoration(labelText: 'Strawpoll URL'), - ), - ) - ], - ), - actions: [ - new FlatButton( - child: const Text('Remove'), - onPressed: () { - setState(() { - int index = this.document.document.nodes.indexOf(node); - this.document.document.nodes.removeAt(index); - }); - Navigator.of(context, rootNavigator: true).pop(); - }), - new FlatButton( - child: const Text('Update'), - onPressed: () { - setState(() { - int index = this.document.document.nodes.indexOf(node); - this.document.document.nodes[index].data.src = - urlController.text; - }); - Navigator.of(context, rootNavigator: true).pop(); - }) - ], - ), - ); - } - - void editUserQuote(SlateNode node) async { - await showDialog( - context: context, - builder: (BuildContext context) => AlertDialog( - title: Text('Remove user quote?'), - contentPadding: const EdgeInsets.all(16.0), - content: Text('Are you sure you want to remove the user quote?'), - actions: [ - new FlatButton( - child: const Text('No'), - onPressed: () { - Navigator.of(context, rootNavigator: true).pop(); - }), - new FlatButton( - child: const Text('Yes'), - onPressed: () { - setState(() { - int index = this.document.document.nodes.indexOf(node); - this.document.document.nodes.removeAt(index); - }); - Navigator.of(context, rootNavigator: true).pop(); - }), - ], - ), - ); - } - - @override - Widget build(BuildContext wcontext) { - return DefaultTabController( - length: 2, - child: Scaffold( - key: _scaffoldKey, - appBar: AppBar( - title: Text('Edit post'), - actions: [ - IconButton( - onPressed: !_isPosting ? onPressPost : null, - icon: Icon(Icons.send), - ), - ], - bottom: TabBar( - tabs: [ - Tab( - text: 'Edit', - ), - Tab(text: 'Preview'), - ], - ), - ), - body: KnockoutLoadingIndicator( - show: _isPosting, - child: TabBarView( - physics: NeverScrollableScrollPhysics(), - children: [ - PostEditor( - document: document, - replyList: this.widget.replyList, - // Blocks - onTapTextBlock: this.showTextEditDialog, - onTapImageBlock: this.editImageDialog, - onTapQuoteBlock: this.showTextEditDialog, - onTapYouTubeBlock: this.editYoutubeVideoDialog, - onTapVideoBlock: this.editVideoDialog, - onTapListBlock: this.editList, - onTapTwitterBlock: this.editTwitterEmbed, - onTapUserQuoteBlock: this.editUserQuote, - // Toolbar - onTapAddTextBlock: this.addTextBlock, - onTapAddHeadingOne: () => this.addHeadingBlock('one'), - onTapAddHeadingTwo: () => this.addHeadingBlock('two'), - onTapAddImage: this.addImageDialog, - onTapAddQuote: this.addQuoteBlock, - onTapAddYouTubeVideo: this.addYoutubeVideoDialog, - onTapAddVideo: this.addVideoDialog, - onTapAddBulletedList: () => this.addListBlock('bulleted-list'), - onTapAddNumberedList: () => this.addListBlock('numbered-list'), - onTapAddTwitterEmbed: this.addTwitterEmbed, - onTapAddStrawPollEmbed: this.addStrawpollEmbed, - onTapAddUserQuote: () => this.addUserquoteDialog(context), - onReorderHandler: (int oldIndex, int newIndex) { - if (oldIndex < newIndex) { - // removing the item at oldIndex will shorten the list by 1. - newIndex -= 1; - } - setState(() { - final SlateNode element = - document.document.nodes.removeAt(oldIndex); - document.document.nodes.insert(newIndex, element); - }); - }, - ), - Container( - child: SingleChildScrollView( - child: Container( - padding: EdgeInsets.all(15), - child: PostContent( - content: document, - onTapSpoiler: (text) { - onPressSpoiler(context, text); - }, - scaffoldKey: this._scaffoldKey), - ), - ), - ), - ], - ), - ), - ), - ); - } -} diff --git a/lib/screens/newPost.dart b/lib/screens/newPost.dart index 901f8ff..66a911a 100644 --- a/lib/screens/newPost.dart +++ b/lib/screens/newPost.dart @@ -12,11 +12,13 @@ import 'package:material_design_icons_flutter/material_design_icons_flutter.dart import 'package:knocky/widget/Thread/PostContent.dart'; class NewPostScreen extends StatefulWidget { - final ThreadPost replyTo; + final ThreadPost post; final Thread thread; final List replyList; + final bool editingPost; - NewPostScreen({this.replyTo, this.thread, this.replyList}); + NewPostScreen( + {this.post, this.thread, this.replyList, this.editingPost = false}); @override _NewPostScreenState createState() => _NewPostScreenState(); @@ -34,6 +36,11 @@ class _NewPostScreenState extends State { @override void initState() { super.initState(); + + if (this.widget.editingPost) { + this.document = this.widget.post.content; + } + if (this.widget.replyList.length == 1) { ThreadPost item = this.widget.replyList.first; this.document.document.nodes.add( @@ -57,11 +64,22 @@ class _NewPostScreenState extends State { setState(() { _isPosting = true; }); - await KnockoutAPI() - .newPost(document.toJson(), this.widget.thread.id) - .catchError((error) { - print(error); - }); + + if (this.widget.editingPost) { + await KnockoutAPI() + .updatePost( + document.toJson(), this.widget.post.id, this.widget.thread.id) + .catchError((error) { + print(error); + }); + } else { + await KnockoutAPI() + .newPost(document.toJson(), this.widget.thread.id) + .catchError((error) { + print(error); + }); + } + Navigator.pop(context, true); } @@ -920,7 +938,7 @@ class _NewPostScreenState extends State { child: Scaffold( key: _scaffoldKey, appBar: AppBar( - title: Text('New post'), + title: Text(this.widget.editingPost ? 'Edit post ' : 'New post'), actions: [ IconButton( onPressed: !_isPosting ? onPressPost : null, diff --git a/lib/screens/thread.dart b/lib/screens/thread.dart index 921b9cd..7689faa 100644 --- a/lib/screens/thread.dart +++ b/lib/screens/thread.dart @@ -6,7 +6,6 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:knocky/helpers/api.dart'; import 'package:after_layout/after_layout.dart'; import 'package:knocky/models/thread.dart'; -import 'package:knocky/screens/editPost.dart'; import 'package:knocky/widget/Thread/ThreadPostItem.dart'; import 'package:knocky/widget/KnockoutLoadingIndicator.dart'; import 'package:numberpicker/numberpicker.dart'; @@ -499,9 +498,10 @@ class _ThreadScreenState extends State final result = await Navigator.push( context, MaterialPageRoute( - builder: (context) => EditPostScreen( + builder: (context) => NewPostScreen( + editingPost: true, thread: details, - postToEdit: post, + post: post, replyList: List(), ), ), From 231c4a1a2e1b76190666559f515407223b02e1a1 Mon Sep 17 00:00:00 2001 From: dasmikko Date: Fri, 9 Aug 2019 17:10:18 +0200 Subject: [PATCH 14/19] added edit strawpoll --- lib/screens/newPost.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/screens/newPost.dart b/lib/screens/newPost.dart index 66a911a..37ce571 100644 --- a/lib/screens/newPost.dart +++ b/lib/screens/newPost.dart @@ -860,7 +860,7 @@ class _NewPostScreenState extends State { ); } - void editStrawpollEmbed(SlateNode node) async { + void editStrawpollEmbed(String url, SlateNode node) async { TextEditingController urlController = TextEditingController(text: node.data.src); await showDialog( @@ -971,6 +971,7 @@ class _NewPostScreenState extends State { onTapListBlock: this.editList, onTapTwitterBlock: this.editTwitterEmbed, onTapUserQuoteBlock: this.editUserQuote, + onTapStrawpollBlock: this.editStrawpollEmbed, // Toolbar onTapAddTextBlock: this.addTextBlock, onTapAddHeadingOne: () => this.addHeadingBlock('one'), From ec44726d45797d58a688b74a16972bb881521604 Mon Sep 17 00:00:00 2001 From: dasmikko Date: Mon, 12 Aug 2019 10:15:33 +0200 Subject: [PATCH 15/19] Add refresh button --- lib/screens/thread.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/screens/thread.dart b/lib/screens/thread.dart index 7689faa..c126546 100644 --- a/lib/screens/thread.dart +++ b/lib/screens/thread.dart @@ -483,6 +483,9 @@ class _ThreadScreenState extends State void onSelectOverflowItem(int item) { switch (item) { + case 0: + refreshPage(); + break; case 1: onTapSubscribe(context); break; @@ -532,6 +535,7 @@ class _ThreadScreenState extends State onSelected: onSelectOverflowItem, itemBuilder: (BuildContext context) { return [ + overFlowItem(Icon(Icons.refresh), 'Refresh', 0), if (details != null) overFlowItem( Icon(FontAwesomeIcons.eye), 'Subscribe to thread', 1), From 5da8c1c1d90486e387b09053877145924c6384a6 Mon Sep 17 00:00:00 2001 From: dasmikko Date: Mon, 12 Aug 2019 10:15:49 +0200 Subject: [PATCH 16/19] Better cloning --- lib/screens/thread.dart | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/lib/screens/thread.dart b/lib/screens/thread.dart index c126546..2808504 100644 --- a/lib/screens/thread.dart +++ b/lib/screens/thread.dart @@ -5,6 +5,7 @@ import 'package:flutter/rendering.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:knocky/helpers/api.dart'; import 'package:after_layout/after_layout.dart'; +import 'package:knocky/models/slateDocument.dart'; import 'package:knocky/models/thread.dart'; import 'package:knocky/widget/Thread/ThreadPostItem.dart'; import 'package:knocky/widget/KnockoutLoadingIndicator.dart'; @@ -356,17 +357,11 @@ class _ThreadScreenState extends State void onPressReply(ThreadPost post) async { if (postsToReplyTo.length > 0) { - onLongPressReply(post); + onLongPressReply(new ThreadPost.clone(post)); } else { List reply = List(); reply.add( - new ThreadPost( - bans: post.bans, - content: post.content, - createdAt: post.createdAt, - id: post.id, - ratings: post.ratings, - user: post.user), + new ThreadPost.clone(post), ); final result = await Navigator.push( From 0bbc1bfb9ed791385968b35556ba44bdf2d506fb Mon Sep 17 00:00:00 2001 From: dasmikko Date: Mon, 12 Aug 2019 10:16:10 +0200 Subject: [PATCH 17/19] Convert the embeds to text for when user quoting --- lib/screens/newPost.dart | 49 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 44 insertions(+), 5 deletions(-) diff --git a/lib/screens/newPost.dart b/lib/screens/newPost.dart index 37ce571..7f37354 100644 --- a/lib/screens/newPost.dart +++ b/lib/screens/newPost.dart @@ -32,17 +32,24 @@ class _NewPostScreenState extends State { GlobalKey _scaffoldKey; bool _isPosting = false; TextEditingController controller = TextEditingController(); + List replyListConverted = List(); @override void initState() { super.initState(); + this.replyListConverted = this.widget.replyList; + if (this.widget.editingPost) { this.document = this.widget.post.content; + } else { + if (this.replyListConverted.length > 0) { + this.convertReplyEmbedsToText(); + } } - if (this.widget.replyList.length == 1) { - ThreadPost item = this.widget.replyList.first; + if (this.replyListConverted.length == 1) { + ThreadPost item = this.replyListConverted.first; this.document.document.nodes.add( SlateNode( object: 'block', @@ -60,6 +67,38 @@ class _NewPostScreenState extends State { } } + void convertReplyEmbedsToText() { + print('Convert embeds'); + setState(() { + this.replyListConverted.forEach((reply) { + reply.content.document.nodes.forEach((node) { + switch (node.type) { + case 'image': + case 'youtube': + case 'video': + case 'strawpoll': + case 'twitter': + print('Should be convertet'); + int replyindex = this.replyListConverted.indexOf(reply); + int nodeIndex = reply.content.document.nodes.indexOf(node); + + this.replyListConverted[replyindex].content.document.nodes.removeAt(nodeIndex); + this.replyListConverted[replyindex].content.document.nodes.insert(nodeIndex, embedConverter(node.type, node.data.src)); + break; + } + }); + }); + }); + } + + SlateNode embedConverter(String type, String url) { + return new SlateNode(type: 'paragraph', object: 'block', nodes: [ + SlateNode(object: 'text', leaves: [ + SlateLeaf(text: '[${type}: ${url}]', marks: [], object: 'leaf') + ]) + ]); + } + void onPressPost() async { setState(() { _isPosting = true; @@ -328,9 +367,9 @@ class _NewPostScreenState extends State { height: 400, width: 200, child: ListView.builder( - itemCount: this.widget.replyList.length, + itemCount: this.replyListConverted.length, itemBuilder: (BuildContext context, int index) { - ThreadPost item = this.widget.replyList[index]; + ThreadPost item = this.replyListConverted[index]; return ListTile( title: Text(item.user.username), onTap: () { @@ -961,7 +1000,7 @@ class _NewPostScreenState extends State { children: [ PostEditor( document: document, - replyList: this.widget.replyList, + replyList: this.replyListConverted, // Blocks onTapTextBlock: this.showTextEditDialog, onTapImageBlock: this.editImageDialog, From 16fe2309a5aab3247872bdc20e72a2b539c7e19e Mon Sep 17 00:00:00 2001 From: dasmikko Date: Mon, 12 Aug 2019 10:16:24 +0200 Subject: [PATCH 18/19] Added clone to models --- lib/models/slateDocument.dart | 2 ++ lib/models/thread.dart | 2 ++ 2 files changed, 4 insertions(+) diff --git a/lib/models/slateDocument.dart b/lib/models/slateDocument.dart index 4acc1cd..f46c795 100644 --- a/lib/models/slateDocument.dart +++ b/lib/models/slateDocument.dart @@ -12,6 +12,8 @@ class SlateObject { factory SlateObject.fromJson(Map json) => _$SlateObjectFromJson(json); Map toJson() => _$SlateObjectToJson(this); + + SlateObject.clone(SlateObject slateObject): this(object: slateObject.object, document: slateObject.document); } Map _documentToJson(SlateDocument document) => document.toJson(); diff --git a/lib/models/thread.dart b/lib/models/thread.dart index 60d5da5..2fb8b2f 100644 --- a/lib/models/thread.dart +++ b/lib/models/thread.dart @@ -81,6 +81,8 @@ class ThreadPost { factory ThreadPost.fromJson(Map json) => _$ThreadPostFromJson(json); Map toJson() => _$ThreadPostToJson(this); + + ThreadPost.clone(ThreadPost post): this(id: post.id, content: SlateObject.fromJson(post.content.toJson()), user: post.user, ratings: post.ratings, createdAt: post.createdAt, bans: post.bans); } SlateObject _contentFromJson(String jsonString) { From 91831cb44486d5ef05a7bb02e50e2f00575c4f22 Mon Sep 17 00:00:00 2001 From: dasmikko Date: Mon, 12 Aug 2019 16:29:00 +0200 Subject: [PATCH 19/19] Bump version --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 18792dc..a0cc8e8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,7 +11,7 @@ description: A knockout! client. # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 1.2.6+20 +version: 1.3.0+21 environment: sdk: ">=2.2.2 <3.0.0"