feat: 专栏文章渲染

This commit is contained in:
guozhigq
2023-09-16 21:47:27 +08:00
parent 380ada9ae0
commit 3daa06a198
8 changed files with 649 additions and 169 deletions

View File

@ -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)),
},
);
}

View File

@ -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(

View File

@ -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<Match> matches = digitRegExp.allMatches(id);
String number = matches.first.group(0)!;
return {
'status': true,
'avatar': '',
'uname': uname,
'updateTime': '',
'content': opusContent,
'commentId': int.parse(number)
};
}
}

View File

@ -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;
}

View File

@ -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<Match> 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('番剧');

View File

@ -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),
),
),
);

View File

@ -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<ReplyItemModel> replyList = <ReplyItemModel>[].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<ReplyItemModel> 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');
}
}

View File

@ -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<HtmlRenderPage> createState() => _HtmlRenderPageState();
}
class _HtmlRenderPageState extends State<HtmlRenderPage> {
HtmlRenderController htmlRenderCtr = Get.put(HtmlRenderController());
class _HtmlRenderPageState extends State<HtmlRenderPage>
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<HtmlRenderPage> {
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) => <PopupMenuEntry>[
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<Offset>(
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),
),
),
),
],
),
);
}