diff --git a/assets/live/shutdown.png b/assets/live/shutdown.png new file mode 100644 index 0000000..1d13191 Binary files /dev/null and b/assets/live/shutdown.png differ diff --git a/lib/const/resource.dart b/lib/const/resource.dart index 21182bd..2b026e7 100644 --- a/lib/const/resource.dart +++ b/lib/const/resource.dart @@ -443,6 +443,9 @@ class R { /// ![preview](file:///Users/akufe/Desktop/recook_temp/assets/live/shop.png) static const String ASSETS_LIVE_SHOP_PNG = 'assets/live/shop.png'; + /// ![preview](file:///Users/akufe/Desktop/recook_temp/assets/live/shutdown.png) + static const String ASSETS_LIVE_SHUTDOWN_PNG = 'assets/live/shutdown.png'; + /// ![preview](file:///Users/akufe/Desktop/recook_temp/assets/live/small_red_cart.png) static const String ASSETS_LIVE_SMALL_RED_CART_PNG = 'assets/live/small_red_cart.png'; diff --git a/lib/constants/api.dart b/lib/constants/api.dart index cdc82dc..15f2704 100644 --- a/lib/constants/api.dart +++ b/lib/constants/api.dart @@ -499,4 +499,13 @@ class LiveAPI { ///获取动态中直播的列表 static const String activityVideoList = '/v1/live/user/live/list'; + + ///直播点赞 + static const String liveLike = '/v1/live/live/praise/add'; + + ///开始讲解 + static const String liveStartExplain = '/v1/live/live/explain'; + + ///取消讲解 + static const String liveStopExplain = '/v1/live/live/un_explain'; } diff --git a/lib/pages/live/live_stream/live_page.dart b/lib/pages/live/live_stream/live_page.dart index 0bef9b4..96199bc 100644 --- a/lib/pages/live/live_stream/live_page.dart +++ b/lib/pages/live/live_stream/live_page.dart @@ -1,4 +1,5 @@ import 'dart:io'; +import 'dart:math'; import 'package:flutter/material.dart'; import 'package:recook/constants/api.dart'; @@ -6,12 +7,17 @@ import 'package:recook/constants/header.dart'; import 'package:recook/manager/http_manager.dart'; import 'package:recook/manager/user_manager.dart'; import 'package:recook/pages/live/live_stream/live_pick_goods_page.dart'; +import 'package:recook/pages/live/live_stream/show_goods_list.dart'; +import 'package:recook/pages/live/models/live_stream_info_model.dart'; import 'package:recook/pages/live/models/topic_list_model.dart'; +import 'package:recook/pages/live/tencent_im/tencent_im_tool.dart'; import 'package:recook/pages/live/video/pick_topic_page.dart'; import 'package:recook/pages/live/widget/live_user_bar.dart'; import 'package:recook/utils/custom_route.dart'; import 'package:recook/widgets/bottom_sheet/action_sheet.dart'; import 'package:recook/widgets/custom_image_button.dart'; +import 'package:tencent_im_plugin/entity/message_entity.dart'; +import 'package:tencent_im_plugin/tencent_im_plugin.dart'; import 'package:tencent_live_fluttify/tencent_live_fluttify.dart'; import 'package:image_picker/image_picker.dart'; @@ -30,10 +36,16 @@ class _LivePageState extends State { List pickedIds = []; int liveItemId = 0; bool _isStream = false; + LiveStreamInfoModel _streamInfoModel; + TencentGroupTool group; + List chatObjects = []; + ScrollController _scrollController = ScrollController(); + int nowExplain = 0; @override void initState() { super.initState(); + _editingController.text = '${UserManager.instance.user.info.nickname}正在直播'; } @@ -42,6 +54,9 @@ class _LivePageState extends State { _livePusher?.stopPush(); _livePusher?.stopPreview(); _editingController?.dispose(); + TencentImPlugin.quitGroup(groupId: _streamInfoModel.groupId); + TencentImPlugin.removeListener(parseMessage); + TencentImPlugin.logout(); if (_isStream) HttpManager.post(LiveAPI.exitLive, { 'liveItemId': liveItemId, @@ -160,29 +175,45 @@ class _LivePageState extends State { minWidth: rSize(205), height: rSize(40), onPressed: () { - _isStream = true; - setState(() {}); - // GSDialog.of(context).showLoadingDialog(context, '上传图片中'); - // HttpManager.uploadFile( - // url: CommonApi.upload, - // file: _imageFile, - // key: "photo", - // ).then((result) { - // GSDialog.of(context).dismiss(context); - // GSDialog.of(context) - // .showLoadingDialog(context, '准备开始直播中'); - // HttpManager.post(LiveAPI.startLive, { - // 'title': _editingController.text, - // 'cover': result.url, - // 'topic': _topicModel == null ? 0 : _topicModel.id, - // 'goodsIds': pickedIds, - // }).then((resultData) { - // GSDialog.of(context).dismiss(context); - // liveItemId = resultData.data['data']['liveItemId']; - // _livePusher - // .startPush(resultData.data['data']['pushUrl']); - // }); - // }); + GSDialog.of(context).showLoadingDialog(context, '上传图片中'); + HttpManager.uploadFile( + url: CommonApi.upload, + file: _imageFile, + key: "photo", + ).then((result) { + GSDialog.of(context).dismiss(context); + GSDialog.of(context) + .showLoadingDialog(context, '准备开始直播中'); + HttpManager.post(LiveAPI.startLive, { + 'title': _editingController.text, + 'cover': result.url, + 'topic': _topicModel == null ? 0 : _topicModel.id, + 'goodsIds': pickedIds, + }).then((resultData) { + GSDialog.of(context).dismiss(context); + liveItemId = resultData.data['data']['liveItemId']; + _livePusher + .startPush(resultData.data['data']['pushUrl']); + _isStream = true; + setState(() {}); + TencentIMTool.login().then((_) { + DPrint.printLongJson('用户登陆'); + getLiveStreamModel(liveItemId).then((model) { + if (model == null) + Navigator.pop(context); + else { + setState(() { + _streamInfoModel = model; + }); + TencentImPlugin.applyJoinGroup( + groupId: model.groupId, reason: 'enterLive'); + TencentImPlugin.addListener(parseMessage); + group = TencentGroupTool.fromId(model.groupId); + } + }); + }); + }); + }); }, child: Text( '开始直播', @@ -199,10 +230,72 @@ class _LivePageState extends State { ], ), ), + AnimatedPositioned( + bottom: _isStream ? 0 : -300, + left: 0, + right: 0, + child: Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Expanded( + child: Container( + height: 300, + child: ListView.builder( + reverse: true, + controller: _scrollController, + physics: NeverScrollableScrollPhysics(), + itemBuilder: (context, index) { + return _buildChatBox( + chatObjects[index].sender, chatObjects[index].note); + }, + itemCount: chatObjects.length, + ), + ), + ), + CustomImageButton( + child: Container( + alignment: Alignment.bottomCenter, + width: rSize(44), + height: rSize(44), + child: Text( + _streamInfoModel == null + ? '' + : _streamInfoModel.goodsLists.length.toString(), + style: TextStyle( + color: Colors.white, + fontSize: rSP(13), + height: 28 / 13, + ), + ), + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage(R.ASSETS_LIVE_LIVE_GOOD_PNG), + ), + ), + ), + onPressed: () { + showGoodsListDialog( + context, + models: _streamInfoModel.goodsLists, + onExplain: (index) { + setState(() { + nowExplain = index; + }); + }, + initExplain: nowExplain, + id: _streamInfoModel.id, + isLive: true, + ); + }, + ), + ], + ), + duration: Duration(milliseconds: 250), + ), AnimatedPositioned( duration: Duration(milliseconds: 250), right: _isStream ? -100 : 0, - top: MediaQuery.of(context).viewPadding.top, + top: MediaQuery.of(context).viewPadding.top + rSize(15), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, @@ -222,9 +315,11 @@ class _LivePageState extends State { ), AnimatedPositioned( duration: Duration(milliseconds: 250), - right: 0, - left: 0, - top: _isStream ? -100 : MediaQuery.of(context).viewPadding.top, + right: rSize(15), + left: rSize(15), + top: _isStream + ? (MediaQuery.of(context).viewPadding.top + rSize(15)) + : -100, child: Row( children: [ LiveUserBar( @@ -234,6 +329,17 @@ class _LivePageState extends State { avatar: Api.getImgUrl(UserManager.instance.user.info.headImgUrl), ), + Spacer(), + CustomImageButton( + onPressed: () { + Navigator.pop(context); + }, + child: Image.asset( + R.ASSETS_LIVE_SHUTDOWN_PNG, + height: rSize(21), + width: rSize(21), + ), + ), ], ), ), @@ -242,6 +348,107 @@ class _LivePageState extends State { ); } + Future getLiveStreamModel(int id) async { + ResultData resultData = await HttpManager.post( + LiveAPI.liveStreamInfo, + {'id': id}, + ); + if (resultData?.data['data'] == null) + return null; + else + return LiveStreamInfoModel.fromJson(resultData.data['data']); + } + + _buildChatBox(String sender, String note) { + final Color color = Color.fromRGBO( + 180 + Random().nextInt(55), + 180 + Random().nextInt(55), + 180 + Random().nextInt(55), + 1, + ); + return Align( + alignment: Alignment.centerLeft, + child: Container( + child: Text.rich( + TextSpan(children: [ + TextSpan( + text: '$sender:', + style: TextStyle( + color: color, + fontSize: rSP(13), + ), + ), + TextSpan( + text: note, + style: TextStyle( + color: Colors.white, + fontSize: rSP(13), + ), + ), + ]), + maxLines: 5, + ), + margin: EdgeInsets.symmetric(vertical: rSize(5 / 2)), + padding: EdgeInsets.symmetric( + horizontal: rSize(10), + vertical: rSize(4), + ), + constraints: BoxConstraints( + maxWidth: rSize(200), + ), + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.1), + borderRadius: BorderRadius.circular(rSize(16)), + ), + ), + ); + } + + parseMessage(ListenerTypeEnum type, params) { + print('ListenerTypeEnum$type'); + switch (type) { + case ListenerTypeEnum.ForceOffline: + break; + case ListenerTypeEnum.UserSigExpired: + break; + case ListenerTypeEnum.Connected: + break; + case ListenerTypeEnum.Disconnected: + break; + case ListenerTypeEnum.WifiNeedAuth: + break; + case ListenerTypeEnum.Refresh: + break; + case ListenerTypeEnum.RefreshConversation: + break; + case ListenerTypeEnum.MessageRevoked: + break; + case ListenerTypeEnum.NewMessages: + chatObjects.insertAll(0, params); + _scrollController.animateTo( + -50, + duration: Duration(milliseconds: 300), + curve: Curves.easeInOutCubic, + ); + setState(() {}); + break; + case ListenerTypeEnum.GroupTips: + break; + case ListenerTypeEnum.RecvReceipt: + break; + case ListenerTypeEnum.ReConnFailed: + break; + case ListenerTypeEnum.ConnFailed: + break; + case ListenerTypeEnum.Connecting: + break; + case ListenerTypeEnum.UploadProgress: + break; + case ListenerTypeEnum.DownloadProgress: + break; + } + } + _buildVerticalButton(String img, String title, VoidCallback onTap) { return CustomImageButton( padding: EdgeInsets.symmetric( diff --git a/lib/pages/live/live_stream/live_playback_view_page.dart b/lib/pages/live/live_stream/live_playback_view_page.dart index b37949f..d80173a 100644 --- a/lib/pages/live/live_stream/live_playback_view_page.dart +++ b/lib/pages/live/live_stream/live_playback_view_page.dart @@ -191,6 +191,12 @@ class _LivePlaybackViewPageState extends State { width: rSize(32), height: rSize(32), ), + onTap: () { + HttpManager.post( + LiveAPI.liveLike, + {'liveItemId': widget.id}, + ); + }, ), SizedBox(width: rSize(10)), Spacer(), diff --git a/lib/pages/live/live_stream/live_stream_view_page.dart b/lib/pages/live/live_stream/live_stream_view_page.dart index d4e1206..2348654 100644 --- a/lib/pages/live/live_stream/live_stream_view_page.dart +++ b/lib/pages/live/live_stream/live_stream_view_page.dart @@ -336,6 +336,12 @@ class _LiveStreamViewPageState extends State { width: rSize(32), height: rSize(32), ), + onTap: () { + HttpManager.post( + LiveAPI.liveLike, + {'liveItemId': widget.id}, + ); + }, ), SizedBox(width: rSize(10)), CustomImageButton( diff --git a/lib/pages/live/live_stream/show_goods_list.dart b/lib/pages/live/live_stream/show_goods_list.dart index 5943118..ef11fed 100644 --- a/lib/pages/live/live_stream/show_goods_list.dart +++ b/lib/pages/live/live_stream/show_goods_list.dart @@ -1,17 +1,24 @@ import 'package:flutter/material.dart'; import 'package:recook/constants/api.dart'; import 'package:recook/constants/header.dart'; +import 'package:recook/manager/http_manager.dart'; import 'package:recook/pages/goods/small_coupon_widget.dart'; import 'package:recook/pages/live/models/live_stream_info_model.dart' show GoodsLists; class GoodsListDialog extends StatefulWidget { final List models; - final bool onLive; + final bool isLive; + final Function(int explain) onExplain; + final int initExplain; + final int id; GoodsListDialog({ Key key, @required this.models, - this.onLive, + this.isLive = false, + this.onExplain, + this.initExplain, + this.id, }) : super(key: key); @override @@ -19,6 +26,13 @@ class GoodsListDialog extends StatefulWidget { } class _GoodsListDialogState extends State { + int explain = 0; + @override + void initState() { + super.initState(); + explain = widget.initExplain; + } + @override Widget build(BuildContext context) { return DraggableScrollableSheet( @@ -229,17 +243,72 @@ class _GoodsListDialogState extends State { ), ), Spacer(), - MaterialButton( - color: Color(0xFFDB2D2D), - height: rSize(22), - minWidth: rSize(58), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(rSize(11)), - ), - padding: EdgeInsets.zero, - onPressed: () {}, - child: Text('马上抢'), - ), + widget.isLive + ? explain == model.id + ? SizedBox( + height: rSize(22), + width: rSize(58), + child: OutlineButton( + onPressed: () { + explain = 0; + widget.onExplain(0); + setState(() {}); + HttpManager.post(LiveAPI.liveStopExplain, { + 'liveItemId': widget.id, + 'goodsId': model.id, + }); + }, + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular(rSize(11)), + ), + padding: EdgeInsets.zero, + child: Text( + '取消讲解', + softWrap: false, + overflow: TextOverflow.visible, + style: TextStyle( + color: Color(0xFFDB2D2D), + fontSize: rSP(12), + ), + ), + borderSide: BorderSide( + color: Color(0xFFDB2D2D), + width: rSize(1), + ), + ), + ) + : MaterialButton( + color: Color(0xFFDB2D2D), + height: rSize(22), + minWidth: rSize(58), + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular(rSize(11)), + ), + padding: EdgeInsets.zero, + onPressed: () { + explain = model.id; + widget.onExplain(model.id); + setState(() {}); + HttpManager.post(LiveAPI.liveStartExplain, { + 'liveItemId': widget.id, + 'goodsId': model.id, + }); + }, + child: Text('讲解'), + ) + : MaterialButton( + color: Color(0xFFDB2D2D), + height: rSize(22), + minWidth: rSize(58), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(rSize(11)), + ), + padding: EdgeInsets.zero, + onPressed: () {}, + child: Text('马上抢'), + ), ], ), ], @@ -254,13 +323,23 @@ class _GoodsListDialogState extends State { showGoodsListDialog( BuildContext context, { @required List models, + bool isLive = false, + Function(int onExplain) onExplain, + int initExplain, + int id, }) { showGeneralDialog( context: context, pageBuilder: (context, animation, secondaryAnimation) { return Align( alignment: Alignment.bottomCenter, - child: GoodsListDialog(models: models), + child: GoodsListDialog( + models: models, + isLive: isLive, + onExplain: onExplain, + initExplain: initExplain, + id: id, + ), ); }, transitionBuilder: (context, animation, secondaryAnimation, child) { diff --git a/lib/pages/live/widget/user_live_playback_card.dart b/lib/pages/live/widget/user_live_playback_card.dart index 8dea055..777cf5e 100644 --- a/lib/pages/live/widget/user_live_playback_card.dart +++ b/lib/pages/live/widget/user_live_playback_card.dart @@ -1,3 +1,4 @@ +import 'package:common_utils/common_utils.dart'; import 'package:flutter/material.dart'; import 'package:recook/constants/api.dart'; import 'package:recook/constants/header.dart'; @@ -24,7 +25,7 @@ class _UserPlaybackCardState extends State { RecookDateUtil.fromMillsecond(widget.model.startAt * 1000); return UserBaseCard( date: dateUtil.prefixDay, - detailDate: '14:30', + detailDate: DateUtil.formatDate(dateUtil.dateTime, format: 'HH:mm'), children: [ SizedBox(height: rSize(35)), Row( diff --git a/pubspec.lock b/pubspec.lock index c74c202..6355328 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -638,7 +638,7 @@ packages: name: many_like url: "https://pub.flutter-io.cn" source: hosted - version: "0.0.2" + version: "0.0.3" matcher: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index b5c91d2..75a745d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -192,7 +192,7 @@ dependencies: video_trimmer: ^0.2.7 #点赞组件 - many_like: ^0.0.2 + many_like: ^0.0.3 dev_dependencies: flutter_test: