diff --git a/images/palRoom/user.png b/images/palRoom/user.png new file mode 100644 index 0000000..0e72d20 Binary files /dev/null and b/images/palRoom/user.png differ diff --git a/lib/api/part.dart b/lib/api/part.dart new file mode 100644 index 0000000..ec2ee37 --- /dev/null +++ b/lib/api/part.dart @@ -0,0 +1,6 @@ +import '../../utils/http_util.dart'; + +// 获取房间列表 +getList(data) { + return MyHttpUtil().get("/chat/api/room/page", data: data); +} diff --git a/lib/utils/http_util.dart b/lib/utils/http_util.dart index 9a7372f..ecac234 100644 --- a/lib/utils/http_util.dart +++ b/lib/utils/http_util.dart @@ -14,8 +14,10 @@ class MyHttpUtil { // "Authorization": getToken() ? 'Bearer $getToken()' : null }; options = dioplugn.BaseOptions( - connectTimeout: 15000, - receiveTimeout: 3000, + connectTimeout: 15000, // 连接超时 + receiveTimeout: 15000, + // baseUrl: 'http://192.118.2.93:9093', + // baseUrl: 'http://HousedeMacBook-Air.local:9093', baseUrl: 'http://101.35.117.69:9093', headers: header ?? defaultHeader, ); diff --git a/lib/utils/settings.dart b/lib/utils/settings.dart index be2f8e9..d4460c6 100644 --- a/lib/utils/settings.dart +++ b/lib/utils/settings.dart @@ -1,3 +1,3 @@ const APP_ID = '809529cf18814549a0249802512a7508'; const Token = - '006809529cf18814549a0249802512a7508IAAPcoSQKL/h/B6xy0PPG/XdoZUWas07EdCBtxLe7HH4Hz4vjswAAAAAEABsSV6NzT/fYgEAAQDKP99i'; + '006809529cf18814549a0249802512a7508IAAV27Y5MjNYz2233R1af76+12kaNyjb5VI8eBXNHY9fED4vjswAAAAAEACOhaHH2fLpYgEAAQDW8uli'; diff --git a/lib/utils/webSocket_util.dart b/lib/utils/webSocket_util.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/views/createRoom/CreateRoom.dart b/lib/views/createRoom/CreateRoom.dart index 8e89603..f1542a9 100644 --- a/lib/views/createRoom/CreateRoom.dart +++ b/lib/views/createRoom/CreateRoom.dart @@ -1,11 +1,13 @@ +import 'dart:async'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:agora_rtc_engine/rtc_engine.dart'; import 'package:permission_handler/permission_handler.dart'; + import 'package:will_play/utils/auth.dart'; import 'package:will_play/views/palRoom/PalRoom.dart'; - import '../../utils/http_util.dart'; +import '../../utils/settings.dart'; class CreateRoomPage extends StatefulWidget { CreateRoomPage({Key? key}) : super(key: key); @@ -17,6 +19,8 @@ class CreateRoomPage extends StatefulWidget { class _CreateRoomPageState extends State { final _formKey = GlobalKey(); final _roomName = TextEditingController(); + + late RtcEngine _engine; // 声网实例变量 bool flag = false; List patternList = [ @@ -57,59 +61,61 @@ class _CreateRoomPageState extends State { ), ]; - var Timer; + late Timer timer; // 轮询定时器 + late String roomName = _roomName.text; // 用户输入的房间名 + late String roomId; // 创建接口根据用户输入的房间名,生成的房间唯一ID - void _create(roomName) { - MyHttpUtil().post( - "/chat/api/room/create", - data: {"name": roomName}, - ).then((res) { - var roomId = res.data; + void _create() { + MyHttpUtil() + .post("/chat/api/room/create", data: {"name": roomName}).then((res) { + roomId = res.data; // print(roomId); - - Timer = Timer.periodic(Duration(seconds: 1), (timer) async { - _lodingCreate(roomId); - }); - - // Timer = Timer.periodic(Duration(seconds: 1), (timer) async { - // var resp = await MyHttpUtil().get( - // "/chat/api/room/create/$roomId", - // ); - // String userName = await Storage.get('userName'); - // print(userName); - // print(resp.data); - // if (resp.data == true) { - // // onJoin(userName, roomName, ClientRole.Broadcaster); // 参数1:用户名; 参数2:频道名; 参数3:身份; - // Timer?.cancel(); - // Timer = null; - // } - // }); + _agoraToken(roomId); }); } - void _lodingCreate(roomId) async { - var resp = await MyHttpUtil().get( - "/chat/api/room/create/$roomId", - ); - String userName = await Storage.get('userName'); - print(userName); - print(resp.data); - if (resp.data == true) { - // onJoin(userName, roomName, ClientRole.Broadcaster); // 参数1:用户名; 参数2:频道名; 参数3:身份; - Timer?.cancel(); - Timer = null; + void _agoraToken(roomId) async { + var res = await MyHttpUtil().get("/chat/api/auth/agora/token", + data: {'channelName': roomId, 'role': 1}); + + var agoraToken = res.data["token"]; + String uid = await Storage.get('id'); + String userName = await Storage.get('userName'); // 用户名 + + _onJoin(agoraToken, userName, roomId, ClientRole.Broadcaster); + + // Future.delayed(Duration(seconds: 3), () { + // _onJoin(agoraToken, userName, roomId, ClientRole.Broadcaster); + // }); + + // if (agoraToken != null && agoraToken.isNotEmpty) { + // _engine = await RtcEngine.create(APP_ID); // 初始化引擎 + // _engine.joinChannel(agoraToken, roomId, null, int.parse(uid)); // 加入频道 + + // timer = Timer.periodic(Duration(seconds: 1), (timer) async { + // _lodingCreate(agoraToken); + // }); + // } + } + + void _lodingCreate(agoraToken) async { + var res = await MyHttpUtil().get("/chat/api/room/create/$roomId"); + String userName = await Storage.get('userName'); // 用户名 + print('_lodingCreate:${res.data}'); + if (res.data == true) { + _onJoin(agoraToken, userName, roomId, ClientRole.Broadcaster); + timer.cancel(); } } @override void dispose() { super.dispose(); - Timer?.cancel(); - Timer = null; + timer.cancel(); } - // 页面跳转方法:参数1:用户名; 参数2:频道名; 参数3:身份; - Future onJoin(userName, channelName, role) async { + // 页面跳转方法:参数1:声网token;参数2:用户名; 参数3:频道名; 参数4:身份; + Future _onJoin(agoraToken, userName, channelName, role) async { await _handleCameraAndMic(Permission.camera); await _handleCameraAndMic(Permission.microphone); @@ -117,6 +123,7 @@ class _CreateRoomPageState extends State { context, MaterialPageRoute( builder: (context) => PalRoomPage( + agoraToken: agoraToken, userName: userName, channelName: channelName, role: role, @@ -374,8 +381,8 @@ class _CreateRoomPageState extends State { ), onPressed: () { if (_formKey.currentState!.validate()) { - _create(_roomName.text); print('表单验证通过!'); + _create(); } }, ), diff --git a/lib/views/information/Information.dart b/lib/views/information/Information.dart index 310aaf3..008c44d 100644 --- a/lib/views/information/Information.dart +++ b/lib/views/information/Information.dart @@ -1,17 +1,95 @@ +// import 'package:flutter/material.dart'; + +// class InformationPage extends StatefulWidget { +// InformationPage({Key? key}) : super(key: key); + +// @override +// State createState() => _InformationPageState(); +// } + +// class _InformationPageState extends State { +// @override +// Widget build(BuildContext context) { +// return Container( +// child: Text('aa'), +// ); +// } +// } + import 'package:flutter/material.dart'; +import 'package:web_socket_channel/io.dart'; class InformationPage extends StatefulWidget { InformationPage({Key? key}) : super(key: key); @override - State createState() => _InformationPageState(); + _InformationPageState createState() => _InformationPageState(); } class _InformationPageState extends State { + TextEditingController _controller = TextEditingController(); + late IOWebSocketChannel channel; + String _text = ""; + + @override + void initState() { + // 创建websocket连接 + channel = IOWebSocketChannel.connect( + 'ws://101.35.117.69:9093/chat/api/chat/room/${1}'); + } + @override Widget build(BuildContext context) { - return Container( - child: Text("消息"), + return Scaffold( + appBar: AppBar( + title: Text("发现页面 WebSocket"), + ), + body: Padding( + padding: const EdgeInsets.all(20.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + child: TextField( + controller: _controller, + decoration: InputDecoration(labelText: '发送消息'), + ), + ), + StreamBuilder( + stream: channel.stream, + builder: (context, snapshot) { + // 网络不通会走到这 + if (snapshot.hasError) { + _text = "网络不通..."; + } else if (snapshot.hasData) { + _text = "echo: ${snapshot.data}"; + } + return Padding( + padding: const EdgeInsets.symmetric(vertical: 24.0), + child: Text(_text), + ); + }, + ) + ], + ), + ), + floatingActionButton: FloatingActionButton( + onPressed: _sendMessage, + tooltip: 'Send message', + child: Icon(Icons.send), + ), ); } + + void _sendMessage() { + if (_controller.text.isNotEmpty) { + channel.sink.add(_controller.text); + } + } + + @override + void dispose() { + channel.sink.close(); + super.dispose(); + } } diff --git a/lib/views/login/Login.dart b/lib/views/login/Login.dart index ff27c1d..7c44f75 100644 --- a/lib/views/login/Login.dart +++ b/lib/views/login/Login.dart @@ -3,7 +3,6 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:will_play/utils/auth.dart'; -// import '../../utils/dioHttp.dart'; import '../../utils/http_util.dart'; import '../../utils/auth.dart'; import '../../utils/showToast.dart'; @@ -36,7 +35,7 @@ class _LoginPageState extends State { var setToken = await Storage.set('token', '${res.data["token"]}'); var setId = await Storage.set('id', '${res.data["id"]}'); - var setUserName = await Storage.set('userName', '${res.data["userName"]}'); + var setUserName = await Storage.set('userName', '${res.data["username"]}'); Navigator.of(context).pushReplacementNamed('/'); } @@ -44,6 +43,7 @@ class _LoginPageState extends State { @override Widget build(BuildContext context) { return Scaffold( + resizeToAvoidBottomInset: false, backgroundColor: Colors.white, appBar: AppBar( elevation: 0, // z轴阴影 @@ -61,7 +61,7 @@ class _LoginPageState extends State { fontWeight: FontWeight.bold), ), ), - body: Padding( + body: SingleChildScrollView( padding: EdgeInsets.symmetric(horizontal: 30), child: Form( key: _formKey, diff --git a/lib/views/palRoom/PalRoom.dart b/lib/views/palRoom/PalRoom.dart index 9b240b5..7b60ca1 100644 --- a/lib/views/palRoom/PalRoom.dart +++ b/lib/views/palRoom/PalRoom.dart @@ -1,77 +1,106 @@ +import 'dart:async'; +import 'dart:convert'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; -import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:agora_rtc_engine/rtc_engine.dart'; import 'package:permission_handler/permission_handler.dart'; -import '../../utils/settings.dart'; +import 'package:will_play/utils/auth.dart'; +import 'package:web_socket_channel/io.dart'; -// 实现语音主要五个步骤逻辑: -// 初始化引擎 -// 开启音频权限,启用音频模块 -// 创建房间 -// 设置事件监听(成功加入房间,是否有用户加入,用户是否离开,用户是否掉线) -// 布局实现 -// 退出语音(根据需要销毁引擎,释放资源) +import '../../utils/settings.dart'; +import '../../utils/http_util.dart'; +import '../../utils/showToast.dart'; + +// 一:实现 RtcEngine 语音主要五个步骤逻辑: +// 1.初始化引擎 +// 2.开启音频权限,启用音频模块 +// 3.创建房间 +// 4.设置事件监听(成功加入房间,是否有用户加入,用户是否离开,用户是否掉线) +// 5.布局实现 +// 6.退出语音(根据需要销毁引擎,释放资源) + +// 二:使用 WebSocket 通信分为四个步骤: +// 1.连接到WebSocket服务器 +// 2.监听来自服务器的消息 +// 3.将数据发送到服务器 +// 4.关闭WebSocket连接 class PalRoomPage extends StatefulWidget { + final String agoraToken; final String userName; final String channelName; final ClientRole? role; - PalRoomPage({Key? key, this.userName = '', this.channelName = '', this.role}) - : super(key: key); + PalRoomPage({ + Key? key, + this.agoraToken = '', + this.userName = '', + this.channelName = '', + this.role, + }) : super(key: key); @override State createState() => _PalRoomPageState(); } class _PalRoomPageState extends State { - final _users = []; // 用户id数组 + // final _users = []; // 用户id数组 final _infoStrings = []; // 状态文字信息列表 bool viewPanel = true; // 是否显示状态文字信息列表 bool muted = false; // 是否开麦 bool volume = true; // 音量 - late RtcEngine _engine; // 声网实例变量 + TextEditingController _controller = TextEditingController(); // 输入框内容 + + late RtcEngine _engine; // RtcEngine 声网实例变量 + late IOWebSocketChannel _wsChannel; // webSocket 实例变量 // 房间座位 - List list = [ - {'name': '', 'content': false, 'image': 'images/palRoom/voice2.png'}, - {'name': '', 'content': false, 'image': 'images/palRoom/voice2.png'}, - {'name': '', 'content': false, 'image': 'images/palRoom/voice2.png'}, - {'name': '', 'content': false, 'image': 'images/palRoom/voice2.png'}, - {'name': '', 'content': false, 'image': 'images/palRoom/voice2.png'}, - {'name': '', 'content': false, 'image': 'images/palRoom/voice2.png'}, - {'name': '', 'content': false, 'image': 'images/palRoom/voice2.png'}, - {'name': '', 'content': false, 'image': 'images/palRoom/voice2.png'}, + List _list = [ + {'nickname': '', 'flag': false, 'image': 'images/palRoom/voice2.png'}, + {'nickname': '', 'flag': false, 'image': 'images/palRoom/voice2.png'}, + {'nickname': '', 'flag': false, 'image': 'images/palRoom/voice2.png'}, + {'nickname': '', 'flag': false, 'image': 'images/palRoom/voice2.png'}, + {'nickname': '', 'flag': false, 'image': 'images/palRoom/voice2.png'}, + {'nickname': '', 'flag': false, 'image': 'images/palRoom/voice2.png'}, + {'nickname': '', 'flag': false, 'image': 'images/palRoom/voice2.png'}, + {'nickname': '', 'flag': false, 'image': 'images/palRoom/voice2.png'}, ]; + // 连麦用户列表数据 + List newUserList = []; + @override void initState() { super.initState(); - _infoStrings.insert(0, { - 'type': 'system', - 'content': '信息-${widget.userName}-${widget.channelName}-${widget.role}' - }); initialize(); - if (widget.role == ClientRole.Broadcaster) { - _infoStrings.insert(0, {'type': 'system', 'content': '您身份为主播,已经上麦状态'}); - } else if (widget.role == ClientRole.Audience) { - _infoStrings.insert(0, {'type': 'system', 'content': '您身份为观众,点击麦克风图标上麦'}); - } + // if (widget.role == ClientRole.Broadcaster) { + // _infoStrings.insert(0, {'type': 'system', 'content': '您身份为主播,已经上麦状态'}); + // } else if (widget.role == ClientRole.Audience) { + // _infoStrings.insert(0, {'type': 'system', 'content': '您身份为观众,点击麦克风图标上麦'}); + // } + + wsInitalize(); } @override void dispose() { super.dispose(); - _users.clear(); + // _users.clear(); _engine.leaveChannel(); _engine.destroy(); + _wsChannel.sink.close(); } Future initialize() async { + _infoStrings.insert(0, { + 'type': 'system', + 'content': + '|令牌:${widget.agoraToken}|用户名:${widget.userName}|房间:${widget.channelName}|身份:${widget.role}' + }); + // 判断appid是否存在 if (APP_ID.isEmpty) { setState(() { @@ -92,12 +121,79 @@ class _PalRoomPageState extends State { AudioScenario.ChatRoomEntertainment); // 设置音频编码属性和音频场景 await _engine.setChannelProfile(ChannelProfile.LiveBroadcasting); + _addAgoraEventHandlers(); // 调用状态文字信息列表方法 + if (widget.role == ClientRole.Broadcaster) { await _engine.setClientRole(ClientRole.Broadcaster); } - _engine.joinChannel(Token, widget.channelName, null, 0); // 加入频道 - _addAgoraEventHandlers(); // 调用状态文字信息列表方法 + String uid = await Storage.get('id'); + + await _engine.joinChannel( + widget.agoraToken, widget.channelName, null, int.parse(uid)); // 加入频道 + + _detail(); + } + + wsInitalize() async { + String uid = await Storage.get('id'); + _wsChannel = IOWebSocketChannel.connect( + 'ws://101.35.117.69:9093/chat/api/chat/room/${int.parse(uid)}'); // 建立链接 + + print('object'); + _wsChannel.stream.listen((data) { + var aa = jsonDecode(data); + print('收到服务器数据:${aa}'); + setState(() { + _infoStrings.insert(0, { + 'type': 'speak', + 'content': '${aa['message']}', + 'name': '${aa['username']}' + }); + }); + }, onDone: () { + print('连接关闭时响应'); + }, onError: (error) { + print('发生错误'); + }, cancelOnError: true); // 监听来自服务器的消息 + } + + // 获取房间详情接口 + void _detail() async { + var res = + await MyHttpUtil().get("/chat/api/room/detail/${widget.channelName}"); + // print('${res}'); + + var _userList = res.data['userList']; + + // 赋值image字段头像 + _userList.forEach((item) { + item['image'] = 'images/palRoom/user.png'; + item['flag'] = true; + }); + + // 筛选出身份类型为主播的用户 + newUserList = + _userList.where((item) => item['type'] != 'audience').toList(); + print('-----------------筛选出身份类型为主播的用户:${newUserList}-----------------'); + + for (var i = 0; i < _list.length; i++) { + if (newUserList.length < (i + 1)) { + setState(() { + _list[i] = { + 'nickname': '', + 'flag': false, + 'image': 'images/palRoom/voice2.png' + }; + }); + } else { + setState(() { + _list[i] = newUserList[i]; + }); + } + } + + print('-----------------${_list}-----------------'); } void _addAgoraEventHandlers() { @@ -109,57 +205,80 @@ class _PalRoomPageState extends State { _infoStrings.insert(0, {'type': 'system', 'content': info}); }); }, + userJoined: (uid, elapsed) { + _detail(); + // 用户加入 + setState(() { + final info = '用户加入: uid $uid'; + _infoStrings.insert(0, {'type': 'system', 'content': info}); + // _users.add(uid); + }); + }, joinChannelSuccess: (channel, uid, elapsed) { + _detail(); // 加入频道成功 setState(() { - final info = '加入频道: $channel, uid: $uid'; + final info = '加入频道成功: $channel, uid: $uid'; _infoStrings.insert(0, {'type': 'system', 'content': info}); }); }, leaveChannel: (stats) { + _detail(); // 离开频道 setState(() { _infoStrings.insert(0, {'type': 'system', 'content': '离开频道'}); - _users.clear(); - }); - }, - userJoined: (uid, elapsed) { - // 用户加入 - setState(() { - final info = '用户加入: $uid'; - _infoStrings.insert(0, {'type': 'system', 'content': info}); - _users.add(uid); + // _users.clear(); }); }, userOffline: (uid, reason) { - // 用户离线 + _detail(); + // 用户离开当前频道 setState(() { - final info = '用户离线: $uid , reason: $reason'; - _infoStrings.insert(0, {'type': 'system', 'content': info}); - _users.remove(uid); - }); - }, - firstRemoteVideoFrame: (uid, width, height, elapsed) { - // 远程视频第一帧 - setState(() { - final info = 'First Remote Video Frame: $uid'; + final info = '用户离开当前频道: $uid , reason: $reason'; _infoStrings.insert(0, {'type': 'system', 'content': info}); + // _users.remove(uid); }); }, clientRoleChanged: (oldRole, newRole) { + _detail(); // 直播场景下用户角色切换成功回调 setState(() { final info = '用户旧身份$oldRole,用户新身份$newRole'; _infoStrings.insert(0, {'type': 'system', 'content': info}); }); }, + rejoinChannelSuccess: (channel, uid, elapsed) { + _detail(); + // 成功重新加入频道回调 + setState(() { + final info = '用户成功重新加入: $uid'; + _infoStrings.insert(0, {'type': 'system', 'content': info}); + // _users.add(uid); + }); + }, + warning: (warn) { + // 报告 RtcChannel 的警告码 + print(warn); + }, + tokenPrivilegeWillExpire: (token) { + // Token 服务将在30s内过期回调 + // app 应重新获取 Token,然后调用 _engine.renewToken(token) 将新的 Token 传给 SDK + print('即将服务失效的 Token:${token}'); + }, + requestToken: () { + // Token 已过期回调 + setState(() { + final info = 'Token 已过期'; + _infoStrings.insert(0, {'type': 'system', 'content': info}); + }); + }, )); } // 头部 Widget _head() { // 底部弹框组件 - _modelBottomSheet(index) async { + _modelBottomSheet(index) { showModalBottomSheet( context: context, builder: (context) { @@ -167,33 +286,34 @@ class _PalRoomPageState extends State { height: 150.0, child: Column( children: [ - list[index]['content'] == false + _list[index]['flag'] == false ? ListTile( title: Text( "连线上麦", textAlign: TextAlign.center, ), onTap: () { - setState(() { - for (var i = 0; i < list.length; i++) { - if (i == index) { - list[index]['content'] = true; - list[index]['name'] = '_users'; - list[index]['image'] = - 'images/palRoom/photo.png'; - } else { - list[i]['content'] = false; - list[i]['name'] = ''; - list[i]['image'] = 'images/palRoom/voice2.png'; - } - } - }); + // setState(() { + // for (var i = 0; i < _list.length; i++) { + // if (i == index) { + // _list[index]['flag'] = true; + // _list[index]['nickname'] = '_users'; + // _list[index]['image'] = + // 'images/palRoom/user.png'; + // } else { + // _list[i]['flag'] = false; + // _list[i]['name'] = ''; + // _list[i]['image'] = 'images/palRoom/voice2.png'; + // } + // } + // }); // 申请连麦需要改变身份为主播! _engine.setClientRole(ClientRole.Broadcaster); - + setState(() { + _list[index]['flag'] = true; + }); Navigator.pop(context); - print("连线上麦:${list[index]['content']}"); }, ) : ListTile( @@ -203,16 +323,13 @@ class _PalRoomPageState extends State { ), onTap: () { setState(() { - list[index]['content'] = false; - list[index]['name'] = ''; - list[index]['image'] = 'images/palRoom/voice2.png'; + _list[index]['flag'] = false; + _list[index]['nickname'] = ''; + _list[index]['image'] = 'images/palRoom/voice2.png'; }); - // 下麦再将身份改回为观众(下麦不代表离开频道) _engine.setClientRole(ClientRole.Audience); - Navigator.pop(context); - print("下麦:${list[index]['content']}"); }, ), ListTile( @@ -233,11 +350,12 @@ class _PalRoomPageState extends State { return Container( child: ListView( children: [ + // --房主 start -- Container( padding: EdgeInsets.fromLTRB(0, 27, 0, 5), alignment: Alignment.topCenter, child: Image.asset( - 'images/palRoom/photo.png', + 'images/palRoom/user.png', width: 70, height: 70, ), @@ -269,9 +387,12 @@ class _PalRoomPageState extends State { ], ), ), + // --房主 end -- + + // -- 网格 start -- Container( + // padding: EdgeInsets.only(left: 15, right: 15), child: GridView.builder( - padding: EdgeInsets.all(15), shrinkWrap: true, gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisSpacing: 40, @@ -279,7 +400,7 @@ class _PalRoomPageState extends State { crossAxisCount: 4, childAspectRatio: 1 / 1.25, ), - itemCount: list.length, + itemCount: _list.length, itemBuilder: (BuildContext context, int index) { return GestureDetector( onTap: () { @@ -304,25 +425,27 @@ class _PalRoomPageState extends State { borderRadius: BorderRadius.circular(50), ), child: Center( - child: Image.asset( - list[index]['image'], - width: list[index]['content'] ? 55 : 14, - height: list[index]['content'] ? 55 : 21, + child: ClipOval( + child: Image.asset( + _list[index]['image'], + width: _list[index]['flag'] ? 55 : 14, + height: _list[index]['flag'] ? 55 : 21, + ), ), ), ), Text( - list[index]['name'], + _list[index]['nickname'], textAlign: TextAlign.center, style: TextStyle(color: Colors.white, fontSize: 12), ), ], ), ); - ; }, ), ), + // -- 网格 end -- ], ), ); @@ -330,7 +453,7 @@ class _PalRoomPageState extends State { // 消息列表 Widget _planel() { - _filtering(type, content) { + _filtering(type, content, {name}) { switch (type) { case 'system': return Container( @@ -342,7 +465,7 @@ class _PalRoomPageState extends State { case 'speak': return Container( child: Text( - '聊天消息:$content', + '${name}的聊天消息:$content', style: TextStyle(color: Color.fromRGBO(155, 154, 170, 1)), ), ); @@ -364,9 +487,9 @@ class _PalRoomPageState extends State { child: FractionallySizedBox( heightFactor: 0.5, child: Container( - decoration: BoxDecoration( - color: Colors.black, - ), + // decoration: BoxDecoration( + // color: Colors.black, + // ), child: ListView.builder( reverse: true, itemCount: _infoStrings.length, @@ -390,7 +513,8 @@ class _PalRoomPageState extends State { borderRadius: BorderRadius.circular(5), ), child: _filtering(_infoStrings[index]['type'], - _infoStrings[index]['content']), + _infoStrings[index]['content'], + name: _infoStrings[index]['name']), ), ), ], @@ -406,7 +530,6 @@ class _PalRoomPageState extends State { // 底部输入框控制台 Widget _footer() { - final _input = TextEditingController(); return Container( width: double.infinity, alignment: Alignment.bottomCenter, @@ -441,10 +564,12 @@ class _PalRoomPageState extends State { color: Color.fromRGBO(201, 201, 201, 1), size: 25.0), ), GestureDetector( + // 表情图标 child: Icon(Icons.tag_faces, color: Color.fromRGBO(201, 201, 201, 1), size: 25.0), ), Container( + // 输入框 width: 220, padding: EdgeInsets.symmetric(horizontal: 10), decoration: BoxDecoration( @@ -452,7 +577,30 @@ class _PalRoomPageState extends State { borderRadius: BorderRadius.circular(30), ), child: TextField( - controller: _input, + textInputAction: TextInputAction.send, // 键盘右下角图标 + // onEditingComplete: () async { + // late String speak = _controller.text.trim(); + // if (speak.isEmpty) { + // showToast("请输入聊天内容~"); + // return; + // } + // String userName = await Storage.get('userName'); // 用户名 + + // _wsChannel.sink.add(speak); + + // // _controller.text = ''; + // }, + onSubmitted: (value) async { + late String speak = value.trim(); + if (speak.isEmpty) { + showToast("请输入聊天内容~"); + return; + } + String userName = await Storage.get('userName'); // 用户名 + + _wsChannel.sink.add(speak); + }, + controller: _controller, decoration: InputDecoration( isDense: true, border: InputBorder.none, @@ -466,10 +614,10 @@ class _PalRoomPageState extends State { fontSize: 12, color: Colors.white, ), - textInputAction: TextInputAction.send, // 键盘右下角图标 ), ), GestureDetector( + // 加号图标 child: Icon(Icons.add, color: Color.fromRGBO(201, 201, 201, 1), size: 25.0), ), @@ -483,6 +631,14 @@ class _PalRoomPageState extends State { return Scaffold( resizeToAvoidBottomInset: true, appBar: AppBar( + leading: IconButton( + icon: Icon(Icons.arrow_back_ios), + onPressed: () { + print('离开'); + _engine.setClientRole(ClientRole.Audience); + _engine.leaveChannel(); + Navigator.of(context).pop(); + }), elevation: 0, // z轴阴影 titleSpacing: 0, // 标题与其他控件的间隔 backgroundColor: Color.fromRGBO(27, 28, 48, 1), @@ -499,7 +655,7 @@ class _PalRoomPageState extends State { padding: EdgeInsets.only(right: 5), child: ClipOval( child: Image.asset( - 'images/palRoom/photo.png', + 'images/palRoom/user.png', width: 46, height: 46, ), diff --git a/lib/views/palRoom/palroom.txt b/lib/views/palRoom/palroom.txt new file mode 100644 index 0000000..5a674c3 --- /dev/null +++ b/lib/views/palRoom/palroom.txt @@ -0,0 +1,652 @@ +import 'dart:async'; +import 'dart:convert'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/foundation.dart'; +import 'package:agora_rtc_engine/rtc_engine.dart'; +import 'package:permission_handler/permission_handler.dart'; +import 'package:will_play/utils/auth.dart'; +import '../../utils/settings.dart'; +import '../../utils/http_util.dart'; + +// 实现语音主要五个步骤逻辑: +// 初始化引擎 +// 开启音频权限,启用音频模块 +// 创建房间 +// 设置事件监听(成功加入房间,是否有用户加入,用户是否离开,用户是否掉线) +// 布局实现 +// 退出语音(根据需要销毁引擎,释放资源) + +class PalRoomPage extends StatefulWidget { + final String agoraToken; + final String userName; + final String channelName; + final ClientRole? role; + + PalRoomPage({ + Key? key, + this.agoraToken = '', + this.userName = '', + this.channelName = '', + this.role, + }) : super(key: key); + + @override + State createState() => _PalRoomPageState(); +} + +class _PalRoomPageState extends State { + final _users = []; // 用户id数组 + final _infoStrings = []; // 状态文字信息列表 + bool viewPanel = true; // 是否显示状态文字信息列表 + bool muted = false; // 是否开麦 + bool volume = true; // 音量 + late RtcEngine _engine; // 声网实例变量 + + // 房间座位 + List _list = [ + {'name': '', 'content': false, 'image': 'images/palRoom/voice2.png'}, + {'name': '', 'content': false, 'image': 'images/palRoom/voice2.png'}, + {'name': '', 'content': false, 'image': 'images/palRoom/voice2.png'}, + {'name': '', 'content': false, 'image': 'images/palRoom/voice2.png'}, + {'name': '', 'content': false, 'image': 'images/palRoom/voice2.png'}, + {'name': '', 'content': false, 'image': 'images/palRoom/voice2.png'}, + {'name': '', 'content': false, 'image': 'images/palRoom/voice2.png'}, + {'name': '', 'content': false, 'image': 'images/palRoom/voice2.png'}, + ]; + + // 在线用户数据 + List _userList = []; + + @override + void initState() { + super.initState(); + _infoStrings.insert(0, { + 'type': 'system', + 'content': + '|令牌:${widget.agoraToken}|用户名:${widget.userName}|房间:${widget.channelName}|身份:${widget.role}' + }); + + initialize(); + + if (widget.role == ClientRole.Broadcaster) { + _infoStrings.insert(0, {'type': 'system', 'content': '您身份为主播,已经上麦状态'}); + } else if (widget.role == ClientRole.Audience) { + _infoStrings.insert(0, {'type': 'system', 'content': '您身份为观众,点击麦克风图标上麦'}); + } + } + + @override + void dispose() { + super.dispose(); + _users.clear(); + _engine.leaveChannel(); + _engine.destroy(); + } + + Future initialize() async { + // 判断appid是否存在 + if (APP_ID.isEmpty) { + setState(() { + _infoStrings.insert(0, { + 'type': 'system', + 'content': 'APP ID缺失,请在settings.dart中提供您的APP ID' + }); + _infoStrings.insert(0, {'type': 'system', 'content': 'Agora引擎没有启动'}); + }); + return; + } + + _engine = await RtcEngine.create(APP_ID); // 初始化引擎 + await _engine.enableAudio(); // 启用音频模块 + await _engine.setDefaultAudioRouteToSpeakerphone( + true); // 设置默认的音频路由:该方法设置接收到的音频从听筒或扬声器出声。如果用户不调用本方法,音频默认从听筒出声。 + await _engine.setAudioProfile(AudioProfile.SpeechStandard, + AudioScenario.ChatRoomEntertainment); // 设置音频编码属性和音频场景 + await _engine.setChannelProfile(ChannelProfile.LiveBroadcasting); + + _addAgoraEventHandlers(); // 调用状态文字信息列表方法 + + if (widget.role == ClientRole.Broadcaster) { + await _engine.setClientRole(ClientRole.Broadcaster); + } + + String uid = await Storage.get('id'); + print(widget.agoraToken); + print(widget.channelName); + print(uid); + + await _engine.joinChannel( + widget.agoraToken, widget.channelName, null, int.parse(uid)); // 加入频道 + } + + // 获取房间详情接口 + void _detail() async { + var res = + await MyHttpUtil().get("/chat/api/room/detail/${widget.channelName}"); + print('_detail接口:${res}'); + setState(() { + this._userList = res.data.userList; + }); + } + + void _addAgoraEventHandlers() { + _engine.setEventHandler(RtcEngineEventHandler(error: (code) { + // 错误 + setState(() { + final info = '错误 $code'; + _infoStrings.insert(0, {'type': 'system', 'content': info}); + }); + }, userJoined: (uid, elapsed) { + // 用户加入 + setState(() { + final info = '用户加入: uid $uid'; + _infoStrings.insert(0, {'type': 'system', 'content': info}); + _users.add(uid); + + print('用户加入:'); + _detail(); + }); + }, joinChannelSuccess: (channel, uid, elapsed) { + // 加入频道成功 + setState(() { + final info = '加入频道: $channel, uid: $uid'; + _infoStrings.insert(0, {'type': 'system', 'content': info}); + + print('加入频道成功:'); + _detail(); + }); + }, leaveChannel: (stats) { + // 离开频道 + setState(() { + _infoStrings.insert(0, {'type': 'system', 'content': '离开频道'}); + _users.clear(); + + print('离开频道:'); + _detail(); + }); + }, userOffline: (uid, reason) { + // 用户离线 + setState(() { + final info = '用户离线: $uid , reason: $reason'; + _infoStrings.insert(0, {'type': 'system', 'content': info}); + _users.remove(uid); + + print('用户离线:'); + _detail(); + }); + }, clientRoleChanged: (oldRole, newRole) { + // 直播场景下用户角色切换成功回调 + setState(() { + final info = '用户旧身份$oldRole,用户新身份$newRole'; + _infoStrings.insert(0, {'type': 'system', 'content': info}); + + print('直播场景下用户角色切换成功回调:'); + _detail(); + }); + }, rejoinChannelSuccess: (channel, uid, elapsed) { + // 成功重新加入频道回调 + setState(() { + final info = '用户成功重新加入: $uid'; + _infoStrings.insert(0, {'type': 'system', 'content': info}); + _users.add(uid); + + print('成功重新加入频道回调:'); + _detail(); + }); + }, requestToken: () { + // Token 已过期回调 + setState(() { + final info = 'Token 已过期'; + _infoStrings.insert(0, {'type': 'system', 'content': info}); + }); + })); + } + + // 头部 + Widget _head() { + // 底部弹框组件 + _modelBottomSheet(index) async { + showModalBottomSheet( + context: context, + builder: (context) { + return Container( + height: 150.0, + child: Column( + children: [ + _list[index]['content'] == false + ? ListTile( + title: Text( + "连线上麦", + textAlign: TextAlign.center, + ), + onTap: () { + setState(() { + for (var i = 0; i < _list.length; i++) { + if (i == index) { + _list[index]['content'] = true; + _list[index]['name'] = '_users'; + _list[index]['image'] = + 'images/palRoom/photo.png'; + } else { + _list[i]['content'] = false; + _list[i]['name'] = ''; + _list[i]['image'] = 'images/palRoom/voice2.png'; + } + } + }); + + // 申请连麦需要改变身份为主播! + _engine.setClientRole(ClientRole.Broadcaster); + + Navigator.pop(context); + print("连线上麦:${_list[index]['content']}"); + }, + ) + : ListTile( + title: Text( + "下麦", + textAlign: TextAlign.center, + ), + onTap: () { + setState(() { + _list[index]['content'] = false; + _list[index]['name'] = ''; + _list[index]['image'] = 'images/palRoom/voice2.png'; + }); + + // 下麦再将身份改回为观众(下麦不代表离开频道) + _engine.setClientRole(ClientRole.Audience); + + Navigator.pop(context); + print("下麦:${_list[index]['content']}"); + }, + ), + ListTile( + title: Text( + "取消", + textAlign: TextAlign.center, + ), + onTap: () { + Navigator.pop(context); + }), + ], + ), + ); + }, + ); + } + + return Container( + child: ListView( + children: [ + Container( + padding: EdgeInsets.fromLTRB(0, 27, 0, 5), + alignment: Alignment.topCenter, + child: Image.asset( + 'images/palRoom/photo.png', + width: 70, + height: 70, + ), + ), + Container( + margin: EdgeInsets.only(bottom: 30), + alignment: Alignment.topCenter, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + width: 18, + height: 18, + alignment: Alignment.center, + decoration: BoxDecoration( + color: Color.fromRGBO(245, 173, 29, 1), + borderRadius: BorderRadius.circular(2), + ), + child: Text( + '房', + style: TextStyle( + color: Colors.white, + fontSize: 12, + ), + ), + ), + SizedBox(width: 5), + Text('以冬', style: TextStyle(color: Colors.white, fontSize: 14)), + ], + ), + ), + Container( + // padding: EdgeInsets.only(left: 15, right: 15), + child: GridView.builder( + shrinkWrap: true, + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisSpacing: 40, + mainAxisSpacing: 30, + crossAxisCount: 4, + childAspectRatio: 1 / 1.25, + ), + itemCount: _list.length, + itemBuilder: (BuildContext context, int index) { + return GestureDetector( + onTap: () { + setState(() { + if (widget.role == ClientRole.Audience) { + // 观众身份的时候点击上麦克图标 + _modelBottomSheet(index); // 调用底部弹框方法 + } else { + _infoStrings.insert( + 0, {'type': 'system', 'content': '您身份为主播,已经上麦状态'}); + } + }); + }, + child: Column( + children: [ + Container( + width: 55, + height: 55, + margin: EdgeInsets.only(bottom: 5), + decoration: BoxDecoration( + color: Color.fromRGBO(52, 51, 69, 1), + borderRadius: BorderRadius.circular(50), + ), + child: Center( + child: Image.asset( + _list[index]['image'], + width: _list[index]['content'] ? 55 : 14, + height: _list[index]['content'] ? 55 : 21, + ), + ), + ), + Text( + _list[index]['name'], + textAlign: TextAlign.center, + style: TextStyle(color: Colors.white, fontSize: 12), + ), + ], + ), + ); + }, + ), + ), + ], + ), + ); + } + + // 消息列表 + Widget _planel() { + _filtering(type, content) { + switch (type) { + case 'system': + return Container( + child: Text( + '系统公告:$content', + style: TextStyle(color: Color.fromRGBO(155, 154, 170, 1)), + ), + ); + case 'speak': + return Container( + child: Text( + '聊天消息:$content', + style: TextStyle(color: Color.fromRGBO(155, 154, 170, 1)), + ), + ); + case 'gift': + return Container( + child: Text( + '礼物消息:$content', + style: TextStyle(color: Color.fromRGBO(155, 154, 170, 1)), + ), + ); + } + } + + return Visibility( + visible: viewPanel, + child: Container( + padding: EdgeInsets.symmetric(vertical: 60), + alignment: Alignment.bottomCenter, + child: FractionallySizedBox( + heightFactor: 0.5, + child: Container( + // decoration: BoxDecoration( + // color: Colors.black, + // ), + child: ListView.builder( + reverse: true, + itemCount: _infoStrings.length, + itemBuilder: (BuildContext context, int index) { + if (_infoStrings.isEmpty) { + return Text('null'); + } + return Padding( + padding: EdgeInsets.symmetric(vertical: 5, horizontal: 15), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: Container( + padding: EdgeInsets.symmetric( + vertical: 10, + horizontal: 10, + ), + decoration: BoxDecoration( + color: Color.fromRGBO(50, 51, 71, 1), + borderRadius: BorderRadius.circular(5), + ), + child: _filtering(_infoStrings[index]['type'], + _infoStrings[index]['content']), + ), + ), + ], + ), + ); + }, + ), + ), + ), + ), + ); + } + + // 底部输入框控制台 + Widget _footer() { + final _input = TextEditingController(); + return Container( + width: double.infinity, + alignment: Alignment.bottomCenter, + padding: EdgeInsets.symmetric( + vertical: 18, + horizontal: 15, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + GestureDetector( + // 音量图标 + onTap: () { + setState(() { + volume = !volume; + }); + _engine.adjustPlaybackSignalVolume( + volume ? 100 : 0); // 调节本地播放的所有远端用户信号音量 + }, + child: Icon(volume ? Icons.volume_up : Icons.volume_off, + color: Color.fromRGBO(201, 201, 201, 1), size: 25.0), + ), + GestureDetector( + // 麦克风图标 + onTap: () { + setState(() { + muted = !muted; + }); + _engine.muteLocalAudioStream(muted); // 取消或恢复发布本地音频流(是否静音) + }, + child: Icon(muted ? Icons.mic_off : Icons.mic, + color: Color.fromRGBO(201, 201, 201, 1), size: 25.0), + ), + GestureDetector( + child: Icon(Icons.tag_faces, + color: Color.fromRGBO(201, 201, 201, 1), size: 25.0), + ), + Container( + width: 220, + padding: EdgeInsets.symmetric(horizontal: 10), + decoration: BoxDecoration( + color: Color.fromRGBO(52, 51, 69, 1), + borderRadius: BorderRadius.circular(30), + ), + child: TextField( + controller: _input, + decoration: InputDecoration( + isDense: true, + border: InputBorder.none, + hintText: '我来说几句', + hintStyle: TextStyle( + fontSize: 12, + color: Color.fromRGBO(138, 138, 146, 1), + ), + ), + style: TextStyle( + fontSize: 12, + color: Colors.white, + ), + textInputAction: TextInputAction.send, // 键盘右下角图标 + ), + ), + GestureDetector( + child: Icon(Icons.add, + color: Color.fromRGBO(201, 201, 201, 1), size: 25.0), + ), + ], + ), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + resizeToAvoidBottomInset: true, + appBar: AppBar( + elevation: 0, // z轴阴影 + titleSpacing: 0, // 标题与其他控件的间隔 + backgroundColor: Color.fromRGBO(27, 28, 48, 1), + title: Container( + height: 60, + decoration: BoxDecoration( + color: Color.fromRGBO(27, 28, 48, 1), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + SizedBox( + child: Padding( + padding: EdgeInsets.only(right: 5), + child: ClipOval( + child: Image.asset( + 'images/palRoom/photo.png', + width: 46, + height: 46, + ), + ), + ), + ), + Expanded( + flex: 1, + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Row( + children: [ + Text( + '以冬', + style: TextStyle(color: Colors.white, fontSize: 12), + ), + SizedBox(width: 5), + Text( + 'ID309060', + style: TextStyle( + color: Color.fromRGBO(242, 174, 30, 1), + fontSize: 10, + ), + ), + ], + ), + Row( + children: [ + Text.rich( + TextSpan( + style: TextStyle( + fontSize: 10, + color: Colors.white, + ), + children: [ + TextSpan(text: '交友'), + TextSpan(text: ' | '), + TextSpan(text: '房间名:成人避风港'), + ]), + ), + SizedBox(width: 5), + Container( + padding: EdgeInsets.fromLTRB(6, 2, 6, 2), + decoration: BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(2)), + color: Color.fromRGBO(47, 47, 59, 1), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + '在线:121', + style: TextStyle( + fontSize: 10, + color: Colors.white, + ), + ), + SizedBox(width: 5), + Image.asset( + 'images/palRoom/whiteRight.png', + width: 5, + height: 8, + ), + ], + ), + ) + ], + ), + ], + ), + ) + ], + ), + ), + actions: [ + IconButton( + onPressed: () { + print('more图标被点击'); + setState(() { + viewPanel = !viewPanel; + }); + }, + icon: Image.asset( + 'images/palRoom/more.png', + fit: BoxFit.cover, + width: 20.0, + height: 5.0, + ), + ), + ], + ), + backgroundColor: Color.fromRGBO(27, 28, 48, 1), + body: Stack( + children: [ + _head(), + _planel(), + _footer(), + ], + ), + ); + } +} diff --git a/lib/views/palRoom/palroom2.txt b/lib/views/palRoom/palroom2.txt new file mode 100644 index 0000000..540a6de --- /dev/null +++ b/lib/views/palRoom/palroom2.txt @@ -0,0 +1,719 @@ +import 'dart:async'; +import 'dart:convert'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/foundation.dart'; +import 'package:agora_rtc_engine/rtc_engine.dart'; +import 'package:permission_handler/permission_handler.dart'; +import 'package:will_play/utils/auth.dart'; + +import '../../utils/settings.dart'; +import '../../utils/http_util.dart'; +import '../../utils/showToast.dart'; + +// 实现语音主要五个步骤逻辑: +// 初始化引擎 +// 开启音频权限,启用音频模块 +// 创建房间 +// 设置事件监听(成功加入房间,是否有用户加入,用户是否离开,用户是否掉线) +// 布局实现 +// 退出语音(根据需要销毁引擎,释放资源) + +class PalRoomPage extends StatefulWidget { + final String agoraToken; + final String userName; + final String channelName; + final ClientRole? role; + + PalRoomPage({ + Key? key, + this.agoraToken = '', + this.userName = '', + this.channelName = '', + this.role, + }) : super(key: key); + + @override + State createState() => _PalRoomPageState(); +} + +class _PalRoomPageState extends State { + // final _users = []; // 用户id数组 + final _infoStrings = []; // 状态文字信息列表 + bool viewPanel = true; // 是否显示状态文字信息列表 + bool muted = false; // 是否开麦 + bool volume = true; // 音量 + late RtcEngine _engine; // 声网实例变量 + + // 房间座位 + List _list = [ + {'nickname': '', 'flag': false, 'image': 'images/palRoom/voice2.png'}, + {'nickname': '', 'flag': false, 'image': 'images/palRoom/voice2.png'}, + {'nickname': '', 'flag': false, 'image': 'images/palRoom/voice2.png'}, + {'nickname': '', 'flag': false, 'image': 'images/palRoom/voice2.png'}, + {'nickname': '', 'flag': false, 'image': 'images/palRoom/voice2.png'}, + {'nickname': '', 'flag': false, 'image': 'images/palRoom/voice2.png'}, + {'nickname': '', 'flag': false, 'image': 'images/palRoom/voice2.png'}, + {'nickname': '', 'flag': false, 'image': 'images/palRoom/voice2.png'}, + ]; + + // 连麦用户列表数据 + List newUserList = []; + + @override + void initState() { + super.initState(); + _infoStrings.insert(0, { + 'type': 'system', + 'content': + '|令牌:${widget.agoraToken}|用户名:${widget.userName}|房间:${widget.channelName}|身份:${widget.role}' + }); + + initialize(); + + if (widget.role == ClientRole.Broadcaster) { + _infoStrings.insert(0, {'type': 'system', 'content': '您身份为主播,已经上麦状态'}); + } else if (widget.role == ClientRole.Audience) { + _infoStrings.insert(0, {'type': 'system', 'content': '您身份为观众,点击麦克风图标上麦'}); + } + } + + @override + void dispose() { + super.dispose(); + // _users.clear(); + _engine.leaveChannel(); + _engine.destroy(); + } + + Future initialize() async { + // 判断appid是否存在 + if (APP_ID.isEmpty) { + setState(() { + _infoStrings.insert(0, { + 'type': 'system', + 'content': 'APP ID缺失,请在settings.dart中提供您的APP ID' + }); + _infoStrings.insert(0, {'type': 'system', 'content': 'Agora引擎没有启动'}); + }); + return; + } + + _engine = await RtcEngine.create(APP_ID); // 初始化引擎 + await _engine.enableAudio(); // 启用音频模块 + await _engine.setDefaultAudioRouteToSpeakerphone( + true); // 设置默认的音频路由:该方法设置接收到的音频从听筒或扬声器出声。如果用户不调用本方法,音频默认从听筒出声。 + await _engine.setAudioProfile(AudioProfile.SpeechStandard, + AudioScenario.ChatRoomEntertainment); // 设置音频编码属性和音频场景 + await _engine.setChannelProfile(ChannelProfile.LiveBroadcasting); + + _addAgoraEventHandlers(); // 调用状态文字信息列表方法 + + if (widget.role == ClientRole.Broadcaster) { + await _engine.setClientRole(ClientRole.Broadcaster); + } + + String uid = await Storage.get('id'); + + await _engine.joinChannel( + widget.agoraToken, widget.channelName, null, int.parse(uid)); // 加入频道 + + _detail(); + } + + // 获取房间详情接口 + void _detail() async { + var res = + await MyHttpUtil().get("/chat/api/room/detail/${widget.channelName}"); + // print('${res}'); + + var _userList = res.data['userList']; + + // 赋值image字段头像 + _userList.forEach((item) { + item['image'] = 'images/palRoom/photo.png'; + item['flag'] = true; + }); + + // 筛选出身份类型为主播的用户 + newUserList = + _userList.where((item) => item['type'] != 'audience').toList(); + print('-----------------筛选出身份类型为主播的用户:${newUserList}-----------------'); + + for (var i = 0; i < _list.length; i++) { + if (newUserList.length < (i + 1)) { + setState(() { + _list[i] = { + 'nickname': '', + 'flag': false, + 'image': 'images/palRoom/voice2.png' + }; + }); + } else { + setState(() { + _list[i] = newUserList[i]; + }); + } + } + + print('-----------------${_list}-----------------'); + } + + void _addAgoraEventHandlers() { + _engine.setEventHandler(RtcEngineEventHandler( + error: (code) { + // 错误 + setState(() { + final info = '错误 $code'; + _infoStrings.insert(0, {'type': 'system', 'content': info}); + }); + }, + userJoined: (uid, elapsed) { + _detail(); + // 用户加入 + setState(() { + final info = '用户加入: uid $uid'; + _infoStrings.insert(0, {'type': 'system', 'content': info}); + // _users.add(uid); + }); + }, + joinChannelSuccess: (channel, uid, elapsed) { + _detail(); + // 加入频道成功 + setState(() { + final info = '加入频道成功: $channel, uid: $uid'; + _infoStrings.insert(0, {'type': 'system', 'content': info}); + }); + }, + leaveChannel: (stats) { + _detail(); + // 离开频道 + setState(() { + _infoStrings.insert(0, {'type': 'system', 'content': '离开频道'}); + // _users.clear(); + }); + }, + userOffline: (uid, reason) { + _detail(); + // 用户离开当前频道 + setState(() { + final info = '用户离开当前频道: $uid , reason: $reason'; + _infoStrings.insert(0, {'type': 'system', 'content': info}); + // _users.remove(uid); + }); + }, + clientRoleChanged: (oldRole, newRole) { + _detail(); + // 直播场景下用户角色切换成功回调 + setState(() { + final info = '用户旧身份$oldRole,用户新身份$newRole'; + _infoStrings.insert(0, {'type': 'system', 'content': info}); + }); + }, + rejoinChannelSuccess: (channel, uid, elapsed) { + _detail(); + // 成功重新加入频道回调 + setState(() { + final info = '用户成功重新加入: $uid'; + _infoStrings.insert(0, {'type': 'system', 'content': info}); + // _users.add(uid); + }); + }, + warning: (warn) { + // 报告 RtcChannel 的警告码 + print(warn); + }, + tokenPrivilegeWillExpire: (token) { + // Token 服务将在30s内过期回调 + // app 应重新获取 Token,然后调用 _engine.renewToken(token) 将新的 Token 传给 SDK + print('即将服务失效的 Token:${token}'); + }, + requestToken: () { + // Token 已过期回调 + setState(() { + final info = 'Token 已过期'; + _infoStrings.insert(0, {'type': 'system', 'content': info}); + }); + }, + )); + } + + // 头部 + Widget _head() { + // 底部弹框组件 + _modelBottomSheet(index) async { + showModalBottomSheet( + context: context, + builder: (context) { + return Container( + height: 150.0, + child: Column( + children: [ + _list[index]['flag'] == false + ? ListTile( + title: Text( + "连线上麦", + textAlign: TextAlign.center, + ), + onTap: () { + // setState(() { + // for (var i = 0; i < _list.length; i++) { + // if (i == index) { + // _list[index]['flag'] = true; + // _list[index]['nickname'] = '_users'; + // _list[index]['image'] = + // 'images/palRoom/photo.png'; + // } else { + // _list[i]['flag'] = false; + // _list[i]['name'] = ''; + // _list[i]['image'] = 'images/palRoom/voice2.png'; + // } + // } + // }); + + // 申请连麦需要改变身份为主播! + _engine.setClientRole(ClientRole.Broadcaster); + setState(() { + _list[index]['flag'] = true; + }); + Navigator.pop(context); + }, + ) + : ListTile( + title: Text( + "下麦", + textAlign: TextAlign.center, + ), + onTap: () { + setState(() { + _list[index]['flag'] = false; + _list[index]['nickname'] = ''; + _list[index]['image'] = 'images/palRoom/voice2.png'; + }); + // 下麦再将身份改回为观众(下麦不代表离开频道) + _engine.setClientRole(ClientRole.Audience); + Navigator.pop(context); + }, + ), + ListTile( + title: Text( + "取消", + textAlign: TextAlign.center, + ), + onTap: () { + Navigator.pop(context); + }), + ], + ), + ); + }, + ); + } + + return Container( + child: ListView( + children: [ + // --房主 start -- + Container( + padding: EdgeInsets.fromLTRB(0, 27, 0, 5), + alignment: Alignment.topCenter, + child: Image.asset( + 'images/palRoom/photo.png', + width: 70, + height: 70, + ), + ), + Container( + margin: EdgeInsets.only(bottom: 30), + alignment: Alignment.topCenter, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + width: 18, + height: 18, + alignment: Alignment.center, + decoration: BoxDecoration( + color: Color.fromRGBO(245, 173, 29, 1), + borderRadius: BorderRadius.circular(2), + ), + child: Text( + '房', + style: TextStyle( + color: Colors.white, + fontSize: 12, + ), + ), + ), + SizedBox(width: 5), + Text('以冬', style: TextStyle(color: Colors.white, fontSize: 14)), + ], + ), + ), + // --房主 end -- + + // -- 网格 start -- + Container( + // padding: EdgeInsets.only(left: 15, right: 15), + child: GridView.builder( + shrinkWrap: true, + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisSpacing: 40, + mainAxisSpacing: 30, + crossAxisCount: 4, + childAspectRatio: 1 / 1.25, + ), + itemCount: _list.length, + itemBuilder: (BuildContext context, int index) { + return GestureDetector( + onTap: () { + setState(() { + if (widget.role == ClientRole.Audience) { + // 观众身份的时候点击上麦克图标 + _modelBottomSheet(index); // 调用底部弹框方法 + } else { + _infoStrings.insert( + 0, {'type': 'system', 'content': '您身份为主播,已经上麦状态'}); + } + }); + }, + child: Column( + children: [ + Container( + width: 55, + height: 55, + margin: EdgeInsets.only(bottom: 5), + decoration: BoxDecoration( + color: Color.fromRGBO(52, 51, 69, 1), + borderRadius: BorderRadius.circular(50), + ), + child: Center( + child: Image.asset( + _list[index]['image'], + width: _list[index]['flag'] ? 55 : 14, + height: _list[index]['flag'] ? 55 : 21, + ), + ), + ), + Text( + _list[index]['nickname'], + textAlign: TextAlign.center, + style: TextStyle(color: Colors.white, fontSize: 12), + ), + ], + ), + ); + }, + ), + ), + // -- 网格 end -- + ], + ), + ); + } + + // 消息列表 + Widget _planel() { + _filtering(type, content, {name}) { + switch (type) { + case 'system': + return Container( + child: Text( + '系统公告:$content', + style: TextStyle(color: Color.fromRGBO(155, 154, 170, 1)), + ), + ); + case 'speak': + return Container( + child: Text( + '${name}的聊天消息:$content', + style: TextStyle(color: Color.fromRGBO(155, 154, 170, 1)), + ), + ); + case 'gift': + return Container( + child: Text( + '礼物消息:$content', + style: TextStyle(color: Color.fromRGBO(155, 154, 170, 1)), + ), + ); + } + } + + return Visibility( + visible: viewPanel, + child: Container( + padding: EdgeInsets.symmetric(vertical: 60), + alignment: Alignment.bottomCenter, + child: FractionallySizedBox( + heightFactor: 0.5, + child: Container( + // decoration: BoxDecoration( + // color: Colors.black, + // ), + child: ListView.builder( + reverse: true, + itemCount: _infoStrings.length, + itemBuilder: (BuildContext context, int index) { + if (_infoStrings.isEmpty) { + return Text('null'); + } + return Padding( + padding: EdgeInsets.symmetric(vertical: 5, horizontal: 15), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: Container( + padding: EdgeInsets.symmetric( + vertical: 10, + horizontal: 10, + ), + decoration: BoxDecoration( + color: Color.fromRGBO(50, 51, 71, 1), + borderRadius: BorderRadius.circular(5), + ), + child: _filtering(_infoStrings[index]['type'], + _infoStrings[index]['content'], + name: _infoStrings[index]['name']), + ), + ), + ], + ), + ); + }, + ), + ), + ), + ), + ); + } + + // 底部输入框控制台 + Widget _footer() { + final _input = TextEditingController(); + return Container( + width: double.infinity, + alignment: Alignment.bottomCenter, + padding: EdgeInsets.symmetric( + vertical: 18, + horizontal: 15, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + GestureDetector( + // 音量图标 + onTap: () { + setState(() { + volume = !volume; + }); + _engine.adjustPlaybackSignalVolume( + volume ? 100 : 0); // 调节本地播放的所有远端用户信号音量 + }, + child: Icon(volume ? Icons.volume_up : Icons.volume_off, + color: Color.fromRGBO(201, 201, 201, 1), size: 25.0), + ), + GestureDetector( + // 麦克风图标 + onTap: () { + setState(() { + muted = !muted; + }); + _engine.muteLocalAudioStream(muted); // 取消或恢复发布本地音频流(是否静音) + }, + child: Icon(muted ? Icons.mic_off : Icons.mic, + color: Color.fromRGBO(201, 201, 201, 1), size: 25.0), + ), + GestureDetector( + // 表情图标 + child: Icon(Icons.tag_faces, + color: Color.fromRGBO(201, 201, 201, 1), size: 25.0), + ), + Container( + // 输入框 + width: 220, + padding: EdgeInsets.symmetric(horizontal: 10), + decoration: BoxDecoration( + color: Color.fromRGBO(52, 51, 69, 1), + borderRadius: BorderRadius.circular(30), + ), + child: TextField( + onSubmitted: (value) async { + late String speak = value.trim(); + if (speak.isEmpty) { + showToast("请输入聊天内容~"); + return; + } + + String userName = await Storage.get('userName'); // 用户名 + + setState(() { + _infoStrings.insert(0, { + 'type': 'speak', + 'content': '${speak}', + 'name': '${userName}' + }); + }); + }, + controller: _input, + decoration: InputDecoration( + isDense: true, + border: InputBorder.none, + hintText: '我来说几句', + hintStyle: TextStyle( + fontSize: 12, + color: Color.fromRGBO(138, 138, 146, 1), + ), + ), + style: TextStyle( + fontSize: 12, + color: Colors.white, + ), + textInputAction: TextInputAction.send, // 键盘右下角图标 + ), + ), + GestureDetector( + // 加号图标 + child: Icon(Icons.add, + color: Color.fromRGBO(201, 201, 201, 1), size: 25.0), + ), + ], + ), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + resizeToAvoidBottomInset: true, + appBar: AppBar( + leading: IconButton( + icon: Icon(Icons.arrow_back_ios), + onPressed: () { + print('离开'); + _engine.setClientRole(ClientRole.Audience); + _engine.leaveChannel(); + Navigator.of(context).pop(); + }), + elevation: 0, // z轴阴影 + titleSpacing: 0, // 标题与其他控件的间隔 + backgroundColor: Color.fromRGBO(27, 28, 48, 1), + title: Container( + height: 60, + decoration: BoxDecoration( + color: Color.fromRGBO(27, 28, 48, 1), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + SizedBox( + child: Padding( + padding: EdgeInsets.only(right: 5), + child: ClipOval( + child: Image.asset( + 'images/palRoom/photo.png', + width: 46, + height: 46, + ), + ), + ), + ), + Expanded( + flex: 1, + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Row( + children: [ + Text( + '以冬', + style: TextStyle(color: Colors.white, fontSize: 12), + ), + SizedBox(width: 5), + Text( + 'ID309060', + style: TextStyle( + color: Color.fromRGBO(242, 174, 30, 1), + fontSize: 10, + ), + ), + ], + ), + Row( + children: [ + Text.rich( + TextSpan( + style: TextStyle( + fontSize: 10, + color: Colors.white, + ), + children: [ + TextSpan(text: '交友'), + TextSpan(text: ' | '), + TextSpan(text: '房间名:成人避风港'), + ]), + ), + SizedBox(width: 5), + Container( + padding: EdgeInsets.fromLTRB(6, 2, 6, 2), + decoration: BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(2)), + color: Color.fromRGBO(47, 47, 59, 1), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + '在线:121', + style: TextStyle( + fontSize: 10, + color: Colors.white, + ), + ), + SizedBox(width: 5), + Image.asset( + 'images/palRoom/whiteRight.png', + width: 5, + height: 8, + ), + ], + ), + ) + ], + ), + ], + ), + ) + ], + ), + ), + actions: [ + IconButton( + onPressed: () { + print('more图标被点击'); + setState(() { + viewPanel = !viewPanel; + }); + }, + icon: Image.asset( + 'images/palRoom/more.png', + fit: BoxFit.cover, + width: 20.0, + height: 5.0, + ), + ), + ], + ), + backgroundColor: Color.fromRGBO(27, 28, 48, 1), + body: Stack( + children: [ + _head(), + _planel(), + _footer(), + ], + ), + ); + } +} diff --git a/lib/views/party/Party.dart b/lib/views/party/Party.dart index f512581..061c0d1 100644 --- a/lib/views/party/Party.dart +++ b/lib/views/party/Party.dart @@ -1,11 +1,12 @@ -import 'package:flutter/material.dart'; import 'dart:async'; -import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; import 'package:agora_rtc_engine/rtc_engine.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:will_play/views/palRoom/PalRoom.dart'; +import 'package:will_play/utils/auth.dart'; import '../../utils/http_util.dart'; +import '../../api/part.dart'; class PartyPage extends StatefulWidget { PartyPage({Key? key}) : super(key: key); @@ -126,7 +127,7 @@ class _PartyPageState extends State { } } -// tab视图页 +// tab视图页组件 class ViewsWidget extends StatefulWidget { ViewsWidget({Key? key}) : super(key: key); @@ -135,53 +136,6 @@ class ViewsWidget extends StatefulWidget { } class _ViewsWidgetState extends State { - @override - Widget build(BuildContext context) { - return SingleChildScrollView( - child: Column( - children: [ - Banner(), - Lists(), - ], - ), - ); - } -} - -// 横幅组件 -class Banner extends StatefulWidget { - Banner({Key? key}) : super(key: key); - - @override - State createState() => _BannerState(); -} - -class _BannerState extends State { - @override - Widget build(BuildContext context) { - return SingleChildScrollView( - child: Container( - padding: EdgeInsets.fromLTRB(15, 15, 15, 0), - child: GestureDetector( - child: Image.asset('images/party/banner.png'), - onTap: () { - print('Banner on tap'); - }, - ), - ), - ); - } -} - -// 列表组件 -class Lists extends StatefulWidget { - Lists({Key? key}) : super(key: key); - - @override - State createState() => _ListsState(); -} - -class _ListsState extends State { List _list = [ { "photo": "images/party/photo1.png", @@ -201,6 +155,15 @@ class _ListsState extends State { "sex": "1", "role": ClientRole.Audience, }, + { + "photo": "images/party/photo1.png", + "name": "小红", + "num": "12", + "title": "成年人的避风港", + "type": "pal", + "sex": "1", + "role": ClientRole.Audience, + }, { "photo": "images/party/photo1.png", "name": "小黑", @@ -219,21 +182,23 @@ class _ListsState extends State { "sex": "0", "role": ClientRole.Audience, }, + { + "photo": "images/party/photo1.png", + "name": "小白", + "num": "122", + "title": "今日听君歌一曲", + "type": "family", + "sex": "0", + "role": ClientRole.Audience, + }, ]; - - @override - void initState() { - super.initState(); - _getList(); - } - - _getList() async { - var res = await MyHttpUtil().get( - "http://101.35.117.69:9093/chat/api/room/page", - ); - print(res); - // _list = res.data['content']; - } + int page = 1; // 当前页面 + int size = 10; // 每页N条 + late int total; // 总条数 + bool isLoadmore = false; // 加载更多 + List _data = []; // 列表数据 + // 给ListView加一个 ScrollController 组件,通过事件监听滚动条的高度来显示和隐藏加载更多的组件 + ScrollController _scrollController = new ScrollController(); // 判断是否开启权限方法 Future _handleCameraAndMic(Permission permission) async { @@ -245,7 +210,7 @@ class _ListsState extends State { } // 页面跳转方法 - Future onJoin(userName, role) async { + Future _onJoin(agoraToken, userName, channelName, role) async { await _handleCameraAndMic(Permission.camera); await _handleCameraAndMic(Permission.microphone); @@ -253,96 +218,202 @@ class _ListsState extends State { context, MaterialPageRoute( builder: (context) => PalRoomPage( + agoraToken: agoraToken, userName: userName, - channelName: 'call', + channelName: channelName, role: role, ), ), ); } - List _tempList() { - var tempList = _list.map((value) { - return GestureDetector( - onTap: () => onJoin(value['name'], value['role']), - child: Container( - height: 75, - padding: EdgeInsets.all(5), - margin: EdgeInsets.fromLTRB(15, 30, 15, 0), - width: double.infinity, - decoration: BoxDecoration( - color: Color.fromRGBO(255, 255, 255, 1), - borderRadius: BorderRadius.all(Radius.circular(50)), - boxShadow: [ - BoxShadow( - color: Color.fromRGBO(0, 0, 0, 0.1), - offset: Offset(0.0, 1.0), // 阴影xy轴偏移量 - blurRadius: 1.0, // 阴影模糊程度 - spreadRadius: 1.0 // 阴影扩散程度 - ) - ], - ), - child: Row( - children: [ - ClipOval( - child: Image.asset( - value['photo'], - width: 65, - height: 65, - ), - ), - SizedBox( - width: 10, - ), - Expanded( - flex: 1, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Row( - children: [ - typeElement(type: value['type']), - Container( - child: Text( - value['title'], - style: TextStyle( - color: Color.fromRGBO(51, 51, 51, 1), - fontSize: 16), - ), - ) - ], - ), - SizedBox(height: 9), - Row( - children: [ - Image.asset( - 'images/party/live.png', - width: 17, - height: 18, - ), - SizedBox(width: 5), - Text( - value["num"] + '人', - style: TextStyle( - color: Color.fromRGBO(153, 153, 153, 1), - fontSize: 12), - ) - ], - ), - ], - )), - ], - ), - ), - ); + // 刷新列表方法 + _onRefresh() { + _data.clear(); + this.page = 1; + getList({'page': this.page, 'size': this.size}).then((res) { + print('_onRefresh:${res.data}'); + setState(() { + _data.addAll(res.data['content']); + }); }); - return tempList.toList(); + } + + // 加载数据方法 + _onLoadmore() { + this.page++; + getList({'page': this.page, 'size': this.size}).then((res) { + setState(() { + print('_onLoadmore:${res.data}'); + _data.addAll(res.data['content']); + isLoadmore = false; + }); + }); + } + + // 加载中动画组件 + Widget _loadMoreWidget() { + return Padding( + padding: EdgeInsets.all(15.0), + child: Center(child: CircularProgressIndicator()), + ); } + @override + void initState() { + super.initState(); + + getList({'page': this.page, 'size': this.size}).then((res) { + // print('列表数据:${res.data}'); + print('${res.data['content']}'); + setState(() { + _data.addAll(res.data['content']!); + }); + }); + + // _onRefresh(); + // _scrollController.addListener(() { + // if (_scrollController.position.pixels == + // _scrollController.position.maxScrollExtent) { + // _onLoadmore(); + // } + // }); + } + + @override + void dispose() { + super.dispose(); + _scrollController.dispose(); + } + + void _agoraToken(roomId, channelName, role) async {} + @override Widget build(BuildContext context) { - return Column( - children: _tempList(), + // return RefreshIndicator( onRefresh: _onRefresh(), + return SingleChildScrollView( + child: Column( + children: [ + Container( + padding: EdgeInsets.fromLTRB(15, 15, 15, 0), + child: GestureDetector( + child: Image.asset('images/party/banner.png'), + onTap: () { + print('Banner on tap'); + }, + ), + ), + _data.length > 0 + ? ListView.builder( + controller: _scrollController, + itemCount: _data.length, + shrinkWrap: true, + itemBuilder: (BuildContext context, int index) { + // if (index == this.data.length - 1) { + // return _loadMoreWidget(); + // } + return GestureDetector( + onTap: () async { + var res = await MyHttpUtil() + .get("/chat/api/auth/agora/token", data: { + 'channelName': _data[index]['id'], + 'role': 2 + }); + + var agoraToken = res.data["token"]; + String uid = await Storage.get('id'); + String userName = await Storage.get('userName'); // 用户名 + + print(agoraToken); + print(_data[index]['id']); + + _onJoin(agoraToken, userName, _data[index]['id'], + ClientRole.Audience); + }, + child: Container( + height: 75, + padding: EdgeInsets.all(5), + margin: EdgeInsets.fromLTRB(15, 30, 15, 0), + width: double.infinity, + decoration: BoxDecoration( + color: Color.fromRGBO(255, 255, 255, 1), + borderRadius: BorderRadius.all(Radius.circular(50)), + boxShadow: [ + BoxShadow( + color: Color.fromRGBO(0, 0, 0, 0.1), + offset: Offset(0.0, 1.0), // 阴影xy轴偏移量 + blurRadius: 1.0, // 阴影模糊程度 + spreadRadius: 1.0 // 阴影扩散程度 + ) + ], + ), + child: Row( + children: [ + ClipOval( + child: Image.asset( + 'images/party/photo1.png', + width: 65, + height: 65, + ), + ), + SizedBox( + width: 10, + ), + Expanded( + flex: 1, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Row( + children: [ + typeElement(type: 'pal'), + Container( + child: Text( + _data[index]['name'], + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: Color.fromRGBO( + 51, 51, 51, 1), + fontSize: 16), + ), + ) + ], + ), + SizedBox(height: 9), + Row( + children: [ + Image.asset( + 'images/party/live.png', + width: 17, + height: 18, + ), + SizedBox(width: 5), + Text( + _data[index]['id'], + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: Color.fromRGBO( + 153, 153, 153, 1), + fontSize: 12), + ) + ], + ), + ], + )), + ], + ), + ), + ); + }, + ) + : Container( + padding: EdgeInsets.all(15), + child: Center( + child: Text('暂无数据~'), + ), + ), + ], + ), ); } } diff --git a/lib/views/party/party.txt b/lib/views/party/party.txt new file mode 100644 index 0000000..208cbeb --- /dev/null +++ b/lib/views/party/party.txt @@ -0,0 +1,467 @@ +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:agora_rtc_engine/rtc_engine.dart'; +import 'package:permission_handler/permission_handler.dart'; +import 'package:will_play/views/palRoom/PalRoom.dart'; + +import '../../api/part.dart'; + +class PartyPage extends StatefulWidget { + PartyPage({Key? key}) : super(key: key); + + @override + State createState() => _PartyPageState(); +} + +class _PartyPageState extends State { + final _search = TextEditingController(); + final _tabDataList = [ + Tab(text: '关注'), + Tab(text: '推荐'), + Tab(text: '相亲'), + Tab(text: 'KTV'), + Tab(text: '交友'), + Tab(text: '音乐'), + ]; + + @override + Widget build(BuildContext context) { + return DefaultTabController( + initialIndex: 1, // 默认选中 + length: 6, + child: Scaffold( + backgroundColor: Colors.white, + appBar: AppBar( + elevation: 0, // z轴阴影 + leading: null, + titleSpacing: 10, // 标题与其他控件的间隔 + backgroundColor: Colors.white, + title: Container( + height: 35, + margin: EdgeInsetsDirectional.only(end: 25), + padding: EdgeInsets.fromLTRB(20, 0, 20, 0), + decoration: BoxDecoration( + color: Color.fromRGBO(247, 247, 247, 1), + borderRadius: BorderRadius.circular(30), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + SizedBox( + child: Padding( + padding: EdgeInsets.only(right: 5), + child: Image.asset( + 'images/party/search.png', + width: 17.0, + height: 17.0, + fit: BoxFit.cover, + ), + ), + ), + Expanded( + flex: 1, + child: TextField( + controller: _search, + decoration: InputDecoration( + isDense: true, + border: InputBorder.none, + hintText: '搜索房间号', + hintStyle: TextStyle( + fontSize: 12, + color: Color.fromRGBO(203, 203, 203, 1), + ), + ), + style: TextStyle( + fontSize: 12, + color: Colors.black, + ), + textInputAction: TextInputAction.search, // 键盘右下角图标 + ), + ), + ], + ), + ), + actions: [ + IconButton( + onPressed: () { + Navigator.pushNamed(context, 'CreateRoom'); + }, + icon: Image.asset( + 'images/party/create.png', + fit: BoxFit.cover, + width: 22.0, + height: 22.0, + ), + ), + ], + bottom: TabBar( + // isScrollable: true, + indicatorSize: TabBarIndicatorSize.label, // 指示器大小计算方式 + indicatorColor: Color.fromRGBO(255, 255, 255, 0), // 指示器颜色 + labelColor: Colors.black, // 选中label颜色 + labelStyle: TextStyle(fontSize: 18), // 选中label的Style + unselectedLabelStyle: TextStyle(fontSize: 14), // 未选中label的Style + unselectedLabelColor: + Color.fromRGBO(153, 153, 153, 1), // 未选中label颜色 + tabs: _tabDataList, + enableFeedback: true, + onTap: (index) { + print(_tabDataList[index]); + }, + ), + ), + body: TabBarView( + children: [ + ViewsWidget(), + ViewsWidget(), + ViewsWidget(), + ViewsWidget(), + ViewsWidget(), + ViewsWidget(), + ], + ), + ), + ); + } +} + +// tab视图页组件 +class ViewsWidget extends StatefulWidget { + ViewsWidget({Key? key}) : super(key: key); + + @override + State createState() => _ViewsWidgetState(); +} + +class _ViewsWidgetState extends State { + List _list = [ + { + "photo": "images/party/photo1.png", + "name": "小明", + "num": "12", + "title": "今日听君歌一曲", + "type": "music", + "sex": "0", + "role": ClientRole.Broadcaster, + }, + { + "photo": "images/party/photo1.png", + "name": "小红", + "num": "12", + "title": "成年人的避风港", + "type": "pal", + "sex": "1", + "role": ClientRole.Audience, + }, + { + "photo": "images/party/photo1.png", + "name": "小红", + "num": "12", + "title": "成年人的避风港", + "type": "pal", + "sex": "1", + "role": ClientRole.Audience, + }, + { + "photo": "images/party/photo1.png", + "name": "小黑", + "num": "12", + "title": "今日听君歌一曲", + "type": "auction", + "sex": "0", + "role": ClientRole.Audience, + }, + { + "photo": "images/party/photo1.png", + "name": "小白", + "num": "122", + "title": "今日听君歌一曲", + "type": "family", + "sex": "0", + "role": ClientRole.Audience, + }, + { + "photo": "images/party/photo1.png", + "name": "小白", + "num": "122", + "title": "今日听君歌一曲", + "type": "family", + "sex": "0", + "role": ClientRole.Audience, + }, + ]; + int page = 1; // 当前页面 + int size = 10; // 每页N条 + late int total; // 总条数 + bool isLoadmore = false; // 加载更多 + late List _data; // 列表数据 + // 给ListView加一个 ScrollController 组件,通过事件监听滚动条的高度来显示和隐藏加载更多的组件 + ScrollController _scrollController = new ScrollController(); + + // 判断是否开启权限方法 + Future _handleCameraAndMic(Permission permission) async { + PermissionStatus status = await permission.request(); + print('权限状态$status'); + if (!status.isGranted) { + openAppSettings(); + } + } + + // 页面跳转方法 + Future _onJoin(userName, role) async { + await _handleCameraAndMic(Permission.camera); + await _handleCameraAndMic(Permission.microphone); + + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => PalRoomPage( + userName: userName, + channelName: 'call', + role: role, + ), + ), + ); + } + + // 刷新列表方法 + _onRefresh() { + this.data.clear(); + this.page = 1; + getList({'page': this.page, 'size': this.size}).then((res) { + print('_onRefresh:${res.data}'); + setState(() { + this.data.addAll(res.data.content); + }); + }); + } + + // 加载数据方法 + _onLoadmore() { + this.page++; + getList({'page': this.page, 'size': this.size}).then((res) { + setState(() { + print('_onLoadmore:${res.data}'); + this.data.addAll(res.data.content); + isLoadmore = false; + }); + }); + } + + // 加载中动画组件 + Widget _loadMoreWidget() { + return Padding( + padding: EdgeInsets.all(15.0), + child: Center(child: CircularProgressIndicator()), + ); + } + + @override + void initState() { + super.initState(); + + getList({'page': this.page, 'size': this.size}).then((res) { + print('列表数据:${res.data}'); + }); + + // _onRefresh(); + // _scrollController.addListener(() { + // if (_scrollController.position.pixels == + // _scrollController.position.maxScrollExtent) { + // _onLoadmore(); + // } + // }); + } + + @override + void dispose() { + super.dispose(); + _scrollController.dispose(); + } + + @override + Widget build(BuildContext context) { + // return RefreshIndicator( onRefresh: _onRefresh(), + return SingleChildScrollView( + child: Column( + children: [ + Container( + padding: EdgeInsets.fromLTRB(15, 15, 15, 0), + child: GestureDetector( + child: Image.asset('images/party/banner.png'), + onTap: () { + print('Banner on tap'); + }, + ), + ), + ListView.builder( + controller: _scrollController, + itemCount: _list.length, + shrinkWrap: true, + itemBuilder: (BuildContext context, int index) { + // if (index == this.data.length - 1) { + // return _loadMoreWidget(); + // } + return GestureDetector( + onTap: () => + _onJoin(_list[index]['name'], _list[index]['role']), + child: Container( + height: 75, + padding: EdgeInsets.all(5), + margin: EdgeInsets.fromLTRB(15, 30, 15, 0), + width: double.infinity, + decoration: BoxDecoration( + color: Color.fromRGBO(255, 255, 255, 1), + borderRadius: BorderRadius.all(Radius.circular(50)), + boxShadow: [ + BoxShadow( + color: Color.fromRGBO(0, 0, 0, 0.1), + offset: Offset(0.0, 1.0), // 阴影xy轴偏移量 + blurRadius: 1.0, // 阴影模糊程度 + spreadRadius: 1.0 // 阴影扩散程度 + ) + ], + ), + child: Row( + children: [ + ClipOval( + child: Image.asset( + _list[index]['photo'], + width: 65, + height: 65, + ), + ), + SizedBox( + width: 10, + ), + Expanded( + flex: 1, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Row( + children: [ + typeElement(type: _list[index]['type']), + Container( + child: Text( + _list[index]['title'], + style: TextStyle( + color: Color.fromRGBO(51, 51, 51, 1), + fontSize: 16), + ), + ) + ], + ), + SizedBox(height: 9), + Row( + children: [ + Image.asset( + 'images/party/live.png', + width: 17, + height: 18, + ), + SizedBox(width: 5), + Text( + _list[index]["num"] + '人', + style: TextStyle( + color: Color.fromRGBO(153, 153, 153, 1), + fontSize: 12), + ) + ], + ), + ], + )), + ], + ), + ), + ); + }, + ) + ], + ), + ); + } +} + +// 房间类型标签小组件 +class typeElement extends StatelessWidget { + String type; + + var typeData = { + "music": { + 'title': "音乐", + 'color': [ + Color.fromRGBO(207, 167, 248, 1), + Color.fromRGBO(133, 157, 254, 1), + ], + }, + "pal": { + "title": "交友", + 'color': [ + Color.fromRGBO(207, 167, 248, 1), + Color.fromRGBO(133, 157, 254, 1), + ], + }, + "auction": { + "title": "拍卖", + "color": [ + Color.fromRGBO(249, 163, 125, 1), + Color.fromRGBO(251, 218, 137, 1), + ] + }, + "family": { + 'title': "家族", + "color": [ + Color.fromRGBO(85, 221, 236, 1), + Color.fromRGBO(66, 224, 154, 1), + ] + }, + }; + + var textData = {"music": '音乐', "pal": '交友', "auction": '拍卖', "family": '家族'}; + + var colorData = { + "music": [ + Color.fromRGBO(207, 167, 248, 1), + Color.fromRGBO(133, 157, 254, 1), + ], + "pal": [ + Color.fromRGBO(254, 174, 205, 1), + Color.fromRGBO(247, 111, 162, 1), + ], + "auction": [ + Color.fromRGBO(249, 163, 125, 1), + Color.fromRGBO(251, 218, 137, 1), + ], + "family": [ + Color.fromRGBO(85, 221, 236, 1), + Color.fromRGBO(66, 224, 154, 1), + ], + }; + + typeElement({Key? key, this.type = ''}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + width: 32, + height: 16, + margin: EdgeInsets.only(right: 5), + decoration: BoxDecoration( + borderRadius: BorderRadius.all( + Radius.circular(8), + ), + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: colorData[type] ?? [], + ), + ), + child: Text( + textData[type] ?? "", + textAlign: TextAlign.center, + style: TextStyle(color: Colors.white, fontSize: 10), + ), + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index a221f26..73dde28 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -50,6 +50,13 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.16.0" + crypto: + dependency: transitive + description: + name: crypto + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.0.2" cupertino_icons: dependency: "direct main" description: @@ -378,6 +385,13 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "2.1.2" + web_socket_channel: + dependency: "direct main" + description: + name: web_socket_channel + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.2.0" win32: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 5f5ff42..9c31936 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -40,6 +40,7 @@ dependencies: fluttertoast: ^8.0.6 shared_preferences: ^2.0.7 pretty_dio_logger: ^1.1.1 + web_socket_channel: ^2.1.0 dev_dependencies: flutter_test: @@ -105,6 +106,7 @@ flutter: - images/palRoom/add.png - images/palRoom/face.png - images/palRoom/photo.png + - images/palRoom/user.png - images/palRoom/tip.png - images/palRoom/voice1.png - images/palRoom/voice2.png