TweetDetailPage.dart 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449
  1. import 'package:flutter/material.dart';
  2. import 'package:flutter_osc/model/api.dart';
  3. import 'package:flutter_osc/model/constants.dart';
  4. import 'dart:convert';
  5. import 'package:flutter_osc/util/DataUtils.dart';
  6. import 'package:flutter_osc/util/NetUtils.dart';
  7. import 'package:flutter_osc/widgets/CommonEndLine.dart';
  8. // 动弹详情
  9. class TweetDetailPage extends StatefulWidget {
  10. Map<String, dynamic>? tweetData;
  11. TweetDetailPage({Key? key, this.tweetData}):super(key: key);
  12. @override
  13. State<StatefulWidget> createState() {
  14. return TweetDetailPageState(tweetData: tweetData);
  15. }
  16. }
  17. class TweetDetailPageState extends State<TweetDetailPage> {
  18. Map<String, dynamic>? tweetData;
  19. List? commentList;
  20. RegExp regExp1 = RegExp("</.*>");
  21. RegExp regExp2 = RegExp("<.*>");
  22. TextStyle subtitleStyle = TextStyle(
  23. fontSize: 12.0,
  24. color: const Color(0xFFB5BDC0)
  25. );
  26. TextStyle contentStyle = TextStyle(
  27. fontSize: 15.0,
  28. color: Colors.black
  29. );
  30. num curPage = 1;
  31. ScrollController _controller = ScrollController();
  32. TextEditingController _inputController = TextEditingController();
  33. TweetDetailPageState({Key? key, this.tweetData});
  34. // 获取动弹的回复
  35. getReply(bool isLoadMore) {
  36. DataUtils.isLogin().then((isLogin) {
  37. if (isLogin) {
  38. DataUtils.getAccessToken().then((token) {
  39. if (token == null || token.length == 0) {
  40. return;
  41. }
  42. Map<String, String> params = Map();
  43. var id = this.tweetData!['id'];
  44. params['id'] = '$id';
  45. params['catalog'] = '3';// 3是动弹评论
  46. params['access_token'] = token;
  47. params['page'] = '$curPage';
  48. params['pageSize'] = '20';
  49. params['dataType'] = 'json';
  50. NetUtils.get(Api.COMMENT_LIST, params: params).then((data) {
  51. setState(() {
  52. if (!isLoadMore) {
  53. commentList = json.decode(data)['commentList'];
  54. if (commentList == null) {
  55. commentList = [];
  56. }
  57. } else {
  58. // 加载更多数据
  59. List list = [];
  60. list.addAll(commentList!);
  61. list.addAll(json.decode(data)['commentList']);
  62. if (list.length >= tweetData!['commentCount']) {
  63. list.add(Constants.END_LINE_TAG);
  64. }
  65. commentList = list;
  66. }
  67. });
  68. });
  69. });
  70. }
  71. });
  72. }
  73. @override
  74. void initState() {
  75. super.initState();
  76. getReply(false);
  77. _controller.addListener(() {
  78. var max = _controller.position.maxScrollExtent;
  79. var pixels = _controller.position.pixels;
  80. if (max == pixels && commentList!.length < tweetData!['commentCount']) {
  81. // scroll to end, load next page
  82. curPage++;
  83. getReply(true);
  84. }
  85. });
  86. }
  87. @override
  88. Widget build(BuildContext context) {
  89. var _body = commentList == null ? Center(
  90. child: CircularProgressIndicator(),
  91. ) : ListView.builder(
  92. itemCount: commentList!.length == 0 ? 1 : commentList!.length * 2,
  93. itemBuilder: renderListItem,
  94. controller: _controller,
  95. );
  96. return Scaffold(
  97. appBar: AppBar(
  98. title: Text("动弹详情", style: TextStyle(color: Colors.white)),
  99. iconTheme: IconThemeData(color: Colors.white),
  100. actions: <Widget>[
  101. IconButton(
  102. icon: Icon(Icons.send),
  103. onPressed: () {
  104. // 回复楼主
  105. showReplyBottomView(context, true);
  106. },
  107. )
  108. ],
  109. ),
  110. body: _body
  111. );
  112. }
  113. Widget renderListItem(BuildContext context, int i) {
  114. if (i == 0) {
  115. return getTweetView(this.tweetData!);
  116. }
  117. i -= 1;
  118. if (i.isOdd) {
  119. return Divider(height: 1.0,);
  120. }
  121. i ~/= 2;
  122. return _renderCommentRow(context, i);
  123. }
  124. // 渲染评论列表
  125. _renderCommentRow(context, i) {
  126. var listItem = commentList![i];
  127. if (listItem is String && listItem == Constants.END_LINE_TAG) {
  128. return CommonEndLine();
  129. }
  130. String avatar = listItem['commentPortrait'];
  131. String author = listItem['commentAuthor'];
  132. String date = listItem['pubDate'];
  133. String content = listItem['content'];
  134. content = clearHtmlContent(content);
  135. var row = Row(
  136. children: [
  137. Padding(
  138. padding: const EdgeInsets.all(10.0),
  139. child: Image.network(avatar, width: 35.0, height: 35.0,)
  140. ),
  141. Expanded(
  142. child: Container(
  143. margin: const EdgeInsets.fromLTRB(0.0, 5.0, 0.0, 5.0),
  144. child: Column(
  145. children: [
  146. Row(
  147. children: [
  148. Expanded(
  149. child: Text(author, style: TextStyle(color: const Color(0xFF63CA6C)),),
  150. ),
  151. Padding(
  152. padding: const EdgeInsets.fromLTRB(0.0, 0.0, 10.0, 0.0),
  153. child: Text(date, style: subtitleStyle,)
  154. )
  155. ],
  156. ),
  157. Padding(
  158. padding: const EdgeInsets.fromLTRB(0.0, 0.0, 10.0, 0.0),
  159. child: Row(
  160. children: [
  161. Expanded(
  162. child: Text(content, style: contentStyle,)
  163. )
  164. ],
  165. )
  166. )
  167. ],
  168. ),
  169. )
  170. )
  171. ],
  172. );
  173. return Builder(
  174. builder: (ctx) {
  175. return InkWell(
  176. onTap: () {
  177. showReplyBottomView(ctx, false, data: listItem);
  178. },
  179. child: row,
  180. );
  181. },
  182. );
  183. }
  184. showReplyBottomView(ctx, bool isMainFloor, {data}) {
  185. String title;
  186. String authorId;
  187. if (isMainFloor) {
  188. title = "@${tweetData!['author']}";
  189. authorId = "${tweetData!['authorid']}";
  190. } else {
  191. title = "@${data['commentAuthor']}";
  192. authorId = "${data['commentAuthorId']}";
  193. }
  194. print("authorId = $authorId");
  195. showModalBottomSheet(
  196. context: ctx,
  197. builder: (sheetCtx) {
  198. return Container(
  199. height: 230.0,
  200. padding: const EdgeInsets.all(20.0),
  201. child: Column(
  202. children: [
  203. Row(
  204. children: [
  205. Text(isMainFloor ? "回复楼主" : "回复"),
  206. Expanded(child: Text(title, style: TextStyle(color: const Color(0xFF63CA6C)),)),
  207. InkWell(
  208. child: Container(
  209. padding: const EdgeInsets.fromLTRB(10.0, 6.0, 10.0, 6.0),
  210. decoration: BoxDecoration(
  211. border: Border.all(
  212. color: const Color(0xFF63CA6C),
  213. width: 1.0,
  214. ),
  215. borderRadius: BorderRadius.all(Radius.circular(6.0))
  216. ),
  217. child: Text("发送", style: TextStyle(color: const Color(0xFF63CA6C)),),
  218. ),
  219. onTap: () {
  220. // 发送回复
  221. sendReply(authorId);
  222. },
  223. )
  224. ],
  225. ),
  226. Container(
  227. height: 10.0,
  228. ),
  229. TextField(
  230. maxLines: 5,
  231. controller: _inputController,
  232. decoration: InputDecoration(
  233. hintText: "说点啥~",
  234. hintStyle: TextStyle(
  235. color: const Color(0xFF808080)
  236. ),
  237. border: OutlineInputBorder(
  238. borderRadius: const BorderRadius.all(const Radius.circular(10.0)),
  239. )
  240. ),
  241. )
  242. ],
  243. )
  244. );
  245. }
  246. );
  247. }
  248. void sendReply(authorId) {
  249. String replyStr = _inputController.text;
  250. if (replyStr == null || replyStr.length == 0 || replyStr.trim().length == 0) {
  251. return;
  252. } else {
  253. DataUtils.isLogin().then((isLogin) {
  254. if (isLogin) {
  255. DataUtils.getAccessToken().then((token) {
  256. Map<String, String?> params = Map();
  257. params['access_token'] = token;
  258. params['id'] = "${tweetData!['id']}";
  259. print("id: ${tweetData!['id']}");
  260. params['catalog'] = "3";
  261. params['content'] = replyStr;
  262. params['authorid'] = "$authorId";
  263. print("authorId: $authorId");
  264. params['isPostToMyZone'] = "0";
  265. params['dataType'] = "json";
  266. NetUtils.get(Api.COMMENT_REPLY, params: params).then((data) {
  267. if (data != null) {
  268. var obj = json.decode(data);
  269. var error = obj['error'];
  270. if (error != null && error == '200') {
  271. // 回复成功
  272. Navigator.of(context).pop();
  273. getReply(false);
  274. }
  275. }
  276. });
  277. });
  278. }
  279. });
  280. }
  281. }
  282. Widget getTweetView(Map<String, dynamic> listItem) {
  283. var authorRow = Row(
  284. children: [
  285. Container(
  286. width: 35.0,
  287. height: 35.0,
  288. decoration: BoxDecoration(
  289. shape: BoxShape.circle,
  290. color: Colors.blue,
  291. image: DecorationImage(
  292. image: NetworkImage(listItem['portrait']),
  293. fit: BoxFit.cover
  294. ),
  295. border: Border.all(
  296. color: Colors.white,
  297. width: 2.0,
  298. ),
  299. ),
  300. ),
  301. Padding(
  302. padding: const EdgeInsets.fromLTRB(6.0, 0.0, 0.0, 0.0),
  303. child: Text(listItem['author'], style: TextStyle(
  304. fontSize: 16.0,
  305. ))
  306. ),
  307. Expanded(
  308. child: Row(
  309. mainAxisAlignment: MainAxisAlignment.end,
  310. children: [
  311. Text('${listItem['commentCount']}', style: subtitleStyle,),
  312. Image.asset('./images/ic_comment.png', width: 20.0, height: 20.0,)
  313. ],
  314. ),
  315. )
  316. ],
  317. );
  318. var _body = listItem['body'];
  319. _body = clearHtmlContent(_body);
  320. var contentRow = Row(
  321. children: [
  322. Expanded(child: Text(_body),)
  323. ],
  324. );
  325. var timeRow = Row(
  326. mainAxisAlignment: MainAxisAlignment.start,
  327. children: [
  328. Text(listItem['pubDate'], style: subtitleStyle,)
  329. ],
  330. );
  331. var columns = <Widget>[
  332. Padding(
  333. padding: const EdgeInsets.fromLTRB(10.0, 10.0, 10.0, 2.0),
  334. child: authorRow,
  335. ),
  336. Padding(
  337. padding: const EdgeInsets.fromLTRB(52.0, 0.0, 10.0, 0.0),
  338. child: contentRow,
  339. ),
  340. ];
  341. String? imgSmall = listItem['imgSmall'];
  342. if (imgSmall != null && imgSmall.length > 0) {
  343. // 动弹中有图片
  344. List<String> list = imgSmall.split(",");
  345. List<String> imgUrlList = <String>[];
  346. for (String s in list) {
  347. if (s.startsWith("http")) {
  348. imgUrlList.add(s);
  349. } else {
  350. imgUrlList.add("https://static.oschina.net/uploads/space/" + s);
  351. }
  352. }
  353. List<Widget> imgList = [];
  354. List rows = [];
  355. num len = imgUrlList.length;
  356. for (var row = 0; row < getRow(len as int); row++) {
  357. List<Widget> rowArr = [];
  358. for (var col = 0; col < 3; col++) {
  359. num index = row * 3 + col;
  360. num screenWidth = MediaQuery.of(context).size.width;
  361. double cellWidth = (screenWidth - 100) / 3;
  362. if (index < len) {
  363. rowArr.add(Padding(
  364. padding: const EdgeInsets.all(2.0),
  365. child: Image.network(imgUrlList[index as int], width: cellWidth, height: cellWidth),
  366. ));
  367. }
  368. }
  369. rows.add(rowArr);
  370. }
  371. for (var row in rows) {
  372. imgList.add(Row(
  373. children: row,
  374. ));
  375. }
  376. columns.add(Padding(
  377. padding: const EdgeInsets.fromLTRB(52.0, 5.0, 10.0, 0.0),
  378. child: Column(
  379. children: imgList,
  380. ),
  381. ));
  382. }
  383. columns.add(Padding(
  384. padding: const EdgeInsets.fromLTRB(52.0, 10.0, 10.0, 6.0),
  385. child: timeRow,
  386. ));
  387. columns.add(Divider(height: 5.0,));
  388. columns.add(Container(
  389. margin: const EdgeInsets.fromLTRB(0.0, 6.0, 0.0, 0.0),
  390. child: Row(
  391. children: [
  392. Container(
  393. width: 4.0,
  394. height: 20.0,
  395. color: const Color(0xFF63CA6C),
  396. ),
  397. Expanded(
  398. flex: 1,
  399. child: Container(
  400. height: 20.0,
  401. color: const Color(0xFFECECEC),
  402. child: Text("评论列表", style: TextStyle(color: const Color(0xFF63CA6C)),)
  403. ),
  404. )
  405. ],
  406. ),
  407. ));
  408. return Column(
  409. children: columns,
  410. );
  411. }
  412. int getRow(int n) {
  413. int a = n % 3;
  414. int b = n ~/ 3;
  415. if (a != 0) {
  416. return b + 1;
  417. }
  418. return b;
  419. }
  420. // 去掉文本中的html代码
  421. String clearHtmlContent(String str) {
  422. if (str.startsWith("<emoji")) {
  423. return "[emoji]";
  424. }
  425. var s = str.replaceAll(regExp1, "");
  426. s = s.replaceAll(regExp2, "");
  427. s = s.replaceAll("\n", "");
  428. return s;
  429. }
  430. }