post_page.dart 24 KB

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