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/helpers/api.dart b/lib/helpers/api.dart index 5257af4..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 { @@ -170,6 +167,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,33 +179,38 @@ class KnockoutAPI { }); } on DioError catch (e) { print(e); + print(e.response); } } - 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/helpers/bbcode.dart b/lib/helpers/bbcode.dart index de80bd4..b2bac19 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,127 +143,27 @@ 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 blocks - 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': - 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]'); + // 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]'); + }); }); - 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); - }); - }); - } - } + } 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) { 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) { 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/home.dart b/lib/screens/home.dart index 63c7eb2..08a5a2e 100644 --- a/lib/screens/home.dart +++ b/lib/screens/home.dart @@ -2,9 +2,13 @@ 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/screens/thread.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'; @@ -12,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(); @@ -30,9 +40,46 @@ 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') + ]); + + 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 @@ -49,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/newPost.dart b/lib/screens/newPost.dart index 7495be5..7f37354 100644 --- a/lib/screens/newPost.dart +++ b/lib/screens/newPost.dart @@ -1,56 +1,129 @@ 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/SlateDocumentParser/SlateDocumentParser.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 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}); + {this.post, this.thread, this.replyList, this.editingPost = false}); @override _NewPostScreenState createState() => _NewPostScreenState(); } class _NewPostScreenState extends State { - TextEditingController controller = TextEditingController(text: ''); - SlateObject document; + SlateObject document = new SlateObject( + object: 'value', + document: SlateDocument(object: 'document', nodes: List()), + ); GlobalKey _scaffoldKey; - FocusNode textFocusNode = FocusNode(); bool _isPosting = false; - - List history = List(); + TextEditingController controller = TextEditingController(); + List replyListConverted = List(); @override void initState() { super.initState(); - document = BBCodeHandler() - .parse(controller.text, this.widget.thread, this.widget.replyList); + 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.replyListConverted.length == 1) { + ThreadPost item = this.replyListConverted.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 { + void convertReplyEmbedsToText() { + print('Convert embeds'); setState(() { - _isPosting = true; + 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; + } + }); + }); }); - await KnockoutAPI().newPost(document.toJson(), this.widget.thread.id); - Navigator.pop(context, true); } - void refreshPreview() { + 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(() { - document = BBCodeHandler() - .parse(controller.text, this.widget.thread, this.widget.replyList); + _isPosting = true; }); + + 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); } + void refreshPreview() {} + void onPressSpoiler(BuildContext context, String content) { showDialog( context: context, @@ -72,7 +145,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,20 +158,29 @@ 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 = - TextEditingController(text: clipBoardText.text); + TextEditingController imgurlController = TextEditingController( + text: clipBoardText != null ? clipBoardText.text : ''); await showDialog( context: context, child: new AlertDialog( @@ -124,22 +207,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 +253,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(); @@ -214,16 +298,13 @@ 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: 'youtube', + data: SlateNodeData(src: urlController.text))); + }); - 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(); + Navigator.of(context, rootNavigator: true).pop(); }) ], ), @@ -236,7 +317,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: [ @@ -260,16 +341,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(); }) ], ), @@ -286,22 +367,29 @@ 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: () { + 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(); }, ); }, @@ -318,6 +406,570 @@ 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: [], + ), + ], + ), + ); + }, + ); + } + + 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(String url, 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( @@ -325,7 +977,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, @@ -346,141 +998,54 @@ 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, + replyList: this.replyListConverted, + // 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, + onTapStrawpollBlock: this.editStrawpollEmbed, + // 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: SlateDocumentParser( - context: context, - scaffoldkey: _scaffoldKey, - slateObject: document, - onPressSpoiler: (content) { - onPressSpoiler(context, content); - }, - ), + 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 31c128c..2808504 100644 --- a/lib/screens/thread.dart +++ b/lib/screens/thread.dart @@ -5,8 +5,8 @@ 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/screens/editPost.dart'; import 'package:knocky/widget/Thread/ThreadPostItem.dart'; import 'package:knocky/widget/KnockoutLoadingIndicator.dart'; import 'package:numberpicker/numberpicker.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(); }); @@ -353,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( @@ -480,6 +478,9 @@ class _ThreadScreenState extends State void onSelectOverflowItem(int item) { switch (item) { + case 0: + refreshPage(); + break; case 1: onTapSubscribe(context); break; @@ -491,12 +492,15 @@ class _ThreadScreenState extends State } void onTapEditPost (BuildContext context, ThreadPost post) async { + final result = await Navigator.push( context, MaterialPageRoute( - builder: (context) => EditPostScreen( + builder: (context) => NewPostScreen( + editingPost: true, thread: details, post: post, + replyList: List(), ), ), ); @@ -510,6 +514,7 @@ class _ThreadScreenState extends State print('Do the scroll'); scrollController.jumpTo(scrollController.position.maxScrollExtent); } + } @override @@ -525,6 +530,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), 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, ), ) 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 new file mode 100644 index 0000000..b19f2a0 --- /dev/null +++ b/lib/widget/PostEditor.dart @@ -0,0 +1,435 @@ +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'; + +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; + final Function onTapStrawpollBlock; + + // 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; + final Function onTapAddBulletedList; + final Function onTapAddNumberedList; + final Function onTapAddUserQuote; + final Function onTapAddTwitterEmbed; + final Function 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) { + 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), + ); + } + + 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) + ], + ), + ), + ); + }); + + 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 -'), + ), + ); + } + + 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); + }, + 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)], + ), + ), + ); + } + + Widget 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 -'), + ), + ), + ); + } + + 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 -')), + ), + ); + } + + Widget _twitterHandler(String twitterUrl, SlateNode node) { + return ListTile( + onTap: () => this.widget.onTapTwitterBlock(twitterUrl, node), + leading: Icon(MdiIcons.twitter), + title: Text(twitterUrl), + ); + } + + Widget _strawpollHandler(SlateNode node) { + return ListTile( + onTap: () => this.widget.onTapStrawpollBlock(node.data.src, node), + leading: Icon(MdiIcons.poll), + title: Text(node.data.src), + ); + } + + 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, + 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, + twitterEmbedHandler: this._twitterHandler, + strawpollHandler: this._strawpollHandler, + userQuoteHandler: this._userquoteHandler, + ) + .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.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, + ), + IconButton( + icon: Icon(Icons.ondemand_video), + onPressed: this.widget.onTapAddYouTubeVideo, + ), + IconButton( + icon: Icon(Icons.videocam), + onPressed: this.widget.onTapAddVideo, + ), + IconButton( + icon: Icon(MdiIcons.twitter), + onPressed: this.widget.onTapAddTwitterEmbed, + ), + IconButton( + 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/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 d9f7035..01c1dd7 100644 --- a/lib/widget/SlateDocumentParser/SlateDocumentParser.dart +++ b/lib/widget/SlateDocumentParser/SlateDocumentParser.dart @@ -1,66 +1,49 @@ 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'; +import 'package:knocky/widget/SlateDocumentParser/SlateDocumentController.dart'; class SlateDocumentParser extends StatelessWidget { final SlateObject slateObject; 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; + final Function paragraphHandler; + final Function headingHandler; + final Function strawpollHandler; + 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, + @required this.twitterEmbedHandler, + @required this.userQuoteHandler, + @required this.bulletedListHandler, + @required this.numberedListHandler, + @required this.quotesHandler, + @required this.paragraphHandler, + @required this.headingHandler, + @required this.strawpollHandler, + 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}) { @@ -148,106 +131,20 @@ 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, node); } 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, node); } 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}) { @@ -256,44 +153,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, node); } Widget youTubeToWidget(SlateNode node) { - return YoutubeVideoEmbed(url: node.data.src); + return this.youTubeWidgetHandler(node.data.src, node); } 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(node, inlineHandler, leafHandler); } 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, node); } Widget handleVideo(SlateNode node) { - return VideoElement(url: node.data.src, scaffoldKey: scaffoldkey); + return this.videoWidgetHandler(node); } List handleNodes(List nodes, {bool isChild = false}) { @@ -333,11 +209,14 @@ 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, node)); break; case 'video': widgets.add(handleVideo(node)); break; + case 'strawpoll': + widgets.add(this.strawpollHandler(node)); + break; default: if (node.object == 'text') { widgets.add( @@ -355,12 +234,18 @@ 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/PostContent.dart b/lib/widget/Thread/PostContent.dart new file mode 100644 index 0000000..4ded1da --- /dev/null +++ b/lib/widget/Thread/PostContent.dart @@ -0,0 +1,250 @@ +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, SlateNode node) { + return EmbedWidget( + url: embedUrl, + ); + }, + strawpollHandler: (SlateNode node) { + return EmbedWidget( + url: node.data.src, + ); + }, + userQuoteHandler: (String username, List widgets, bool isChild, SlateNode node) { + 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 f37598e..cd67223 100644 --- a/lib/widget/Thread/ThreadPostItem.dart +++ b/lib/widget/Thread/ThreadPostItem.dart @@ -1,6 +1,6 @@ + import 'package:flutter/material.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/PostHeader.dart'; @@ -9,6 +9,7 @@ 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:knocky/widget/Thread/PostContent.dart'; class ThreadPostItem extends StatelessWidget { final ThreadPost postDetails; @@ -119,7 +120,6 @@ class ThreadPostItem extends StatelessWidget { } List ownPostButtons(BuildContext context) { - return []; return [ FlatButton( child: Text('Edit'), @@ -184,13 +184,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, - ), + child: PostContent( + content: postDetails.content, + onTapSpoiler: (text) { + onPressSpoiler(context, text); + }, + scaffoldKey: this.scaffoldKey), ), Container( padding: diff --git a/pubspec.yaml b/pubspec.yaml index 9241b18..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" @@ -43,10 +43,13 @@ 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 path: hive + quick_actions: ^0.3.2+2 + material_design_icons_flutter: 3.2.3895 flutter: sdk: flutter