TweetsListPage.dart 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510
  1. import 'package:flutter/material.dart';
  2. import 'package:flutter_osc/constants/Constants.dart';
  3. import 'package:flutter_osc/events/LoginEvent.dart';
  4. import 'package:flutter_osc/events/LogoutEvent.dart';
  5. import 'package:flutter_osc/util/Utf8Utils.dart';
  6. import '../util/BlackListUtils.dart';
  7. import '../api/Api.dart';
  8. import '../util/NetUtils.dart';
  9. import '../pages/TweetDetailPage.dart';
  10. import 'LoginPage.dart';
  11. import 'dart:convert';
  12. import 'dart:async';
  13. import '../util/DataUtils.dart';
  14. class TweetsListPage extends StatefulWidget {
  15. @override
  16. State<StatefulWidget> createState() {
  17. return TweetsListPageState();
  18. }
  19. }
  20. class TweetsListPageState extends State<TweetsListPage> {
  21. List hotTweetsList;
  22. List normalTweetsList;
  23. TextStyle authorTextStyle;
  24. TextStyle subtitleStyle;
  25. RegExp regExp1 = RegExp("</.*>");
  26. RegExp regExp2 = RegExp("<.*>");
  27. num curPage = 1;
  28. bool loading = false;
  29. ScrollController _controller;
  30. bool isUserLogin = false;
  31. @override
  32. void initState() {
  33. super.initState();
  34. DataUtils.isLogin().then((isLogin) {
  35. setState(() {
  36. this.isUserLogin = isLogin;
  37. });
  38. });
  39. Constants.eventBus.on(LoginEvent).listen((event) {
  40. setState(() {
  41. this.isUserLogin = true;
  42. });
  43. });
  44. Constants.eventBus.on(LogoutEvent).listen((event) {
  45. setState(() {
  46. this.isUserLogin = false;
  47. });
  48. });
  49. }
  50. TweetsListPageState() {
  51. authorTextStyle =
  52. TextStyle(fontSize: 15.0, fontWeight: FontWeight.bold);
  53. subtitleStyle =
  54. TextStyle(fontSize: 12.0, color: const Color(0xFFB5BDC0));
  55. _controller = ScrollController();
  56. _controller.addListener(() {
  57. var maxScroll = _controller.position.maxScrollExtent;
  58. var pixels = _controller.position.pixels;
  59. if (maxScroll == pixels) {
  60. // load next page
  61. curPage++;
  62. getTweetsList(true, false);
  63. }
  64. });
  65. }
  66. getTweetsList(bool isLoadMore, bool isHot) {
  67. DataUtils.isLogin().then((isLogin) {
  68. if (isLogin) {
  69. DataUtils.getAccessToken().then((token) {
  70. if (token == null || token.length == 0) {
  71. return;
  72. }
  73. loading = true;
  74. Map<String, String> params = Map();
  75. params['access_token'] = token;
  76. params['page'] = "$curPage";
  77. if (isHot) {
  78. params['user'] = "-1";
  79. } else {
  80. params['user'] = "0";
  81. }
  82. params['pageSize'] = "20";
  83. params['dataType'] = "json";
  84. NetUtils.get(Api.TWEETS_LIST, params: params).then((data) {
  85. Map<String, dynamic> obj = json.decode(data);
  86. if (!isLoadMore) {
  87. // first load
  88. if (isHot) {
  89. hotTweetsList = obj['tweetlist'];
  90. } else {
  91. normalTweetsList = obj['tweetlist'];
  92. }
  93. } else {
  94. // load more
  95. List list = List();
  96. list.addAll(normalTweetsList);
  97. list.addAll(obj['tweetlist']);
  98. normalTweetsList = list;
  99. }
  100. filterList(hotTweetsList, true);
  101. filterList(normalTweetsList, false);
  102. });
  103. });
  104. }
  105. });
  106. }
  107. // 根据黑名单过滤出新的数组
  108. filterList(List<dynamic> objList, bool isHot) {
  109. BlackListUtils.getBlackListIds().then((intList) {
  110. if (intList != null && intList.isNotEmpty && objList != null) {
  111. List newList = List();
  112. for (dynamic item in objList) {
  113. int authorId = item['authorid'];
  114. if (!intList.contains(authorId)) {
  115. newList.add(item);
  116. }
  117. }
  118. setState(() {
  119. if (isHot) {
  120. hotTweetsList = newList;
  121. } else {
  122. normalTweetsList = newList;
  123. }
  124. loading = false;
  125. });
  126. } else {
  127. // 黑名单为空,直接返回原始数据
  128. setState(() {
  129. if (isHot) {
  130. hotTweetsList = objList;
  131. } else {
  132. normalTweetsList = objList;
  133. }
  134. loading = false;
  135. });
  136. }
  137. });
  138. }
  139. // 去掉文本中的html代码
  140. String clearHtmlContent(String str) {
  141. if (str.startsWith("<emoji")) {
  142. return "[emoji]";
  143. }
  144. var s = str.replaceAll(regExp1, "");
  145. s = s.replaceAll(regExp2, "");
  146. s = s.replaceAll("\n", "");
  147. return s;
  148. }
  149. Widget getRowWidget(Map<String, dynamic> listItem) {
  150. var authorRow = Row(
  151. children: [
  152. Container(
  153. width: 35.0,
  154. height: 35.0,
  155. decoration: BoxDecoration(
  156. shape: BoxShape.circle,
  157. color: Colors.transparent,
  158. image: DecorationImage(
  159. image: NetworkImage(listItem['portrait']),
  160. fit: BoxFit.cover),
  161. border: Border.all(
  162. color: Colors.white,
  163. width: 2.0,
  164. ),
  165. ),
  166. ),
  167. Padding(
  168. padding: const EdgeInsets.fromLTRB(6.0, 0.0, 0.0, 0.0),
  169. child: Text(listItem['author'],
  170. style: TextStyle(
  171. fontSize: 16.0,
  172. ))),
  173. Expanded(
  174. child: Row(
  175. mainAxisAlignment: MainAxisAlignment.end,
  176. children: [
  177. Text(
  178. '${listItem['commentCount']}',
  179. style: subtitleStyle,
  180. ),
  181. Image.asset(
  182. './images/ic_comment.png',
  183. width: 16.0,
  184. height: 16.0,
  185. )
  186. ],
  187. ),
  188. )
  189. ],
  190. );
  191. var _body = listItem['body'];
  192. _body = clearHtmlContent(_body);
  193. var contentRow = Row(
  194. children: [
  195. Expanded(
  196. child: Text(_body),
  197. )
  198. ],
  199. );
  200. var timeRow = Row(
  201. mainAxisAlignment: MainAxisAlignment.end,
  202. children: [
  203. Text(
  204. listItem['pubDate'],
  205. style: subtitleStyle,
  206. )
  207. ],
  208. );
  209. var columns = <Widget>[
  210. Padding(
  211. padding: const EdgeInsets.fromLTRB(10.0, 10.0, 10.0, 2.0),
  212. child: authorRow,
  213. ),
  214. Padding(
  215. padding: const EdgeInsets.fromLTRB(52.0, 0.0, 10.0, 0.0),
  216. child: contentRow,
  217. ),
  218. ];
  219. String imgSmall = listItem['imgSmall'];
  220. if (imgSmall != null && imgSmall.length > 0) {
  221. // 动弹中有图片
  222. List<String> list = imgSmall.split(",");
  223. List<String> imgUrlList = List<String>();
  224. for (String s in list) {
  225. if (s.startsWith("http")) {
  226. imgUrlList.add(s);
  227. } else {
  228. imgUrlList.add("https://static.oschina.net/uploads/space/" + s);
  229. }
  230. }
  231. List<Widget> imgList = [];
  232. List<List<Widget>> rows = [];
  233. num len = imgUrlList.length;
  234. for (var row = 0; row < getRow(len); row++) {
  235. List<Widget> rowArr = [];
  236. for (var col = 0; col < 3; col++) {
  237. num index = row * 3 + col;
  238. num screenWidth = MediaQuery.of(context).size.width;
  239. double cellWidth = (screenWidth - 100) / 3;
  240. if (index < len) {
  241. rowArr.add(Padding(
  242. padding: const EdgeInsets.all(2.0),
  243. child: Image.network(imgUrlList[index],
  244. width: cellWidth, height: cellWidth),
  245. ));
  246. }
  247. }
  248. rows.add(rowArr);
  249. }
  250. for (var row in rows) {
  251. imgList.add(Row(
  252. children: row,
  253. ));
  254. }
  255. columns.add(Padding(
  256. padding: const EdgeInsets.fromLTRB(52.0, 5.0, 10.0, 0.0),
  257. child: Column(
  258. children: imgList,
  259. ),
  260. ));
  261. }
  262. columns.add(Padding(
  263. padding: const EdgeInsets.fromLTRB(0.0, 10.0, 10.0, 6.0),
  264. child: timeRow,
  265. ));
  266. return InkWell(
  267. child: Column(
  268. children: columns,
  269. ),
  270. onTap: () {
  271. // 跳转到动弹详情
  272. Navigator.of(context).push(MaterialPageRoute(builder: (ctx) {
  273. return TweetDetailPage(
  274. tweetData: listItem,
  275. );
  276. }));
  277. },
  278. onLongPress: () {
  279. showDialog(
  280. context: context,
  281. builder: (BuildContext ctx) {
  282. return AlertDialog(
  283. title: Text('提示'),
  284. content: Text('要把\"${listItem['author']}\"关进小黑屋吗?'),
  285. actions: <Widget>[
  286. FlatButton(
  287. child: Text(
  288. '取消',
  289. style: TextStyle(color: Colors.red),
  290. ),
  291. onPressed: () {
  292. Navigator.of(context).pop();
  293. },
  294. ),
  295. FlatButton(
  296. child: Text(
  297. '确定',
  298. style: TextStyle(color: Colors.blue),
  299. ),
  300. onPressed: () {
  301. putIntoBlackHouse(listItem);
  302. },
  303. )
  304. ],
  305. );
  306. });
  307. },
  308. );
  309. }
  310. // 关进小黑屋
  311. putIntoBlackHouse(item) {
  312. int authorId = item['authorid'];
  313. String portrait = "${item['portrait']}";
  314. String nickname = "${item['author']}";
  315. DataUtils.getUserInfo().then((info) {
  316. if (info != null) {
  317. int loginUserId = info.id;
  318. Map<String, String> params = Map();
  319. params['userid'] = '$loginUserId';
  320. params['authorid'] = '$authorId';
  321. params['authoravatar'] = portrait;
  322. params['authorname'] = Utf8Utils.encode(nickname);
  323. NetUtils.post(Api.ADD_TO_BLACK, params: params).then((data) {
  324. Navigator.of(context).pop();
  325. if (data != null) {
  326. var obj = json.decode(data);
  327. if (obj['code'] == 0) {
  328. // 添加到小黑屋成功
  329. showAddBlackHouseResultDialog("添加到小黑屋成功!");
  330. BlackListUtils.addBlackId(authorId).then((arg) {
  331. // 添加之后,重新过滤数据
  332. filterList(normalTweetsList, false);
  333. filterList(hotTweetsList, true);
  334. });
  335. } else {
  336. // 添加失败
  337. var msg = obj['msg'];
  338. showAddBlackHouseResultDialog("添加到小黑屋失败:$msg");
  339. }
  340. }
  341. }).catchError((e) {
  342. Navigator.of(context).pop();
  343. showAddBlackHouseResultDialog("网络请求出错:$e");
  344. });
  345. }
  346. });
  347. }
  348. showAddBlackHouseResultDialog(String msg) {
  349. showDialog(
  350. context: context,
  351. builder: (BuildContext ctx) {
  352. return AlertDialog(
  353. title: Text('提示'),
  354. content: Text(msg),
  355. actions: <Widget>[
  356. FlatButton(
  357. child: Text(
  358. '确定',
  359. style: TextStyle(color: Colors.red),
  360. ),
  361. onPressed: () {
  362. Navigator.of(context).pop();
  363. },
  364. )
  365. ],
  366. );
  367. });
  368. }
  369. renderHotRow(i) {
  370. if (i.isOdd) {
  371. return Divider(
  372. height: 1.0,
  373. );
  374. } else {
  375. i = i ~/ 2;
  376. return getRowWidget(hotTweetsList[i]);
  377. }
  378. }
  379. renderNormalRow(i) {
  380. if (i.isOdd) {
  381. return Divider(
  382. height: 1.0,
  383. );
  384. } else {
  385. i = i ~/ 2;
  386. return getRowWidget(normalTweetsList[i]);
  387. }
  388. }
  389. int getRow(int n) {
  390. int a = n % 3;
  391. int b = n ~/ 3;
  392. if (a != 0) {
  393. return b + 1;
  394. }
  395. return b;
  396. }
  397. Future<Null> _pullToRefresh() async {
  398. curPage = 1;
  399. getTweetsList(false, false);
  400. return null;
  401. }
  402. Widget getHotListView() {
  403. if (hotTweetsList == null) {
  404. getTweetsList(false, true);
  405. return Center(
  406. child: CircularProgressIndicator(),
  407. );
  408. } else {
  409. // 热门动弹列表
  410. return ListView.builder(
  411. itemCount: hotTweetsList.length * 2 - 1,
  412. itemBuilder: (context, i) => renderHotRow(i),
  413. );
  414. }
  415. }
  416. Widget getNormalListView() {
  417. if (normalTweetsList == null) {
  418. getTweetsList(false, false);
  419. return Center(
  420. child: CircularProgressIndicator(),
  421. );
  422. } else {
  423. // 普通动弹列表
  424. return RefreshIndicator(
  425. child: ListView.builder(
  426. itemCount: normalTweetsList.length * 2 - 1,
  427. itemBuilder: (context, i) => renderNormalRow(i),
  428. physics: const AlwaysScrollableScrollPhysics(),
  429. controller: _controller,
  430. ),
  431. onRefresh: _pullToRefresh);
  432. }
  433. }
  434. @override
  435. Widget build(BuildContext context) {
  436. if (!isUserLogin) {
  437. return Center(
  438. child: Column(
  439. mainAxisAlignment: MainAxisAlignment.center,
  440. children: [
  441. Container(
  442. padding: const EdgeInsets.all(10.0),
  443. child: Center(
  444. child: Column(
  445. children: [
  446. Text("由于OSC的openapi限制"),
  447. Text("必须登录后才能获取动弹信息")
  448. ],
  449. ),
  450. )
  451. ),
  452. InkWell(
  453. child: Container(
  454. padding: const EdgeInsets.fromLTRB(15.0, 8.0, 15.0, 8.0),
  455. child: Text("去登录"),
  456. decoration: BoxDecoration(
  457. border: Border.all(color: Colors.black),
  458. borderRadius: BorderRadius.all(Radius.circular(5.0))
  459. ),
  460. ),
  461. onTap: () async {
  462. final result = await Navigator.of(context).push(MaterialPageRoute(builder: (BuildContext context) {
  463. return LoginPage();
  464. }));
  465. if (result != null && result == "refresh") {
  466. // 通知动弹页面刷新
  467. Constants.eventBus.fire(LoginEvent());
  468. }
  469. },
  470. ),
  471. ],
  472. ),
  473. );
  474. }
  475. return DefaultTabController(
  476. length: 2,
  477. child: Scaffold(
  478. appBar: TabBar(
  479. tabs: <Widget>[
  480. Tab(
  481. text: "动弹列表",
  482. ),
  483. Tab(
  484. text: "热门动弹",
  485. )
  486. ],
  487. ),
  488. body: TabBarView(
  489. children: [getNormalListView(), getHotListView()],
  490. )),
  491. );
  492. }
  493. }