From 3daa06a198633107d9503c11a4c65f78ebd791db Mon Sep 17 00:00:00 2001 From: guozhigq Date: Sat, 16 Sep 2023 21:47:27 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=B8=93=E6=A0=8F=E6=96=87=E7=AB=A0?= =?UTF-8?q?=E6=B8=B2=E6=9F=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/common/widgets/html_render.dart | 120 ++--- lib/common/widgets/video_card_v.dart | 9 + lib/http/html.dart | 49 +- lib/http/init.dart | 29 +- lib/pages/dynamics/controller.dart | 28 +- .../dynamics/widgets/rich_node_panel.dart | 6 +- lib/pages/html/controller.dart | 97 +++- lib/pages/html/view.dart | 480 +++++++++++++++--- 8 files changed, 649 insertions(+), 169 deletions(-) diff --git a/lib/common/widgets/html_render.dart b/lib/common/widgets/html_render.dart index 6733d7cb..2e97ceed 100644 --- a/lib/common/widgets/html_render.dart +++ b/lib/common/widgets/html_render.dart @@ -1,7 +1,7 @@ -import 'package:cached_network_image/cached_network_image.dart'; import 'package:get/get.dart'; import 'package:flutter/material.dart'; import 'package:flutter_html/flutter_html.dart'; +import 'package:pilipala/common/widgets/network_img_layer.dart'; // ignore: must_be_immutable class HtmlRender extends StatelessWidget { @@ -20,35 +20,47 @@ class HtmlRender extends StatelessWidget { Widget build(BuildContext context) { return Html( data: htmlContent, - // tagsList: Html.tags..addAll(["form", "label", "input"]), onLinkTap: (url, buildContext, attributes) => {}, extensions: [ TagExtension( tagsToExtend: {"img"}, builder: (extensionContext) { - String? imgUrl = extensionContext.attributes['src']; - if (imgUrl!.startsWith('//')) { - imgUrl = 'https:$imgUrl'; + try { + Map attributes = extensionContext.attributes; + List key = attributes.keys.toList(); + String? imgUrl = key.contains('src') + ? attributes['src'] + : attributes['data-src']; + if (imgUrl!.startsWith('//')) { + imgUrl = 'https:$imgUrl'; + } + if (imgUrl.startsWith('http://')) { + imgUrl = imgUrl.replaceAll('http://', 'https://'); + } + imgUrl = imgUrl.contains('@') ? imgUrl.split('@').first : imgUrl; + bool isEmote = imgUrl.contains('/emote/'); + bool isMall = imgUrl.contains('/mall/'); + if (isMall) { + return const SizedBox(); + } + // bool inTable = + // extensionContext.element!.previousElementSibling == null || + // extensionContext.element!.nextElementSibling == null; + // imgUrl = Utils().imageUrl(imgUrl!); + // return Image.network( + // imgUrl, + // width: isEmote ? 22 : null, + // height: isEmote ? 22 : null, + // ); + return NetworkImgLayer( + width: isEmote ? 22 : Get.size.width - 24, + height: isEmote ? 22 : 200, + src: imgUrl, + ); + } catch (err) { + print(err); + return const SizedBox(); } - if (imgUrl.startsWith('http://')) { - imgUrl = imgUrl.replaceAll('http://', 'https://'); - } - - print(imgUrl); - bool isEmote = imgUrl.contains('/emote/'); - bool isMall = imgUrl.contains('/mall/'); - if (isMall) { - return SizedBox(); - } - // bool inTable = - // extensionContext.element!.previousElementSibling == null || - // extensionContext.element!.nextElementSibling == null; - // imgUrl = Utils().imageUrl(imgUrl!); - return Image.network( - imgUrl, - width: isEmote ? 22 : null, - height: isEmote ? 22 : null, - ); }, ), ], @@ -63,11 +75,13 @@ class HtmlRender extends StatelessWidget { textDecoration: TextDecoration.none, ), "p": Style( - margin: Margins.only(bottom: 0), + margin: Margins.only(bottom: 10), ), "span": Style( fontSize: FontSize.medium, + height: Height(1.65), ), + "div": Style(height: Height.auto()), "li > p": Style( display: Display.inline, ), @@ -75,61 +89,7 @@ class HtmlRender extends StatelessWidget { padding: HtmlPaddings.only(bottom: 4), textAlign: TextAlign.justify, ), - "image": Style(margin: Margins.only(top: 4, bottom: 4)), - "p > img": Style(margin: Margins.only(top: 4, bottom: 4)), - "code": Style( - backgroundColor: Theme.of(context).colorScheme.onInverseSurface), - "code > span": Style(textAlign: TextAlign.start), - "hr": Style( - margin: Margins.zero, - padding: HtmlPaddings.zero, - border: Border( - top: BorderSide( - width: 1.0, - color: - Theme.of(context).colorScheme.onBackground.withOpacity(0.3), - ), - ), - ), - 'table': Style( - border: Border( - right: BorderSide( - width: 0.5, - color: - Theme.of(context).colorScheme.onBackground.withOpacity(0.3), - ), - bottom: BorderSide( - width: 0.5, - color: - Theme.of(context).colorScheme.onBackground.withOpacity(0.3), - ), - ), - ), - 'tr': Style( - border: Border( - top: BorderSide( - width: 1.0, - color: - Theme.of(context).colorScheme.onBackground.withOpacity(0.3), - ), - left: BorderSide( - width: 1.0, - color: - Theme.of(context).colorScheme.onBackground.withOpacity(0.3), - ), - ), - ), - 'thead': Style( - backgroundColor: Theme.of(context).colorScheme.background, - ), - 'th': Style( - padding: HtmlPaddings.only(left: 3, right: 3), - ), - 'td': Style( - padding: HtmlPaddings.all(4.0), - alignment: Alignment.center, - textAlign: TextAlign.center, - ), + "img": Style(margin: Margins.only(top: 4, bottom: 4)), }, ); } diff --git a/lib/common/widgets/video_card_v.dart b/lib/common/widgets/video_card_v.dart index cc23f110..9e34cbcb 100644 --- a/lib/common/widgets/video_card_v.dart +++ b/lib/common/widgets/video_card_v.dart @@ -62,6 +62,15 @@ class VideoCardV extends StatelessWidget { 'heroTag': heroTag, }); break; + // 动态 + case 'picture': + Get.toNamed('/htmlRender', parameters: { + 'url': videoItem.uri, + 'title': videoItem.title, + 'id': videoItem.param.toString(), + 'dynamicType': 'picture' + }); + break; default: SmartDialog.showToast(videoItem.goto); Get.toNamed( diff --git a/lib/http/html.dart b/lib/http/html.dart index ec92bcfa..4a922e8f 100644 --- a/lib/http/html.dart +++ b/lib/http/html.dart @@ -3,8 +3,12 @@ import 'package:html/parser.dart'; import 'package:pilipala/http/index.dart'; class HtmlHttp { - static Future reqHtml(id) async { - var response = await Request().get("https://www.bilibili.com/opus/$id"); + // article + static Future reqHtml(id, dynamicType) async { + var response = await Request().get( + "https://www.bilibili.com/opus/$id", + extra: {'ua': 'pc'}, + ); Document rootTree = parse(response.data); Element body = rootTree.body!; Element appDom = body.querySelector('#app')!; @@ -34,7 +38,46 @@ class HtmlHttp { 'uname': uname, 'updateTime': updateTime, 'content': opusContent, - 'commentId': commentId + 'commentId': int.parse(commentId) + }; + } + + // read + static Future reqReadHtml(id, dynamicType) async { + var response = await Request().get( + "https://www.bilibili.com/$dynamicType/$id/", + extra: {'ua': 'pc'}, + ); + Document rootTree = parse(response.data); + Element body = rootTree.body!; + Element appDom = body.querySelector('#app')!; + Element authorHeader = appDom.querySelector('.up-left')!; + // 头像 + // String avatar = + // authorHeader.querySelector('.bili-avatar-img')!.attributes['data-src']!; + // print(avatar); + // avatar = 'https:${avatar.split('@')[0]}'; + String uname = authorHeader.querySelector('.up-name')!.text.trim(); + // 动态详情 + Element opusDetail = appDom.querySelector('.article-content')!; + // 发布时间 + // String updateTime = + // opusDetail.querySelector('.opus-module-author__pub__text')!.text; + // print(updateTime); + + // + String opusContent = + opusDetail.querySelector('#read-article-holder')!.innerHtml; + RegExp digitRegExp = RegExp(r'\d+'); + Iterable matches = digitRegExp.allMatches(id); + String number = matches.first.group(0)!; + return { + 'status': true, + 'avatar': '', + 'uname': uname, + 'updateTime': '', + 'content': opusContent, + 'commentId': int.parse(number) }; } } diff --git a/lib/http/init.dart b/lib/http/init.dart index f927e2e5..4af20cae 100644 --- a/lib/http/init.dart +++ b/lib/http/init.dart @@ -130,11 +130,19 @@ class Request { Response response; Options options; ResponseType resType = ResponseType.json; - if (extra != null) { - resType = extra!['resType'] ?? ResponseType.json; - } + options = Options(); options.responseType = resType; + + if (extra != null) { + options.responseType = extra!['resType'] ?? ResponseType.json; + if (extra['ua'] != null) { + print(options.headers); + options.headers = {'user-agent': headerUa(type: extra['ua'])}; + // options.headers!['user-agent'] = headerUa(type: extra['ua']); + } + } + try { response = await dio.get( url, @@ -201,14 +209,19 @@ class Request { token.cancel("cancelled"); } - String headerUa() { + String headerUa({type = 'mob'}) { String headerUa = ''; - if (Platform.isIOS) { - headerUa = - 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1 Mobile/15E148 Safari/604.1'; + if (type == 'mob') { + if (Platform.isIOS) { + headerUa = + 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1 Mobile/15E148 Safari/604.1'; + } else { + headerUa = + 'Mozilla/5.0 (Linux; Android 10; SM-G975F) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.101 Mobile Safari/537.36'; + } } else { headerUa = - 'Mozilla/5.0 (Linux; Android 10; SM-G975F) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.101 Mobile Safari/537.36'; + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.2 Safari/605.1.15'; } return headerUa; } diff --git a/lib/pages/dynamics/controller.dart b/lib/pages/dynamics/controller.dart index 5b524510..26ba2b22 100644 --- a/lib/pages/dynamics/controller.dart +++ b/lib/pages/dynamics/controller.dart @@ -149,10 +149,30 @@ class DynamicsController extends GetxController { case 'DYNAMIC_TYPE_ARTICLE': String title = item.modules.moduleDynamic.major.opus.title; String url = item.modules.moduleDynamic.major.opus.jumpUrl; - Get.toNamed( - '/webview', - parameters: {'url': 'https:$url', 'type': 'note', 'pageTitle': title}, - ); + if (url.contains('opus') || url.contains('read')) { + RegExp digitRegExp = RegExp(r'\d+'); + Iterable matches = digitRegExp.allMatches(url); + String number = matches.first.group(0)!; + if (url.contains('read')) { + number = 'cv$number'; + } + Get.toNamed('/htmlRender', parameters: { + 'url': url.startsWith('//') ? url.split('//').last : url, + 'title': title, + 'id': number, + 'dynamicType': url.split('//').last.split('/')[1] + }); + } else { + Get.toNamed( + '/webview', + parameters: { + 'url': 'https:$url', + 'type': 'note', + 'pageTitle': title + }, + ); + } + break; case 'DYNAMIC_TYPE_PGC': print('番剧'); diff --git a/lib/pages/dynamics/widgets/rich_node_panel.dart b/lib/pages/dynamics/widgets/rich_node_panel.dart index 61422ceb..27820b27 100644 --- a/lib/pages/dynamics/widgets/rich_node_panel.dart +++ b/lib/pages/dynamics/widgets/rich_node_panel.dart @@ -225,8 +225,10 @@ InlineSpan richNode(item, context) { width: box.maxWidth / 2, height: box.maxWidth * 0.5 * - pictureItem.height! / - pictureItem.width!, + (pictureItem.height != null && + pictureItem.width != null + ? pictureItem.height! / pictureItem.width! + : 1), ), ), ); diff --git a/lib/pages/html/controller.dart b/lib/pages/html/controller.dart index 2ec55621..f3187828 100644 --- a/lib/pages/html/controller.dart +++ b/lib/pages/html/controller.dart @@ -1,19 +1,112 @@ +import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:hive/hive.dart'; import 'package:pilipala/http/html.dart'; +import 'package:pilipala/http/reply.dart'; +import 'package:pilipala/models/common/reply_sort_type.dart'; +import 'package:pilipala/models/video/reply/item.dart'; +import 'package:pilipala/utils/feed_back.dart'; +import 'package:pilipala/utils/storage.dart'; class HtmlRenderController extends GetxController { late String id; + late String dynamicType; + late int type; + RxInt oid = (-1).obs; late Map response; + int? floor; + int currentPage = 0; + bool isLoadingMore = false; + RxString noMore = ''.obs; + RxList replyList = [].obs; + RxInt acount = 0.obs; + final ScrollController scrollController = ScrollController(); + + ReplySortType _sortType = ReplySortType.time; + RxString sortTypeTitle = ReplySortType.time.titles.obs; + RxString sortTypeLabel = ReplySortType.time.labels.obs; + Box setting = GStrorage.setting; @override void onInit() { super.onInit(); id = Get.parameters['id']!; + dynamicType = Get.parameters['dynamicType']!; + type = dynamicType == 'picture' ? 11 : 12; } - Future reqHtml() async { - var res = await HtmlHttp.reqHtml(id); + // 请求动态内容 + Future reqHtml(id) async { + late dynamic res; + if (dynamicType == 'opus' || dynamicType == 'picture') { + res = await HtmlHttp.reqHtml(id, dynamicType); + } else { + res = await HtmlHttp.reqReadHtml(id, dynamicType); + } response = res; + oid.value = res['commentId']; return res; } + + // 请求评论 + Future queryReplyList({reqType = 'init'}) async { + var res = await ReplyHttp.replyList( + oid: oid.value, + pageNum: currentPage + 1, + type: type, + sort: _sortType.index, + ); + if (res['status']) { + List replies = res['data'].replies; + acount.value = res['data'].page.acount; + if (replies.isNotEmpty) { + currentPage++; + noMore.value = '加载中...'; + if (replies.length < 20) { + noMore.value = '没有更多了'; + } + } else { + noMore.value = currentPage == 0 ? '还没有评论' : '没有更多了'; + } + if (reqType == 'init') { + // 添加置顶回复 + if (res['data'].upper.top != null) { + bool flag = res['data'] + .topReplies + .any((reply) => reply.rpid == res['data'].upper.top.rpid); + if (!flag) { + replies.insert(0, res['data'].upper.top); + } + } + replies.insertAll(0, res['data'].topReplies); + replyList.value = replies; + } else { + replyList.addAll(replies); + } + } + isLoadingMore = false; + return res; + } + + // 排序搜索评论 + queryBySort() { + feedBack(); + switch (_sortType) { + case ReplySortType.time: + _sortType = ReplySortType.like; + break; + case ReplySortType.like: + _sortType = ReplySortType.reply; + break; + case ReplySortType.reply: + _sortType = ReplySortType.time; + break; + default: + } + sortTypeTitle.value = _sortType.titles; + sortTypeLabel.value = _sortType.labels; + currentPage = 0; + replyList.clear(); + queryReplyList(reqType: 'init'); + } } diff --git a/lib/pages/html/view.dart b/lib/pages/html/view.dart index f8572b14..d5e4556d 100644 --- a/lib/pages/html/view.dart +++ b/lib/pages/html/view.dart @@ -1,7 +1,19 @@ +import 'package:easy_debounce/easy_throttle.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; +import 'package:pilipala/common/skeleton/video_reply.dart'; import 'package:pilipala/common/widgets/html_render.dart'; +import 'package:pilipala/common/widgets/http_error.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart'; +import 'package:pilipala/models/common/reply_type.dart'; +import 'package:pilipala/pages/mine/index.dart'; +import 'package:pilipala/pages/video/detail/reply/widgets/reply_item.dart'; +import 'package:pilipala/pages/video/detail/replyNew/index.dart'; +import 'package:pilipala/pages/video/detail/replyReply/index.dart'; +import 'package:pilipala/utils/feed_back.dart'; import 'controller.dart'; @@ -12,16 +24,104 @@ class HtmlRenderPage extends StatefulWidget { State createState() => _HtmlRenderPageState(); } -class _HtmlRenderPageState extends State { - HtmlRenderController htmlRenderCtr = Get.put(HtmlRenderController()); +class _HtmlRenderPageState extends State + with TickerProviderStateMixin { + final HtmlRenderController _htmlRenderCtr = Get.put(HtmlRenderController()); late String title; late String id; + late String url; + late String dynamicType; + late int type; + bool _isFabVisible = true; + late Future _futureBuilderFuture; + late ScrollController scrollController; + late AnimationController fabAnimationCtr; @override void initState() { super.initState(); title = Get.parameters['title']!; id = Get.parameters['id']!; + url = Get.parameters['url']!; + dynamicType = Get.parameters['dynamicType']!; + type = dynamicType == 'picture' ? 11 : 12; + _futureBuilderFuture = _htmlRenderCtr.reqHtml(id); + fabAnimationCtr = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 300), + ); + scrollListener(); + } + + void scrollListener() { + scrollController = _htmlRenderCtr.scrollController; + scrollController.addListener( + () { + // 分页加载 + if (scrollController.position.pixels >= + scrollController.position.maxScrollExtent - 300) { + EasyThrottle.throttle('replylist', const Duration(seconds: 2), () { + _htmlRenderCtr.queryReplyList(reqType: 'onLoad'); + }); + } + + // 标题 + // if (scrollController.offset > 55 && !_visibleTitle) { + // _visibleTitle = true; + // titleStreamC.add(true); + // } else if (scrollController.offset <= 55 && _visibleTitle) { + // _visibleTitle = false; + // titleStreamC.add(false); + // } + + // fab按钮 + final ScrollDirection direction = + scrollController.position.userScrollDirection; + if (direction == ScrollDirection.forward) { + _showFab(); + } else if (direction == ScrollDirection.reverse) { + _hideFab(); + } + }, + ); + } + + void _showFab() { + if (!_isFabVisible) { + _isFabVisible = true; + fabAnimationCtr.forward(); + } + } + + void _hideFab() { + if (_isFabVisible) { + _isFabVisible = false; + fabAnimationCtr.reverse(); + } + } + + void replyReply(replyItem) { + int oid = replyItem.oid; + int rpid = replyItem.rpid!; + Get.to( + () => Scaffold( + appBar: AppBar( + titleSpacing: 0, + centerTitle: false, + title: Text( + '评论详情', + style: Theme.of(context).textTheme.titleMedium, + ), + ), + body: VideoReplyReplyPanel( + oid: oid, + rpid: rpid, + source: 'dynamic', + replyType: ReplyType.values[type], + firstFloor: replyItem, + ), + ), + ); } @override @@ -29,88 +129,328 @@ class _HtmlRenderPageState extends State { return Scaffold( appBar: AppBar( centerTitle: false, - title: Text(title), + titleSpacing: 0, + title: Text( + title, + style: Theme.of(context).textTheme.titleMedium, + ), + actions: [ + const SizedBox(width: 4), + IconButton( + onPressed: () { + Get.toNamed('/webview', parameters: { + 'url': 'https:$url', + 'type': 'url', + 'pageTitle': title, + }); + }, + icon: const Icon(Icons.open_in_browser_outlined, size: 19), + ), + PopupMenuButton( + icon: const Icon(Icons.more_vert), + itemBuilder: (BuildContext context) => [ + PopupMenuItem( + onTap: () => { + Clipboard.setData(ClipboardData(text: url)), + SmartDialog.showToast('已复制'), + }, + child: const Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.copy_rounded, size: 19), + SizedBox(width: 10), + Text('复制链接'), + ], + ), + ), + PopupMenuItem( + onTap: () => {}, + child: const Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.share_outlined, size: 19), + SizedBox(width: 10), + Text('分享'), + ], + ), + ), + ], + ), + const SizedBox(width: 6) + ], ), - body: SingleChildScrollView( - child: Column( - children: [ - FutureBuilder( - future: htmlRenderCtr.reqHtml(), - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.done) { - var data = snapshot.data; - if (data['status']) { - return Column( - children: [ - Padding( - padding: const EdgeInsets.fromLTRB(12, 12, 12, 8), - child: Row( - children: [ - NetworkImgLayer( - width: 40, - height: 40, - type: 'avatar', - src: htmlRenderCtr.response['avatar']!, - ), - const SizedBox(width: 10), - Column( - crossAxisAlignment: CrossAxisAlignment.start, + body: Stack( + children: [ + SingleChildScrollView( + controller: scrollController, + child: Column( + children: [ + FutureBuilder( + future: _futureBuilderFuture, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + var data = snapshot.data; + fabAnimationCtr.forward(); + if (data['status']) { + return Column( + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(12, 12, 12, 8), + child: Row( children: [ - Text(htmlRenderCtr.response['uname'], - style: TextStyle( - fontSize: Theme.of(context) - .textTheme - .titleSmall! - .fontSize, - )), - Text( - htmlRenderCtr.response['updateTime'], - style: TextStyle( - color: - Theme.of(context).colorScheme.outline, - fontSize: Theme.of(context) - .textTheme - .labelSmall! - .fontSize, - ), + NetworkImgLayer( + width: 40, + height: 40, + type: 'avatar', + src: _htmlRenderCtr.response['avatar']!, ), + const SizedBox(width: 10), + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text(_htmlRenderCtr.response['uname'], + style: TextStyle( + fontSize: Theme.of(context) + .textTheme + .titleSmall! + .fontSize, + )), + Text( + _htmlRenderCtr.response['updateTime'], + style: TextStyle( + color: Theme.of(context) + .colorScheme + .outline, + fontSize: Theme.of(context) + .textTheme + .labelSmall! + .fontSize, + ), + ), + ], + ), + const Spacer(), ], ), - const Spacer(), - ], - ), - ), - Padding( - padding: const EdgeInsets.fromLTRB(12, 12, 12, 8), - child: HtmlRender( - htmlContent: htmlRenderCtr.response['content'], - ), - ), - Container( + ), + Padding( + padding: const EdgeInsets.fromLTRB(12, 8, 12, 8), + child: HtmlRender( + htmlContent: _htmlRenderCtr.response['content'], + ), + ), + Container( + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + width: 8, + color: Theme.of(context) + .dividerColor + .withOpacity(0.05), + ), + ), + ), + ), + ], + ); + } else { + return const Text('error'); + } + } else { + // 骨架屏 + return const SizedBox(); + } + }, + ), + Obx( + () => _htmlRenderCtr.oid.value != -1 + ? Container( decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, border: Border( - bottom: BorderSide( - width: 8, + top: BorderSide( + width: 0.6, color: Theme.of(context) .dividerColor .withOpacity(0.05), ), ), ), - ), - ], - ); - } else { - return Text('error'); - } - } else { - // 骨架屏 - return const SizedBox(); - } - }, + height: 45, + padding: const EdgeInsets.only(left: 12, right: 6), + child: Row( + children: [ + const Text('回复'), + const Spacer(), + SizedBox( + height: 35, + child: TextButton.icon( + onPressed: () => _htmlRenderCtr.queryBySort(), + icon: const Icon(Icons.sort, size: 16), + label: Obx( + () => Text( + _htmlRenderCtr.sortTypeLabel.value, + style: const TextStyle(fontSize: 13), + ), + ), + ), + ) + ], + ), + ) + : const SizedBox(), + ), + Obx( + () => _htmlRenderCtr.oid.value != -1 + ? FutureBuilder( + future: _htmlRenderCtr.queryReplyList(), + builder: (context, snapshot) { + if (snapshot.connectionState == + ConnectionState.done) { + Map data = snapshot.data as Map; + if (snapshot.data['status']) { + // 请求成功 + return Obx( + () => _htmlRenderCtr.replyList.isEmpty && + _htmlRenderCtr.isLoadingMore + ? ListView.builder( + itemCount: 5, + shrinkWrap: true, + physics: + const NeverScrollableScrollPhysics(), + itemBuilder: (context, index) { + return const VideoReplySkeleton(); + }, + ) + : ListView.builder( + shrinkWrap: true, + physics: + const NeverScrollableScrollPhysics(), + itemCount: + _htmlRenderCtr.replyList.length + + 1, + itemBuilder: (context, index) { + if (index == + _htmlRenderCtr + .replyList.length) { + return Container( + padding: EdgeInsets.only( + bottom: + MediaQuery.of(context) + .padding + .bottom), + height: MediaQuery.of(context) + .padding + .bottom + + 100, + child: Center( + child: Obx( + () => Text( + _htmlRenderCtr + .noMore.value, + style: TextStyle( + fontSize: 12, + color: Theme.of(context) + .colorScheme + .outline, + ), + ), + ), + ), + ); + } else { + return ReplyItem( + replyItem: _htmlRenderCtr + .replyList[index], + showReplyRow: true, + replyLevel: '1', + replyReply: (replyItem) => + replyReply(replyItem), + replyType: + ReplyType.values[type], + addReply: (replyItem) { + _htmlRenderCtr + .replyList[index].replies! + .add(replyItem); + }, + ); + } + }, + ), + ); + } else { + // 请求错误 + return CustomScrollView( + slivers: [ + HttpError( + errMsg: data['msg'], + fn: () => setState(() {}), + ) + ], + ); + } + } else { + // 骨架屏 + return ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: 5, + itemBuilder: (context, index) { + return const VideoReplySkeleton(); + }, + ); + } + }, + ) + : const SizedBox(), + ) + ], ), - ], - ), + ), + Positioned( + bottom: MediaQuery.of(context).padding.bottom + 14, + right: 14, + child: SlideTransition( + position: Tween( + begin: const Offset(0, 2), + end: const Offset(0, 0), + ).animate(CurvedAnimation( + parent: fabAnimationCtr, + curve: Curves.easeInOut, + )), + child: FloatingActionButton( + heroTag: null, + onPressed: () { + feedBack(); + showModalBottomSheet( + context: context, + isScrollControlled: true, + builder: (BuildContext context) { + return VideoReplyNewDialog( + oid: _htmlRenderCtr.oid.value, + root: 0, + parent: 0, + replyType: ReplyType.values[type], + ); + }, + ).then( + (value) => { + // 完成评论,数据添加 + if (value != null && value['data'] != null) + { + _htmlRenderCtr.replyList.add(value['data']), + _htmlRenderCtr.acount.value++ + } + }, + ); + }, + tooltip: '评论动态', + child: const Icon(Icons.reply), + ), + ), + ), + ], ), ); }