post_page.dart 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631
  1. import 'dart:io';
  2. import 'dart:math';
  3. import 'package:cached_network_image/cached_network_image.dart';
  4. import 'package:flutter/material.dart';
  5. import 'package:fluttertoast/fluttertoast.dart';
  6. import 'package:images_picker/images_picker.dart';
  7. import 'package:provider/provider.dart';
  8. import 'package:sport/bean/forum.dart';
  9. import 'package:sport/bean/image.dart' as photo;
  10. import 'package:sport/bean/post.dart';
  11. import 'package:sport/pages/social/gallery_photo_view.dart';
  12. import 'package:sport/provider/user_model.dart';
  13. import 'package:sport/services/api/inject_api.dart';
  14. import 'package:sport/utils/toast.dart';
  15. import 'package:sport/widgets/appbar.dart';
  16. import 'package:sport/widgets/button_primary.dart';
  17. import 'package:sport/widgets/dialog/alert_dialog.dart';
  18. import 'package:sport/widgets/dialog/bindphone_dialog.dart';
  19. import 'package:sport/widgets/space.dart';
  20. import 'package:umeng_common_sdk/umeng_common_sdk.dart';
  21. class PostPage extends StatefulWidget {
  22. final String id; // 论坛Id
  23. final Forum? forum; // 论坛实例
  24. final Post? post; // 帖子 转发的情况下
  25. final String? url; // url 转发的情况下
  26. final String? hash; // 转发的情况下
  27. final String? image; // 转发的情况下
  28. final List<Forum>? forums; // 主要是获取 forums 名字 分享好像拿不到这个东西...
  29. const PostPage(this.id, {this.post, this.forum, this.url, this.hash, this.image, this.forums});
  30. @override
  31. State<StatefulWidget> createState() => _PageState();
  32. }
  33. class _PageState extends State<PostPage> {
  34. List<Media> imageList = [];
  35. TextEditingController? _controller;
  36. ValueNotifier<String> _valueNotifier = ValueNotifier("");
  37. FocusNode? _focusNode;
  38. ValueNotifier<int> labelIndex = ValueNotifier(0);
  39. Forum? selectLabel;
  40. @override
  41. void initState() {
  42. super.initState();
  43. _focusNode = FocusNode();
  44. _controller = TextEditingController()..addListener(() {});
  45. //...
  46. if (widget.post != null) {
  47. _controller?.text = "转发帖子";
  48. _valueNotifier.value = "转发帖子";
  49. }
  50. UmengCommonSdk.onEvent("social_new_post", {});
  51. }
  52. @override
  53. void dispose() {
  54. super.dispose();
  55. _controller?.dispose();
  56. _focusNode?.dispose();
  57. _valueNotifier.dispose();
  58. PaintingBinding.instance?.imageCache?.clear();
  59. }
  60. @override
  61. Widget build(BuildContext context) {
  62. return Scaffold(
  63. backgroundColor: Colors.white,
  64. appBar: AppBar(
  65. leading: buildBackButton(context),
  66. title: Row(
  67. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  68. children: <Widget>[
  69. Text(""),
  70. PrimaryButton(
  71. width: 65,
  72. height: 35,
  73. content: "发布",
  74. callback: () async {
  75. UmengCommonSdk.onEvent("social_new_post_click", {});
  76. if (await showBindPhoneDialog(context) != true) {
  77. return;
  78. }
  79. // if(widget.forum != null) {
  80. // NavigatorUtil.pushAndRemoveUntil(context, (context) => SocialDetailPage(widget.forum, index: 2), RouteSettings(name: "forum"));
  81. // }else {
  82. // Navigator.of(context).pop(true);
  83. // }
  84. _focusNode?.unfocus();
  85. String postValue = _valueNotifier.value.trim();
  86. if (postValue == "") {
  87. ToastUtil.show("不能发布空白内容喔!");
  88. return;
  89. }
  90. if (await showDialog(
  91. context: context,
  92. builder: (context) => CustomAlertDialog(title: '是否确认发布', ok: () => Navigator.of(context).pop(true)),
  93. ) !=
  94. true) {
  95. return;
  96. }
  97. bool result = await showDialog(
  98. context: context,
  99. barrierDismissible: false,
  100. builder: (context) => SimpleDialog(
  101. children: <Widget>[PostAction(selectLabel?.forumId ?? widget.id, postValue, imageList, widget.post?.quoteSubjectId == '0' ? widget.post?.id : widget.post?.quoteSubjectId, widget.url, widget.hash, widget.image)],
  102. ));
  103. if (result == true) {
  104. ToastUtil.show("发布成功");
  105. await Future.delayed(Duration(seconds: 1));
  106. // if(widget.forum != null) {
  107. // NavigatorUtil.pushAndRemoveUntil(context, (context) => SocialDetailPage(widget.forum, index: 2), RouteSettings(name: "forum"));
  108. // }else {
  109. Navigator.of(context).pop(true);
  110. // }
  111. } else {
  112. // ToastUtil.show("已取消发布");
  113. }
  114. },
  115. )
  116. ],
  117. ),
  118. ),
  119. body: SingleChildScrollView(
  120. child: Padding(
  121. padding: const EdgeInsets.symmetric(horizontal: 12.0),
  122. child: Form(
  123. onWillPop: () async {
  124. if (_valueNotifier.value.isNotEmpty || imageList.isNotEmpty) {
  125. bool result = await showDialog(
  126. context: context,
  127. barrierDismissible: false,
  128. builder: (context) {
  129. return CustomAlertDialog(
  130. title: '确认关闭吗?',
  131. ok: () {
  132. Navigator.of(context).pop(true);
  133. },
  134. );
  135. }) ??
  136. false;
  137. return result;
  138. }
  139. return true;
  140. },
  141. child: Column(
  142. children: <Widget>[
  143. TextFormField(
  144. focusNode: _focusNode,
  145. controller: _controller,
  146. keyboardType: TextInputType.multiline,
  147. maxLines: 8,
  148. maxLength: 500,
  149. style: TextStyle(fontSize: 16),
  150. strutStyle: StrutStyle(forceStrutHeight: true, height: 1.4),
  151. onChanged: (v) {
  152. _valueNotifier.value = v;
  153. if (_valueNotifier.value.length == 500) {
  154. ToastUtil.show("文字数量已达上限");
  155. }
  156. },
  157. buildCounter: (
  158. BuildContext context, {
  159. required int currentLength,
  160. int? maxLength,
  161. required bool isFocused,
  162. }) {
  163. return Align(
  164. alignment: Alignment.centerLeft,
  165. child: Padding(
  166. padding: const EdgeInsets.only(left: 8.0),
  167. child: Text("$currentLength/$maxLength"),
  168. ));
  169. },
  170. cursorColor: Theme.of(context).colorScheme.secondary,
  171. decoration: InputDecoration(hintText: widget.post == null ? '发表你的看法...' : "", border: InputBorder.none, contentPadding: EdgeInsets.symmetric(horizontal: 6), hintStyle: TextStyle(color: Color(0xff999999))),
  172. ),
  173. Space(
  174. height: 16,
  175. ),
  176. widget.post == null
  177. ? widget.url != null
  178. ? _postLink()
  179. : widget.image != null
  180. ? _postSharePoster(widget.image!)
  181. : GridView.builder(
  182. padding: EdgeInsets.zero,
  183. shrinkWrap: true,
  184. physics: NeverScrollableScrollPhysics(),
  185. itemCount: imageList.length + (imageList.length < 9 ? 1 : 0),
  186. gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3, crossAxisSpacing: 12.0, mainAxisSpacing: 12.0),
  187. itemBuilder: (context, index) {
  188. return ClipRRect(
  189. borderRadius: BorderRadius.circular(6),
  190. child: index >= imageList.length
  191. ? InkWell(
  192. onTap: () {
  193. _select();
  194. },
  195. child: Image.asset(
  196. "lib/assets/img/bbs_icon_addimage.png",
  197. fit: BoxFit.cover,
  198. ),
  199. )
  200. : Container(
  201. child: Stack(
  202. children: [
  203. Image.file(
  204. File(imageList[index].path),
  205. fit: BoxFit.cover,
  206. width: 200,
  207. height: 200,
  208. ),
  209. Align(
  210. alignment: Alignment.topRight,
  211. child: GestureDetector(
  212. onTap: (){
  213. setState(() {
  214. imageList.removeAt(index);
  215. });
  216. },
  217. child: Container(
  218. margin: const EdgeInsets.all(6),
  219. padding: const EdgeInsets.all(6),
  220. decoration: BoxDecoration(shape: BoxShape.circle, color: Colors.black.withOpacity(.6)),
  221. child: Image.asset("lib/assets/img/btn_close_small.png", color: Colors.white,),
  222. ),
  223. ))
  224. ],
  225. ),
  226. ),
  227. );
  228. })
  229. : Container(
  230. padding: EdgeInsets.all(11.0),
  231. width: double.infinity,
  232. decoration: BoxDecoration(shape: BoxShape.rectangle, borderRadius: BorderRadius.all(Radius.circular(10)), color: Theme.of(context).scaffoldBackgroundColor),
  233. child: _postWidget(),
  234. ),
  235. // if(widget.url != null)
  236. // _postLink(),
  237. Space(
  238. height: 21.0,
  239. ),
  240. Divider(),
  241. if (widget.forums?.isNotEmpty == true) _postGameLabel(),
  242. ],
  243. ),
  244. ),
  245. ),
  246. ),
  247. );
  248. }
  249. Widget _postWidget() {
  250. Post? post = widget.post?.quoteSubject ?? widget.post;
  251. if (post == null) return Container();
  252. double width = MediaQuery.of(context).size.width - 24 - 22;
  253. return Column(
  254. crossAxisAlignment: CrossAxisAlignment.start,
  255. children: <Widget>[
  256. RichText(
  257. maxLines: 3,
  258. overflow: TextOverflow.ellipsis,
  259. text: TextSpan(style: Theme.of(context).textTheme.subtitle1!.copyWith(fontSize: 16), children: <InlineSpan>[
  260. TextSpan(text: '${post.nickname}:', style: Theme.of(context).textTheme.subtitle1!.copyWith(color: Theme.of(context).accentColor)),
  261. TextSpan(text: '${post.content}', style: Theme.of(context).textTheme.subtitle1!),
  262. ]),
  263. ),
  264. if (post.images?.isNotEmpty == true)
  265. GridView.count(
  266. physics: new NeverScrollableScrollPhysics(),
  267. shrinkWrap: true,
  268. padding: EdgeInsets.only(top: 15),
  269. childAspectRatio: post.images!.length == 1 ? max(16 / 10, post.images![0].getImageAspectRatio()) : 1,
  270. crossAxisSpacing: 10.0,
  271. crossAxisCount: min(3, post.images!.length),
  272. children: post.images!
  273. .asMap()
  274. .keys
  275. .take(min(3, post.images!.length))
  276. .map((i) => GestureDetector(
  277. onTap: () => open(context, i, post.images!),
  278. child: i < 2
  279. ? post.images!.length == 1
  280. ? Row(
  281. mainAxisSize: MainAxisSize.min,
  282. children: <Widget>[
  283. ClipRRect(
  284. borderRadius: BorderRadius.circular(6),
  285. child: Stack(
  286. children: <Widget>[
  287. CachedNetworkImage(
  288. alignment: Alignment.centerLeft,
  289. imageUrl: post.images![i].thumbnail ?? "",
  290. fit: BoxFit.cover,
  291. width: post.images![i].getWidth(width),
  292. ),
  293. if (post.images![i].isLongImage())
  294. Positioned(
  295. bottom: 4,
  296. right: 4,
  297. child: Container(
  298. padding: EdgeInsets.symmetric(horizontal: 8, vertical: 2),
  299. decoration: BoxDecoration(color: Colors.black.withOpacity(.8), borderRadius: BorderRadius.all(Radius.circular(20))),
  300. child: Text(
  301. "长图",
  302. style: Theme.of(context).textTheme.bodyText1!.copyWith(color: Colors.white),
  303. ),
  304. ),
  305. )
  306. ],
  307. ))
  308. ],
  309. )
  310. : ClipRRect(borderRadius: BorderRadius.circular(6), child: CachedNetworkImage(alignment: Alignment.centerLeft, imageUrl: post.images![i].thumbnail ?? "", fit: BoxFit.cover))
  311. : ClipRRect(
  312. borderRadius: BorderRadius.circular(6),
  313. child: Stack(
  314. fit: StackFit.expand,
  315. children: <Widget>[
  316. CachedNetworkImage(
  317. imageUrl: post.images![i].thumbnail ?? "",
  318. fit: BoxFit.cover,
  319. ),
  320. if (post.images!.length - 3 > 0)
  321. Container(
  322. color: Color(0x80000000),
  323. child: Center(
  324. child: Text(
  325. "+${post.images!.length - 3}",
  326. style: TextStyle(color: Colors.white, fontSize: 16),
  327. ),
  328. ),
  329. )
  330. ],
  331. ))))
  332. .toList()),
  333. if (post.quoteData != null) _postLink(),
  334. ],
  335. );
  336. }
  337. Widget _postLink() {
  338. return Container(
  339. padding: EdgeInsets.all(12.0),
  340. color: Colors.white,
  341. child: Row(crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.max, children: <Widget>[
  342. Icon(
  343. Icons.link,
  344. size: 60.0,
  345. ),
  346. Space(
  347. width: 5.0,
  348. ),
  349. Expanded(
  350. child: RichText(
  351. maxLines: 3,
  352. overflow: TextOverflow.ellipsis,
  353. text: TextSpan(style: Theme.of(context).textTheme.subtitle1!.copyWith(fontSize: 16), children: <InlineSpan>[
  354. TextSpan(text: '${Provider.of<UserModel>(context).user.name}:', style: Theme.of(context).textTheme.subtitle1!.copyWith(color: Theme.of(context).accentColor)),
  355. TextSpan(text: '分享了他的运动记录,快来围观吧~', style: Theme.of(context).textTheme.subtitle1!),
  356. ]),
  357. ),
  358. ),
  359. ]),
  360. );
  361. }
  362. Widget _postSharePoster(String image) {
  363. return Row(
  364. mainAxisAlignment: MainAxisAlignment.start,
  365. children: <Widget>[
  366. Container(
  367. constraints: BoxConstraints(
  368. maxWidth: 100,
  369. maxHeight: 200,
  370. ),
  371. child: ClipRRect(
  372. borderRadius: BorderRadius.circular(6),
  373. child: Image.file(
  374. File(image),
  375. fit: BoxFit.cover,
  376. )),
  377. )
  378. ],
  379. );
  380. }
  381. void _select() async {
  382. _focusNode?.unfocus();
  383. int max = 9 - imageList.length;
  384. if (max <= 0) {
  385. Fluttertoast.showToast(msg: "不能再添加了~", backgroundColor: Colors.black, textColor: Colors.white, fontSize: 16.0);
  386. return;
  387. }
  388. try {
  389. List<Media>? resultList = await ImagesPicker.pick(quality: 0.8, maxSize: 1024, count: 9);
  390. if (!mounted) return;
  391. setState(() {
  392. imageList += resultList ?? [];
  393. });
  394. } on Exception catch (e) {
  395. return;
  396. }
  397. }
  398. Widget _labelItem(String? title) {
  399. if (title == null) return Container();
  400. return Container(
  401. padding: EdgeInsets.symmetric(vertical: 6.0, horizontal: 16.0),
  402. decoration: BoxDecoration(border: Border.all(color: Theme.of(context).accentColor), borderRadius: BorderRadius.all(Radius.circular(44.0))),
  403. child: Row(
  404. crossAxisAlignment: CrossAxisAlignment.center,
  405. children: <Widget>[
  406. Text(
  407. title,
  408. style: TextStyle(color: Theme.of(context).accentColor, fontSize: 12.0),
  409. strutStyle: StrutStyle(forceStrutHeight: true),
  410. ),
  411. Space(
  412. width: 5.0,
  413. ),
  414. Image.asset(
  415. "lib/assets/img/btn_close_yellow.png",
  416. width: 7.0,
  417. height: 7.0,
  418. )
  419. ],
  420. ),
  421. );
  422. }
  423. Widget _postGameLabel() {
  424. return Row(
  425. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  426. children: <Widget>[
  427. InkWell(
  428. child: selectLabel != null ? _labelItem(selectLabel?.gameName ?? "") : Container(),
  429. onTap: () {
  430. selectLabel = new Forum();
  431. setState(() {});
  432. },
  433. ),
  434. InkWell(
  435. child: Container(
  436. height: 34.0,
  437. child: Row(
  438. // crossAxisAlignment: CrossAxisAlignment.center,
  439. children: <Widget>[
  440. Text(
  441. "添加运动标签",
  442. style: TextStyle(fontSize: 12.0, color: Color(0xff666666)),
  443. ),
  444. Divider(
  445. height: 2.0,
  446. ),
  447. Space(
  448. width: 4.0,
  449. ),
  450. Image.asset("lib/assets/img/btn_arrow_bottom.png")
  451. ],
  452. ),
  453. ),
  454. onTap: () async {
  455. bool flag = await showDialog(
  456. context: context,
  457. builder: (context) => CustomAlertDialog(
  458. title: "添加运动标签",
  459. isLine: true,
  460. ok: () => Navigator.of(context).pop(true),
  461. child: Container(
  462. padding: EdgeInsets.only(left: 20.0),
  463. width: double.infinity,
  464. child: ValueListenableBuilder(
  465. valueListenable: labelIndex,
  466. builder: (context, index, child) => Wrap(runSpacing: 12.0, spacing: 8.0, children: widget.forums!.asMap().entries.map((e) => _buildDrawerButtonItem(e.value, e.key, labelIndex)).toList()),
  467. ),
  468. ),
  469. ));
  470. // print("${labelIndex}==========================================");
  471. if (flag) {
  472. selectLabel = widget.forums?[labelIndex.value];
  473. setState(() {});
  474. }
  475. },
  476. ),
  477. ],
  478. );
  479. }
  480. Widget _buildDrawerButtonItem(Forum data, int index, ValueNotifier<int> targetIndex) {
  481. return InkWell(
  482. child: Container(
  483. decoration: BoxDecoration(color: index == targetIndex.value ? Theme.of(context).accentColor : Colors.white, borderRadius: BorderRadius.all(Radius.circular(20.0)), border: Border.all(color: index == targetIndex.value ? Colors.white : Theme.of(context).dividerTheme.color!)),
  484. padding: EdgeInsets.symmetric(vertical: 8.0, horizontal: 20.0),
  485. child: Text(
  486. data.gameName ?? "",
  487. strutStyle: StrutStyle(forceStrutHeight: true),
  488. style: TextStyle(fontSize: 14.0, color: index == targetIndex.value ? Colors.white : Color(0xff999999)),
  489. ),
  490. ),
  491. onTap: () {
  492. labelIndex.value = index;
  493. },
  494. );
  495. }
  496. }
  497. class PostAction extends StatefulWidget {
  498. final String forumId;
  499. final String? content;
  500. final List<Media>? imageList;
  501. final String? quoteSubjectId;
  502. final String? url;
  503. final String? hash;
  504. final String? image;
  505. const PostAction(this.forumId, this.content, this.imageList, this.quoteSubjectId, this.url, this.hash, this.image);
  506. @override
  507. State<StatefulWidget> createState() => PostActionState();
  508. }
  509. class PostActionState extends State<PostAction> with InjectApi {
  510. final Map<Media, photo.Image> upload = {};
  511. late ValueNotifier<String> _msg;
  512. bool _disposed = false;
  513. @override
  514. void initState() {
  515. super.initState();
  516. _disposed = false;
  517. _msg = ValueNotifier<String>("请稍候...");
  518. WidgetsBinding.instance?.addPostFrameCallback((timeStamp) {
  519. post();
  520. });
  521. }
  522. @override
  523. void dispose() {
  524. _disposed = true;
  525. _msg.dispose();
  526. super.dispose();
  527. }
  528. void post() async {
  529. List<Media> imageList = [];
  530. if (widget.imageList?.isNotEmpty == true) {
  531. imageList.addAll(widget.imageList!);
  532. }
  533. if (widget.image != null) {
  534. imageList.add(Media(size: 0, path: widget.image!));
  535. }
  536. debugPrint("post add image ${imageList.map((e) => e.path).toList()}");
  537. if (imageList.isNotEmpty) {
  538. for (var i = 0; i < imageList.length; i++) {
  539. if (_disposed) break;
  540. Media asset = imageList[i];
  541. if (upload.containsKey(asset)) continue;
  542. File file = File(asset.path);
  543. _msg.value = "上传图片(${i + 1}/${imageList.length})...";
  544. try {
  545. var resp = await api.mediaUp4Subject(file, srcType: "image");
  546. photo.Image? image = resp.data;
  547. if (image != null) {
  548. upload[asset] = image;
  549. debugPrint("post upload image ${image.toJson()}");
  550. }
  551. } catch (e, stack) {
  552. debugPrintStack(stackTrace: stack);
  553. ToastUtil.show("第${i + 1}张图片检测不通过,请勿使用色情、暴力、广告等的图片");
  554. Navigator.of(context).pop(false);
  555. return;
  556. }
  557. // await Future.delayed(Duration(seconds: 3));
  558. }
  559. }
  560. _msg.value = "发布中...";
  561. // await Future.delayed(Duration(seconds: 3));
  562. if (_disposed) return;
  563. var data;
  564. // 这里我也没办法知道它之前的名字叫什么吧...
  565. if (widget.url != null) {
  566. data = await api.postForum(widget.forumId, widget.content ?? "",
  567. images: upload.values.map((e) => e.id).toList().join(","), quoteSubjectId: widget.quoteSubjectId, quoteData: '{"username":{"value":"${Provider.of<UserModel>(context, listen: false).user.name}","from":"user#${Provider.of<UserModel>(context, listen: false).user.id}"},"url":{"value":"${widget.url}"},"hash":{"value":"${widget.hash}"}}');
  568. } else {
  569. data = await api.postForum(widget.forumId, widget.content ?? "", images: upload.values.map((e) => e.id).toList().join(","), quoteSubjectId: widget.quoteSubjectId);
  570. }
  571. await Future.delayed(Duration(seconds: 1));
  572. if (data != null && data.code == 0) {
  573. for (var i = 0; i < imageList.length; i++) {
  574. Media asset = imageList[i];
  575. File file = File(asset.path);
  576. file.delete();
  577. }
  578. Navigator.of(context).pop(true);
  579. } else {
  580. Navigator.of(context).pop(false);
  581. }
  582. }
  583. @override
  584. Widget build(BuildContext context) {
  585. return Padding(
  586. padding: const EdgeInsets.symmetric(vertical: 16),
  587. child: Column(
  588. children: <Widget>[
  589. CircularProgressIndicator(),
  590. Padding(
  591. padding: const EdgeInsets.only(top: 15),
  592. child: ValueListenableBuilder(valueListenable: _msg, builder: (BuildContext context, String value, Widget? child) => Text(value)),
  593. )
  594. ],
  595. ),
  596. );
  597. }
  598. }