import 'dart:convert'; import 'dart:math'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_easyrefresh/easy_refresh.dart'; import 'package:provider/provider.dart'; import 'package:sport/bean/comment.dart'; import 'package:sport/bean/post.dart'; import 'package:sport/bean/post_user.dart'; import 'package:sport/pages/social/gallery_photo_view.dart'; import 'package:sport/pages/social/notification.dart'; import 'package:sport/pages/social/post_comment.dart'; import 'package:sport/pages/social/post_share_page.dart'; import 'package:sport/pages/social/share_webview.dart'; import 'package:sport/provider/lib/provider_widget.dart'; import 'package:sport/provider/lib/view_state_lifecycle.dart'; import 'package:sport/provider/post_detail_model.dart'; import 'package:sport/provider/user_model.dart'; import 'package:sport/router/navigator_util.dart'; import 'package:sport/services/userid.dart'; import 'package:sport/utils/DateFormat.dart'; import 'package:sport/utils/toast.dart'; import 'package:sport/widgets/appbar.dart'; import 'package:sport/widgets/button_primary.dart'; import 'package:sport/widgets/decoration.dart'; import 'package:sport/widgets/dialog/alert_dialog.dart'; import 'package:sport/widgets/dialog/bindphone_dialog.dart'; import 'package:sport/widgets/dialog/comment_dialog.dart'; import 'package:sport/widgets/dialog/modal_bottom_action.dart'; import 'package:sport/widgets/dialog/request_dialog.dart'; import 'package:sport/widgets/error.dart'; import 'package:sport/widgets/image.dart'; import 'package:sport/widgets/loading.dart'; import 'package:sport/widgets/misc.dart'; import 'package:sport/widgets/popmenu_bg.dart'; import 'package:sport/widgets/space.dart'; import 'package:sport/widgets/text_input.dart' as input; import 'chat_page.dart'; class PostDetailPage extends StatefulWidget { final List posts; final Post post; final bool comment; PostDetailPage(this.post, this.comment, this.posts); @override State createState() => _PageState(); } class _PageState extends ViewStateLifecycle with UserId, TickerProviderStateMixin { FocusNode _focusNode; ScrollController _controller; int _inputType = 0; Post _post; static const List _sort = ["最新评论", "点赞最多"]; String _sortType = _sort[0]; final GlobalKey _key = GlobalKey(); double _offset = 0; var _padding = Space( width: 5, ); var _scrollListener; var _animationController; var _iconAnimation; Comment _toComment; PostUser _toUser; @override PostDetailModel createModel() => PostDetailModel(widget.post); @override void initState() { super.initState(); _post = widget.post; _focusNode = FocusNode(); _scrollListener = () { resetInput(); }; _controller = ScrollController()..addListener(_scrollListener); WidgetsBinding.instance.addPostFrameCallback((timeStamp) { RenderBox box = _key.currentContext.findRenderObject(); var offset = box.localToGlobal(Offset.zero); final double statusBarHeight = MediaQuery.of(context).padding.top; _offset = offset.dy - kToolbarHeight - statusBarHeight; }); if (widget.comment) { setState(() { _inputType = 1; }); WidgetsBinding.instance.addPostFrameCallback((timeStamp) { FocusScope.of(context).requestFocus(_focusNode); }); } _animationController = AnimationController(duration: Duration(milliseconds: 300), vsync: this); _iconAnimation = TweenSequence([ TweenSequenceItem( tween: Tween(begin: 1.0, end: 1.5) .chain(CurveTween(curve: Curves.easeIn)), weight: 50), TweenSequenceItem(tween: Tween(begin: 1.5, end: 1.0), weight: 50), ]).animate(_animationController); } void _scrollToComment() { _controller.animateTo(_offset, duration: Duration(milliseconds: 500), curve: Curves.ease); } void resetInput() { if (_focusNode.hasFocus) { _focusNode.unfocus(); setState(() { _toComment = null; _toUser = null; _inputType = 0; }); } } void readyInput() { setState(() { _inputType = 1; }); WidgetsBinding.instance.addPostFrameCallback((timeStamp) { FocusScope.of(context).requestFocus(_focusNode); }); } @override void dispose() { _controller?.dispose(); _focusNode?.dispose(); super.dispose(); } // /** // * 1: 评论 // * 2: 点赞 // */ _updatePost(int type, int num) { if (widget.posts == null) { _post?.toggleLike(); return; } widget.posts.forEach((element) { if (element.id == widget.post.id) { if (type == 1) { element.commentCount = element.commentCount + num; } else { element.toggleLike(); } } }); } Widget _postWidget(Post post) { print(post.quoteData); double width = MediaQuery.of(context).size.width - 24 - 22; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ RichText( maxLines: 3, overflow: TextOverflow.ellipsis, text: TextSpan(style: Theme.of(context).textTheme.subtitle1.copyWith(fontSize: 16), children: [ TextSpan(text: '${post.nickname}:', style: Theme.of(context).textTheme.subtitle1.copyWith(color: Theme.of(context).accentColor)), TextSpan(text: '${post.content}', style: Theme.of(context).textTheme.subtitle1), ]), ), if (post.images.length > 0) GridView.count( physics: new NeverScrollableScrollPhysics(), shrinkWrap: true, padding: EdgeInsets.only(top: 15), childAspectRatio: post.images.length == 1 ? max(16 / 10, post.images[0].getImageAspectRatio()) : 1, crossAxisSpacing: 10.0, crossAxisCount: min(3, post.images.length), children: post.images .asMap() .keys .take(min(3, post.images.length)) .map((i) => GestureDetector( onTap: () => open(context, i, post.images), child: i < 2 ? post.images.length == 1 ? Row( mainAxisSize: MainAxisSize.min, children: [ ClipRRect( borderRadius: BorderRadius.circular(6), child: Stack( children: [ CachedNetworkImage( alignment: Alignment.centerLeft, imageUrl: post.images[i].thumbnail, fit: BoxFit.cover, width: post.images[i].getWidth(width), ), if (post.images[i].isLongImage()) Positioned( bottom: 4, right: 4, child: Container( padding: EdgeInsets.symmetric(horizontal: 8, vertical: 2), decoration: BoxDecoration(color: Colors.black.withOpacity(.8), borderRadius: BorderRadius.all(Radius.circular(20))), child: Text( "长图", style: Theme.of(context).textTheme.bodyText1.copyWith(color: Colors.white), ), ), ) ], )) ], ) : ClipRRect( borderRadius: BorderRadius.circular(6), child: CachedNetworkImage(alignment: Alignment.centerLeft, imageUrl: post.images[i].thumbnail, fit: BoxFit.cover)) : ClipRRect( borderRadius: BorderRadius.circular(6), child: Stack( fit: StackFit.expand, children: [ CachedNetworkImage( imageUrl: post.images[i].thumbnail, fit: BoxFit.cover, ), if (post.images.length - 3 > 0) Container( color: Color(0x80000000), child: Center( child: Text( "+${post.images.length - 3}", style: TextStyle(color: Colors.white, fontSize: 16), ), ), ) ], )))) .toList()), ], ); } Widget _postLink(String quote){ var data = json.decode(quote); return Container( color: Theme.of(context).scaffoldBackgroundColor, child: Row( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.max, children: [ CachedNetworkImage( imageUrl: avatarList[4], width: 60.0, height: 60.0, ), Space( width: 5.0, ), Expanded( child: RichText( maxLines: 3, overflow: TextOverflow.ellipsis, text: TextSpan(style: Theme.of(context).textTheme.subtitle1.copyWith(fontSize: 16), children: [ TextSpan(text: '${data["username"]["value"]}:', style: Theme.of(context).textTheme.subtitle1.copyWith(color: Theme.of(context).accentColor)), TextSpan(text: '分享了他的运动记录,快来围观吧~', style: Theme.of(context).textTheme.subtitle1), ]), ), ),]), ); } Widget _buildPostDetailWidget(Post post) { double width = MediaQuery.of(context).size.width - 24; return Column( children: [ Container( width: double.infinity, padding: EdgeInsets.fromLTRB(12.0, 12.0, 12.0, 6), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ GestureDetector( onTap: () => NavigatorUtil.goSocialUserDetail(context, PostUser(id: post.userId, name: post.nickname, avatar: post.avatar)), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( mainAxisSize: MainAxisSize.min, children: [ CircleAvatar(backgroundImage: userAvatarProvider(post.avatar), radius: 18), Space( width: 8, ), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Text( post.nickname, style: Theme.of(context).textTheme.subtitle1.copyWith(fontWeight: FontWeight.w600), ), if (post?.isOfficial == "1") Container( decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.all(Radius.circular(2)), border: Border.all( color: Theme.of(context).accentColor, width: .5, )), padding: EdgeInsets.fromLTRB(2, 0, 2, 1), margin: EdgeInsets.fromLTRB(12.0, 0, 12, 0), child: Text( "官方认证", strutStyle: fixedLine, style: Theme.of(context).textTheme.subtitle2.copyWith(color: Theme.of(context).accentColor, fontSize: 11), )) ], ), Space( height: 4, ), Text(DateFormat.formatTime(post.createTime), style: Theme.of(context).textTheme.bodyText1) ], ), ], ), post.isFriend() ? GestureDetector( child: Container( // width: 70, height: 35, alignment: Alignment.center, child: Text( "已关注", strutStyle: fixedLine, style: Theme.of(context).textTheme.subtitle1.copyWith(color: Color(0xff999999)), ), // Row(mainAxisSize: MainAxisSize.min, children: [ // Image.asset("lib/assets/img/mine_icon_followed.png"), // Space( // width: 6, // ), // // ]), // decoration: BoxDecoration( // borderRadius: BorderRadius.circular(20), // border: Border.all( // color: Theme.of(context).accentColor, // width: .5, // ), // ), ), onTap: () async { await request(context, () async { var resp = await model.api.userUnFollow(uid: int.parse(post.userId)).catchError((onError) {}); if (resp?.code == 0) { post.followStatus = "none"; // widget.userFriends?.firstWhere((element) => element.uid == user.id)?.isFriends = "0"; setState(() {}); ToastUtil.show("取关成功"); } }); }, ) : post.isMe(Provider.of(context,listen: false).user.id)? Container() : // PrimaryButton( // width: 90, // height: 35, // callback: () async { // await request(context, () async { // var resp = await model.api.userFollow(uid: int.parse(post.userId)).catchError((onError) {}); // if (resp?.code == 0) { // post.followStatus = "followed"; // // 修改本地的不知道为什么这里不要 先保留... //// var db = widget.userFriends?.firstWhere((element) => element.uid == user.id); //// if (db != null) { //// db.isFriends = "1"; //// db.isIgnore = 0; //// } // setState(() {}); // ToastUtil.show("关注成功"); // } // }); // }, // content: '', // shadow: false, // child: Row(mainAxisSize: MainAxisSize.min, children: [ // Image.asset("lib/assets/img/mine_icon_follow.png"), // Space( // width: 6, // ), // Text( // "关注", // strutStyle: fixedLine, // style: Theme.of(context).textTheme.subtitle1.copyWith(color: Colors.white), // ) // ]), // ), GestureDetector( child: Container( width: 70, height: 30, alignment: Alignment.center, child: Text( "关注", strutStyle: fixedLine, style: Theme.of(context).textTheme.subtitle1.copyWith(color: Theme.of(context).accentColor), ), decoration: BoxDecoration( borderRadius: BorderRadius.circular(20), border: Border.all( color: Theme.of(context).accentColor, width: .5, ), ), ), onTap: () async { await request(context, () async { var resp = await model.api.userFollow(uid: int.parse(post.userId)).catchError((onError) {}); if (resp?.code == 0) { post.followStatus = "followed"; // 修改本地的不知道为什么这里不要 先保留... // var db = widget.userFriends?.firstWhere((element) => element.uid == user.id); // if (db != null) { // db.isFriends = "1"; // db.isIgnore = 0; // } setState(() {}); ToastUtil.show("关注成功"); } }); } ) ], ), ), if (post.title?.isNotEmpty == true) Padding( padding: const EdgeInsets.only(top: 12.0), child: Text(post.title, style: Theme.of(context).textTheme.subtitle1.copyWith(fontSize: 16, fontWeight: FontWeight.w600)), ), if (post.content?.isNotEmpty == true) GestureDetector( onLongPress: () async { await showActionDialog(context, { "复制文字": () { Clipboard.setData(ClipboardData(text: post.content)); ToastUtil.show("已复制到粘贴板"); } }); }, child: Padding( padding: const EdgeInsets.symmetric(vertical: 12.0), child: Text( post.content, style: Theme.of(context).textTheme.subtitle1.copyWith(fontSize: post.title?.isNotEmpty == true ? 14 : 16), strutStyle: StrutStyle(height: 1.8, forceStrutHeight: true), ), ), ), if (post.images.length > 0) GridView.count( physics: new NeverScrollableScrollPhysics(), shrinkWrap: true, crossAxisSpacing: 10.0, mainAxisSpacing: 10.0, childAspectRatio: post.images.length == 1 ? max(16 / 10, post.images[0].getImageAspectRatio()) : 1, crossAxisCount: min(3, post.images.length), children: post.images .asMap() .keys .map((imageIndex) => GestureDetector( onTap: () => open(context, imageIndex, post.images), child: Hero( tag: post.images[imageIndex].id, child: post.images.length == 1 ? Row( mainAxisSize: MainAxisSize.min, children: [ ClipRRect( borderRadius: BorderRadius.circular(6), child: Stack( children: [ CachedNetworkImage( alignment: Alignment.centerLeft, imageUrl: post.images[imageIndex].thumbnail, fit: BoxFit.cover, width: post.images[imageIndex].getWidth(width), ), if (post.images[imageIndex].isLongImage()) Positioned( bottom: 4, right: 4, child: Container( padding: EdgeInsets.symmetric(horizontal: 8, vertical: 2), decoration: BoxDecoration( color: Colors.black.withOpacity(.8), borderRadius: BorderRadius.all(Radius.circular(20))), child: Text( "长图", style: Theme.of(context).textTheme.bodyText1.copyWith(color: Colors.white), ), ), ) ], )) ], ) : ClipRRect( borderRadius: BorderRadius.circular(6), child: CachedNetworkImage( imageUrl: post.images[imageIndex].thumbnail, fit: BoxFit.cover, ), )), )) .toList(), ), if (post.quoteSubject != null) GestureDetector( behavior: HitTestBehavior.opaque, onTap: () { Navigator.push(context, MaterialPageRoute(builder: (context) { return PostDetailPage(post.quoteSubject, false, []); })); }, child: Container( padding: EdgeInsets.all(11.0), margin: EdgeInsets.symmetric(vertical: 5.0), decoration: BoxDecoration( shape: BoxShape.rectangle, borderRadius: BorderRadius.all(Radius.circular(10)), color: Theme.of(context).scaffoldBackgroundColor), child: _postWidget(post.quoteSubject), ), ), if(post.quoteData != null) GestureDetector( behavior: HitTestBehavior.opaque, onTap: () { Navigator.push(context, MaterialPageRoute(builder: (context) { return WebViewSharePage(json.decode(post.quoteData)["url"]["value"],hash:json.decode(post.quoteData)["hash"]["value"] ,); })); }, child: Container( padding: EdgeInsets.all(11.0), margin: EdgeInsets.symmetric(vertical: 5.0), decoration: BoxDecoration( shape: BoxShape.rectangle, borderRadius: BorderRadius.all(Radius.circular(10)), color: Theme.of(context).scaffoldBackgroundColor), child: _postLink(post.quoteData), ), ), Space( height: 10, ), ], ), ), Divider() ], ); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.white, appBar: AppBar( titleSpacing: -5, title: Container( alignment: Alignment.centerLeft, child: Text( "帖子详情", style: titleStyle, ), ), leading: buildBackButton(context), actions: [ PopupMenuTheme( data: PopupMenuThemeData(shape: PopmenuShape(borderRadius: BorderRadius.all(Radius.circular(10.0)))), child: PopupMenuButton( offset: Offset(0, kToolbarHeight / 2 + 15), icon: iconMoreGray(), onSelected: (val) async { switch (val) { case "report": await request(context, () async { await model.api.postForumReport(subjectId: widget.post.id).catchError((onError) {}); ToastUtil.show("举报成功"); }); break; case "delete": if (await showDialog( context: context, builder: (context) => CustomAlertDialog(title: '确定删除帖子?', ok: () => Navigator.of(context).pop(true)), ) == true) { var result = await request(context, () async { await model.api.postDelSubject(widget.post.id); ToastUtil.show("删除成功"); return true; }); if (result == true) { Navigator.pop(context, true); } } break; } }, itemBuilder: (context) { if (selfId == widget.post?.userId) { return [menuItemCenter('delete', '删除帖子')]; } return [menuItemCenter('report', '举报帖子')]; }, )) ], ), body: ProviderWidget( model: model, onModelReady: (model) => model.initData(), builder: (_, model, __) { return NotificationListener( onNotification: (notification) { if (notification is CommentNotification) { if (notification.type == CommentNotification.TYPE_DEL) { model.list.removeWhere((element) => element.id == notification.comment.id); setState(() {}); } _updatePost(1, notification.type); return true; } if (notification is CommentInputNotification) { // _toComment = notification.comment; // _toUser = notification.comment.socialInfo; // readyInput(); showCommentList(context, notification.comment); return true; } return false; }, child: Column( children: [ Expanded( flex: 1, child: EasyRefresh.custom( controller: model.refreshController, enableControlFinishRefresh: true, enableControlFinishLoad: true, scrollController: _controller, onRefresh: () => model.refresh(), onLoad: model.isIdle ? () => model.loadMore() : null, header: buildClassicalHeader(), footer: buildClassicalFooter(), slivers: [ SliverToBoxAdapter( child: _buildPostDetailWidget(widget.post), ), if (model.isBusy) SliverToBoxAdapter( child: RequestLoadingWidget(), ), if (model.isEmpty) SliverToBoxAdapter( child: RequestErrorWidget( null, msg: "暂无评论~", assets: RequestErrorWidget.ASSETS_NO_COMMENT, ), ), if (model.isIdle) SliverPadding( padding: EdgeInsets.only(bottom: 12), sliver: SliverToBoxAdapter( child: Row( key: _key, mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Padding( padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 12.0), child: Text( "全部评论", style: Theme.of(context).textTheme.headline3, ), ), PopupMenuButton( onSelected: (v) { setState(() { _sortType = v; model.sortBy(_sortType); }); }, itemBuilder: (BuildContext context) { return divideMenus(_sort.map((e) => menuItemSelected(e, e, _sortType == e)).toList()); }, child: Row( mainAxisSize: MainAxisSize.min, children: [ Text( _sortType, style: Theme.of(context).textTheme.bodyText1, ), Padding( padding: const EdgeInsets.fromLTRB(6, 6, 12.0, 6), child: arrowBottom(), ) ], ), ), ], ), )), if (model.isIdle) SliverList( delegate: SliverChildBuilderDelegate( (context, index) { return PostCommentWidget( model.list[index], ); }, childCount: model.list.length, ), ), ], ), ), if (_inputType == 0) Container( decoration: shadowTop(), child: SafeArea( child: Container( height: 50, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ InkWell( onTap: () { NavigatorUtil.goPage(context, (context) => PostSharePage(post:_post)); }, child: Row( children: [ SizedBox( width: 24.0, ), Image.asset("lib/assets/img/bbs_icon_share.png"), _padding, Text( "转发", style: TextStyle(color: Color(0xff333333)), ), ], ), ), GestureDetector( behavior: HitTestBehavior.opaque, onTap: () async { Post post = _post; await (post.isLiked ?? false ? model.api.postForumUnLike(post.id, "subject_id") : model.api.postForumLike(post.id, "subject_id")); if (_iconAnimation.status == AnimationStatus.forward || _iconAnimation.status == AnimationStatus.reverse) { return; } setState(() { _updatePost(2, 0); }); if (_iconAnimation.status == AnimationStatus.dismissed) { _animationController.forward(); } else if (_iconAnimation.status == AnimationStatus.completed) { _animationController.reverse(); } }, child: Center( child: Row( children: [ ScaleTransition( scale: _iconAnimation, child: Image.asset("lib/assets/img/bbs_icon_like${_post.isLiked ?? false ? "_complete" : ""}.png")), _padding, Text( "${_post.likeCount}", style: TextStyle(color: Color(0xff333333)), ), ], ), ), ), Padding( padding: const EdgeInsets.symmetric(horizontal: 12.0), child: PrimaryButton( width: 149, height: 35, callback: () async { if (await showBindPhoneDialog(context) == true) { readyInput(); } }, content: "发表评论", ), ) ], ), ), ), ), if (_inputType == 1) input.TextInput( widget.post.id, focusNode: _focusNode, toCommentId: _toComment == null ? null : _toComment.id, user: _toUser, callback: () { resetInput(); // if (_sortType == _sort[0]) { // //最新评论 // model.isEmpty ? model.refresh() : model.loadMore(); // } else if (model.list.isEmpty) { // model.initData(); // } model.initData(); // model.isEmpty ? model.refresh() : model.loadMore(); }, ) ], )); }), ); } }