代码更新即时通讯
This commit is contained in:
parent
08c5947e63
commit
ba45554dfa
Binary file not shown.
|
After Width: | Height: | Size: 1009 B |
|
|
@ -0,0 +1,6 @@
|
||||||
|
import '../../utils/http_util.dart';
|
||||||
|
|
||||||
|
// 获取房间列表
|
||||||
|
getList(data) {
|
||||||
|
return MyHttpUtil().get("/chat/api/room/page", data: data);
|
||||||
|
}
|
||||||
|
|
@ -14,8 +14,10 @@ class MyHttpUtil {
|
||||||
// "Authorization": getToken() ? 'Bearer $getToken()' : null
|
// "Authorization": getToken() ? 'Bearer $getToken()' : null
|
||||||
};
|
};
|
||||||
options = dioplugn.BaseOptions(
|
options = dioplugn.BaseOptions(
|
||||||
connectTimeout: 15000,
|
connectTimeout: 15000, // 连接超时
|
||||||
receiveTimeout: 3000,
|
receiveTimeout: 15000,
|
||||||
|
// baseUrl: 'http://192.118.2.93:9093',
|
||||||
|
// baseUrl: 'http://HousedeMacBook-Air.local:9093',
|
||||||
baseUrl: 'http://101.35.117.69:9093',
|
baseUrl: 'http://101.35.117.69:9093',
|
||||||
headers: header ?? defaultHeader,
|
headers: header ?? defaultHeader,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
const APP_ID = '809529cf18814549a0249802512a7508';
|
const APP_ID = '809529cf18814549a0249802512a7508';
|
||||||
const Token =
|
const Token =
|
||||||
'006809529cf18814549a0249802512a7508IAAPcoSQKL/h/B6xy0PPG/XdoZUWas07EdCBtxLe7HH4Hz4vjswAAAAAEABsSV6NzT/fYgEAAQDKP99i';
|
'006809529cf18814549a0249802512a7508IAAV27Y5MjNYz2233R1af76+12kaNyjb5VI8eBXNHY9fED4vjswAAAAAEACOhaHH2fLpYgEAAQDW8uli';
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,13 @@
|
||||||
|
import 'dart:async';
|
||||||
import 'package:flutter/cupertino.dart';
|
import 'package:flutter/cupertino.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:agora_rtc_engine/rtc_engine.dart';
|
import 'package:agora_rtc_engine/rtc_engine.dart';
|
||||||
import 'package:permission_handler/permission_handler.dart';
|
import 'package:permission_handler/permission_handler.dart';
|
||||||
|
|
||||||
import 'package:will_play/utils/auth.dart';
|
import 'package:will_play/utils/auth.dart';
|
||||||
import 'package:will_play/views/palRoom/PalRoom.dart';
|
import 'package:will_play/views/palRoom/PalRoom.dart';
|
||||||
|
|
||||||
import '../../utils/http_util.dart';
|
import '../../utils/http_util.dart';
|
||||||
|
import '../../utils/settings.dart';
|
||||||
|
|
||||||
class CreateRoomPage extends StatefulWidget {
|
class CreateRoomPage extends StatefulWidget {
|
||||||
CreateRoomPage({Key? key}) : super(key: key);
|
CreateRoomPage({Key? key}) : super(key: key);
|
||||||
|
|
@ -17,6 +19,8 @@ class CreateRoomPage extends StatefulWidget {
|
||||||
class _CreateRoomPageState extends State<CreateRoomPage> {
|
class _CreateRoomPageState extends State<CreateRoomPage> {
|
||||||
final _formKey = GlobalKey<FormState>();
|
final _formKey = GlobalKey<FormState>();
|
||||||
final _roomName = TextEditingController();
|
final _roomName = TextEditingController();
|
||||||
|
|
||||||
|
late RtcEngine _engine; // 声网实例变量
|
||||||
bool flag = false;
|
bool flag = false;
|
||||||
|
|
||||||
List patternList = [
|
List patternList = [
|
||||||
|
|
@ -57,59 +61,61 @@ class _CreateRoomPageState extends State<CreateRoomPage> {
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
var Timer;
|
late Timer timer; // 轮询定时器
|
||||||
|
late String roomName = _roomName.text; // 用户输入的房间名
|
||||||
|
late String roomId; // 创建接口根据用户输入的房间名,生成的房间唯一ID
|
||||||
|
|
||||||
void _create(roomName) {
|
void _create() {
|
||||||
MyHttpUtil().post(
|
MyHttpUtil()
|
||||||
"/chat/api/room/create",
|
.post("/chat/api/room/create", data: {"name": roomName}).then((res) {
|
||||||
data: {"name": roomName},
|
roomId = res.data;
|
||||||
).then((res) {
|
|
||||||
var roomId = res.data;
|
|
||||||
// print(roomId);
|
// print(roomId);
|
||||||
|
_agoraToken(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;
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void _lodingCreate(roomId) async {
|
void _agoraToken(roomId) async {
|
||||||
var resp = await MyHttpUtil().get(
|
var res = await MyHttpUtil().get("/chat/api/auth/agora/token",
|
||||||
"/chat/api/room/create/$roomId",
|
data: {'channelName': roomId, 'role': 1});
|
||||||
);
|
|
||||||
String userName = await Storage.get('userName');
|
var agoraToken = res.data["token"];
|
||||||
print(userName);
|
String uid = await Storage.get('id');
|
||||||
print(resp.data);
|
String userName = await Storage.get('userName'); // 用户名
|
||||||
if (resp.data == true) {
|
|
||||||
// onJoin(userName, roomName, ClientRole.Broadcaster); // 参数1:用户名; 参数2:频道名; 参数3:身份;
|
_onJoin(agoraToken, userName, roomId, ClientRole.Broadcaster);
|
||||||
Timer?.cancel();
|
|
||||||
Timer = null;
|
// 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
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
super.dispose();
|
super.dispose();
|
||||||
Timer?.cancel();
|
timer.cancel();
|
||||||
Timer = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 页面跳转方法:参数1:用户名; 参数2:频道名; 参数3:身份;
|
// 页面跳转方法:参数1:声网token;参数2:用户名; 参数3:频道名; 参数4:身份;
|
||||||
Future<void> onJoin(userName, channelName, role) async {
|
Future<void> _onJoin(agoraToken, userName, channelName, role) async {
|
||||||
await _handleCameraAndMic(Permission.camera);
|
await _handleCameraAndMic(Permission.camera);
|
||||||
await _handleCameraAndMic(Permission.microphone);
|
await _handleCameraAndMic(Permission.microphone);
|
||||||
|
|
||||||
|
|
@ -117,6 +123,7 @@ class _CreateRoomPageState extends State<CreateRoomPage> {
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => PalRoomPage(
|
builder: (context) => PalRoomPage(
|
||||||
|
agoraToken: agoraToken,
|
||||||
userName: userName,
|
userName: userName,
|
||||||
channelName: channelName,
|
channelName: channelName,
|
||||||
role: role,
|
role: role,
|
||||||
|
|
@ -374,8 +381,8 @@ class _CreateRoomPageState extends State<CreateRoomPage> {
|
||||||
),
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
if (_formKey.currentState!.validate()) {
|
if (_formKey.currentState!.validate()) {
|
||||||
_create(_roomName.text);
|
|
||||||
print('表单验证通过!');
|
print('表单验证通过!');
|
||||||
|
_create();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,95 @@
|
||||||
|
// import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
// class InformationPage extends StatefulWidget {
|
||||||
|
// InformationPage({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
// @override
|
||||||
|
// State<InformationPage> createState() => _InformationPageState();
|
||||||
|
// }
|
||||||
|
|
||||||
|
// class _InformationPageState extends State<InformationPage> {
|
||||||
|
// @override
|
||||||
|
// Widget build(BuildContext context) {
|
||||||
|
// return Container(
|
||||||
|
// child: Text('aa'),
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:web_socket_channel/io.dart';
|
||||||
|
|
||||||
class InformationPage extends StatefulWidget {
|
class InformationPage extends StatefulWidget {
|
||||||
InformationPage({Key? key}) : super(key: key);
|
InformationPage({Key? key}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<InformationPage> createState() => _InformationPageState();
|
_InformationPageState createState() => _InformationPageState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _InformationPageState extends State<InformationPage> {
|
class _InformationPageState extends State<InformationPage> {
|
||||||
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Container(
|
return Scaffold(
|
||||||
child: Text("消息"),
|
appBar: AppBar(
|
||||||
|
title: Text("发现页面 WebSocket"),
|
||||||
|
),
|
||||||
|
body: Padding(
|
||||||
|
padding: const EdgeInsets.all(20.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ import 'dart:convert';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:will_play/utils/auth.dart';
|
import 'package:will_play/utils/auth.dart';
|
||||||
|
|
||||||
// import '../../utils/dioHttp.dart';
|
|
||||||
import '../../utils/http_util.dart';
|
import '../../utils/http_util.dart';
|
||||||
import '../../utils/auth.dart';
|
import '../../utils/auth.dart';
|
||||||
import '../../utils/showToast.dart';
|
import '../../utils/showToast.dart';
|
||||||
|
|
@ -36,7 +35,7 @@ class _LoginPageState extends State<LoginPage> {
|
||||||
|
|
||||||
var setToken = await Storage.set('token', '${res.data["token"]}');
|
var setToken = await Storage.set('token', '${res.data["token"]}');
|
||||||
var setId = await Storage.set('id', '${res.data["id"]}');
|
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('/');
|
Navigator.of(context).pushReplacementNamed('/');
|
||||||
}
|
}
|
||||||
|
|
@ -44,6 +43,7 @@ class _LoginPageState extends State<LoginPage> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
|
resizeToAvoidBottomInset: false,
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: Colors.white,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
elevation: 0, // z轴阴影
|
elevation: 0, // z轴阴影
|
||||||
|
|
@ -61,7 +61,7 @@ class _LoginPageState extends State<LoginPage> {
|
||||||
fontWeight: FontWeight.bold),
|
fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
body: Padding(
|
body: SingleChildScrollView(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 30),
|
padding: EdgeInsets.symmetric(horizontal: 30),
|
||||||
child: Form(
|
child: Form(
|
||||||
key: _formKey,
|
key: _formKey,
|
||||||
|
|
|
||||||
|
|
@ -1,77 +1,106 @@
|
||||||
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
import 'package:flutter/gestures.dart';
|
import 'package:flutter/gestures.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'dart:async';
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:agora_rtc_engine/rtc_engine.dart';
|
import 'package:agora_rtc_engine/rtc_engine.dart';
|
||||||
import 'package:permission_handler/permission_handler.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 {
|
class PalRoomPage extends StatefulWidget {
|
||||||
|
final String agoraToken;
|
||||||
final String userName;
|
final String userName;
|
||||||
final String channelName;
|
final String channelName;
|
||||||
final ClientRole? role;
|
final ClientRole? role;
|
||||||
|
|
||||||
PalRoomPage({Key? key, this.userName = '', this.channelName = '', this.role})
|
PalRoomPage({
|
||||||
: super(key: key);
|
Key? key,
|
||||||
|
this.agoraToken = '',
|
||||||
|
this.userName = '',
|
||||||
|
this.channelName = '',
|
||||||
|
this.role,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<PalRoomPage> createState() => _PalRoomPageState();
|
State<PalRoomPage> createState() => _PalRoomPageState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _PalRoomPageState extends State<PalRoomPage> {
|
class _PalRoomPageState extends State<PalRoomPage> {
|
||||||
final _users = <int>[]; // 用户id数组
|
// final _users = <int>[]; // 用户id数组
|
||||||
final _infoStrings = []; // 状态文字信息列表
|
final _infoStrings = []; // 状态文字信息列表
|
||||||
bool viewPanel = true; // 是否显示状态文字信息列表
|
bool viewPanel = true; // 是否显示状态文字信息列表
|
||||||
bool muted = false; // 是否开麦
|
bool muted = false; // 是否开麦
|
||||||
bool volume = true; // 音量
|
bool volume = true; // 音量
|
||||||
late RtcEngine _engine; // 声网实例变量
|
TextEditingController _controller = TextEditingController(); // 输入框内容
|
||||||
|
|
||||||
|
late RtcEngine _engine; // RtcEngine 声网实例变量
|
||||||
|
late IOWebSocketChannel _wsChannel; // webSocket 实例变量
|
||||||
|
|
||||||
// 房间座位
|
// 房间座位
|
||||||
List list = [
|
List _list = [
|
||||||
{'name': '', 'content': false, 'image': 'images/palRoom/voice2.png'},
|
{'nickname': '', 'flag': false, 'image': 'images/palRoom/voice2.png'},
|
||||||
{'name': '', 'content': false, 'image': 'images/palRoom/voice2.png'},
|
{'nickname': '', 'flag': false, 'image': 'images/palRoom/voice2.png'},
|
||||||
{'name': '', 'content': false, 'image': 'images/palRoom/voice2.png'},
|
{'nickname': '', 'flag': false, 'image': 'images/palRoom/voice2.png'},
|
||||||
{'name': '', 'content': false, 'image': 'images/palRoom/voice2.png'},
|
{'nickname': '', 'flag': false, 'image': 'images/palRoom/voice2.png'},
|
||||||
{'name': '', 'content': false, 'image': 'images/palRoom/voice2.png'},
|
{'nickname': '', 'flag': false, 'image': 'images/palRoom/voice2.png'},
|
||||||
{'name': '', 'content': false, 'image': 'images/palRoom/voice2.png'},
|
{'nickname': '', 'flag': false, 'image': 'images/palRoom/voice2.png'},
|
||||||
{'name': '', 'content': false, 'image': 'images/palRoom/voice2.png'},
|
{'nickname': '', 'flag': false, 'image': 'images/palRoom/voice2.png'},
|
||||||
{'name': '', 'content': false, 'image': 'images/palRoom/voice2.png'},
|
{'nickname': '', 'flag': false, 'image': 'images/palRoom/voice2.png'},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// 连麦用户列表数据
|
||||||
|
List newUserList = [];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_infoStrings.insert(0, {
|
|
||||||
'type': 'system',
|
|
||||||
'content': '信息-${widget.userName}-${widget.channelName}-${widget.role}'
|
|
||||||
});
|
|
||||||
|
|
||||||
initialize();
|
initialize();
|
||||||
|
|
||||||
if (widget.role == ClientRole.Broadcaster) {
|
// if (widget.role == ClientRole.Broadcaster) {
|
||||||
_infoStrings.insert(0, {'type': 'system', 'content': '您身份为主播,已经上麦状态'});
|
// _infoStrings.insert(0, {'type': 'system', 'content': '您身份为主播,已经上麦状态'});
|
||||||
} else if (widget.role == ClientRole.Audience) {
|
// } else if (widget.role == ClientRole.Audience) {
|
||||||
_infoStrings.insert(0, {'type': 'system', 'content': '您身份为观众,点击麦克风图标上麦'});
|
// _infoStrings.insert(0, {'type': 'system', 'content': '您身份为观众,点击麦克风图标上麦'});
|
||||||
}
|
// }
|
||||||
|
|
||||||
|
wsInitalize();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
super.dispose();
|
super.dispose();
|
||||||
_users.clear();
|
// _users.clear();
|
||||||
_engine.leaveChannel();
|
_engine.leaveChannel();
|
||||||
_engine.destroy();
|
_engine.destroy();
|
||||||
|
_wsChannel.sink.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> initialize() async {
|
Future<void> initialize() async {
|
||||||
|
_infoStrings.insert(0, {
|
||||||
|
'type': 'system',
|
||||||
|
'content':
|
||||||
|
'|令牌:${widget.agoraToken}|用户名:${widget.userName}|房间:${widget.channelName}|身份:${widget.role}'
|
||||||
|
});
|
||||||
|
|
||||||
// 判断appid是否存在
|
// 判断appid是否存在
|
||||||
if (APP_ID.isEmpty) {
|
if (APP_ID.isEmpty) {
|
||||||
setState(() {
|
setState(() {
|
||||||
|
|
@ -92,12 +121,79 @@ class _PalRoomPageState extends State<PalRoomPage> {
|
||||||
AudioScenario.ChatRoomEntertainment); // 设置音频编码属性和音频场景
|
AudioScenario.ChatRoomEntertainment); // 设置音频编码属性和音频场景
|
||||||
await _engine.setChannelProfile(ChannelProfile.LiveBroadcasting);
|
await _engine.setChannelProfile(ChannelProfile.LiveBroadcasting);
|
||||||
|
|
||||||
|
_addAgoraEventHandlers(); // 调用状态文字信息列表方法
|
||||||
|
|
||||||
if (widget.role == ClientRole.Broadcaster) {
|
if (widget.role == ClientRole.Broadcaster) {
|
||||||
await _engine.setClientRole(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() {
|
void _addAgoraEventHandlers() {
|
||||||
|
|
@ -109,57 +205,80 @@ class _PalRoomPageState extends State<PalRoomPage> {
|
||||||
_infoStrings.insert(0, {'type': 'system', 'content': info});
|
_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) {
|
joinChannelSuccess: (channel, uid, elapsed) {
|
||||||
|
_detail();
|
||||||
// 加入频道成功
|
// 加入频道成功
|
||||||
setState(() {
|
setState(() {
|
||||||
final info = '加入频道: $channel, uid: $uid';
|
final info = '加入频道成功: $channel, uid: $uid';
|
||||||
_infoStrings.insert(0, {'type': 'system', 'content': info});
|
_infoStrings.insert(0, {'type': 'system', 'content': info});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
leaveChannel: (stats) {
|
leaveChannel: (stats) {
|
||||||
|
_detail();
|
||||||
// 离开频道
|
// 离开频道
|
||||||
setState(() {
|
setState(() {
|
||||||
_infoStrings.insert(0, {'type': 'system', 'content': '离开频道'});
|
_infoStrings.insert(0, {'type': 'system', 'content': '离开频道'});
|
||||||
_users.clear();
|
// _users.clear();
|
||||||
});
|
|
||||||
},
|
|
||||||
userJoined: (uid, elapsed) {
|
|
||||||
// 用户加入
|
|
||||||
setState(() {
|
|
||||||
final info = '用户加入: $uid';
|
|
||||||
_infoStrings.insert(0, {'type': 'system', 'content': info});
|
|
||||||
_users.add(uid);
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
userOffline: (uid, reason) {
|
userOffline: (uid, reason) {
|
||||||
// 用户离线
|
_detail();
|
||||||
|
// 用户离开当前频道
|
||||||
setState(() {
|
setState(() {
|
||||||
final info = '用户离线: $uid , reason: $reason';
|
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';
|
|
||||||
_infoStrings.insert(0, {'type': 'system', 'content': info});
|
_infoStrings.insert(0, {'type': 'system', 'content': info});
|
||||||
|
// _users.remove(uid);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
clientRoleChanged: (oldRole, newRole) {
|
clientRoleChanged: (oldRole, newRole) {
|
||||||
|
_detail();
|
||||||
// 直播场景下用户角色切换成功回调
|
// 直播场景下用户角色切换成功回调
|
||||||
setState(() {
|
setState(() {
|
||||||
final info = '用户旧身份$oldRole,用户新身份$newRole';
|
final info = '用户旧身份$oldRole,用户新身份$newRole';
|
||||||
_infoStrings.insert(0, {'type': 'system', 'content': info});
|
_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() {
|
Widget _head() {
|
||||||
// 底部弹框组件
|
// 底部弹框组件
|
||||||
_modelBottomSheet(index) async {
|
_modelBottomSheet(index) {
|
||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
|
|
@ -167,33 +286,34 @@ class _PalRoomPageState extends State<PalRoomPage> {
|
||||||
height: 150.0,
|
height: 150.0,
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
list[index]['content'] == false
|
_list[index]['flag'] == false
|
||||||
? ListTile(
|
? ListTile(
|
||||||
title: Text(
|
title: Text(
|
||||||
"连线上麦",
|
"连线上麦",
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
setState(() {
|
// setState(() {
|
||||||
for (var i = 0; i < list.length; i++) {
|
// for (var i = 0; i < _list.length; i++) {
|
||||||
if (i == index) {
|
// if (i == index) {
|
||||||
list[index]['content'] = true;
|
// _list[index]['flag'] = true;
|
||||||
list[index]['name'] = '_users';
|
// _list[index]['nickname'] = '_users';
|
||||||
list[index]['image'] =
|
// _list[index]['image'] =
|
||||||
'images/palRoom/photo.png';
|
// 'images/palRoom/user.png';
|
||||||
} else {
|
// } else {
|
||||||
list[i]['content'] = false;
|
// _list[i]['flag'] = false;
|
||||||
list[i]['name'] = '';
|
// _list[i]['name'] = '';
|
||||||
list[i]['image'] = 'images/palRoom/voice2.png';
|
// _list[i]['image'] = 'images/palRoom/voice2.png';
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
});
|
// });
|
||||||
|
|
||||||
// 申请连麦需要改变身份为主播!
|
// 申请连麦需要改变身份为主播!
|
||||||
_engine.setClientRole(ClientRole.Broadcaster);
|
_engine.setClientRole(ClientRole.Broadcaster);
|
||||||
|
setState(() {
|
||||||
|
_list[index]['flag'] = true;
|
||||||
|
});
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
print("连线上麦:${list[index]['content']}");
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
: ListTile(
|
: ListTile(
|
||||||
|
|
@ -203,16 +323,13 @@ class _PalRoomPageState extends State<PalRoomPage> {
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
setState(() {
|
setState(() {
|
||||||
list[index]['content'] = false;
|
_list[index]['flag'] = false;
|
||||||
list[index]['name'] = '';
|
_list[index]['nickname'] = '';
|
||||||
list[index]['image'] = 'images/palRoom/voice2.png';
|
_list[index]['image'] = 'images/palRoom/voice2.png';
|
||||||
});
|
});
|
||||||
|
|
||||||
// 下麦再将身份改回为观众(下麦不代表离开频道)
|
// 下麦再将身份改回为观众(下麦不代表离开频道)
|
||||||
_engine.setClientRole(ClientRole.Audience);
|
_engine.setClientRole(ClientRole.Audience);
|
||||||
|
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
print("下麦:${list[index]['content']}");
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
|
|
@ -233,11 +350,12 @@ class _PalRoomPageState extends State<PalRoomPage> {
|
||||||
return Container(
|
return Container(
|
||||||
child: ListView(
|
child: ListView(
|
||||||
children: [
|
children: [
|
||||||
|
// --房主 start --
|
||||||
Container(
|
Container(
|
||||||
padding: EdgeInsets.fromLTRB(0, 27, 0, 5),
|
padding: EdgeInsets.fromLTRB(0, 27, 0, 5),
|
||||||
alignment: Alignment.topCenter,
|
alignment: Alignment.topCenter,
|
||||||
child: Image.asset(
|
child: Image.asset(
|
||||||
'images/palRoom/photo.png',
|
'images/palRoom/user.png',
|
||||||
width: 70,
|
width: 70,
|
||||||
height: 70,
|
height: 70,
|
||||||
),
|
),
|
||||||
|
|
@ -269,9 +387,12 @@ class _PalRoomPageState extends State<PalRoomPage> {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
// --房主 end --
|
||||||
|
|
||||||
|
// -- 网格 start --
|
||||||
Container(
|
Container(
|
||||||
|
// padding: EdgeInsets.only(left: 15, right: 15),
|
||||||
child: GridView.builder(
|
child: GridView.builder(
|
||||||
padding: EdgeInsets.all(15),
|
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
crossAxisSpacing: 40,
|
crossAxisSpacing: 40,
|
||||||
|
|
@ -279,7 +400,7 @@ class _PalRoomPageState extends State<PalRoomPage> {
|
||||||
crossAxisCount: 4,
|
crossAxisCount: 4,
|
||||||
childAspectRatio: 1 / 1.25,
|
childAspectRatio: 1 / 1.25,
|
||||||
),
|
),
|
||||||
itemCount: list.length,
|
itemCount: _list.length,
|
||||||
itemBuilder: (BuildContext context, int index) {
|
itemBuilder: (BuildContext context, int index) {
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
|
|
@ -304,25 +425,27 @@ class _PalRoomPageState extends State<PalRoomPage> {
|
||||||
borderRadius: BorderRadius.circular(50),
|
borderRadius: BorderRadius.circular(50),
|
||||||
),
|
),
|
||||||
child: Center(
|
child: Center(
|
||||||
|
child: ClipOval(
|
||||||
child: Image.asset(
|
child: Image.asset(
|
||||||
list[index]['image'],
|
_list[index]['image'],
|
||||||
width: list[index]['content'] ? 55 : 14,
|
width: _list[index]['flag'] ? 55 : 14,
|
||||||
height: list[index]['content'] ? 55 : 21,
|
height: _list[index]['flag'] ? 55 : 21,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
list[index]['name'],
|
_list[index]['nickname'],
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: TextStyle(color: Colors.white, fontSize: 12),
|
style: TextStyle(color: Colors.white, fontSize: 12),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
;
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
// -- 网格 end --
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
@ -330,7 +453,7 @@ class _PalRoomPageState extends State<PalRoomPage> {
|
||||||
|
|
||||||
// 消息列表
|
// 消息列表
|
||||||
Widget _planel() {
|
Widget _planel() {
|
||||||
_filtering(type, content) {
|
_filtering(type, content, {name}) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'system':
|
case 'system':
|
||||||
return Container(
|
return Container(
|
||||||
|
|
@ -342,7 +465,7 @@ class _PalRoomPageState extends State<PalRoomPage> {
|
||||||
case 'speak':
|
case 'speak':
|
||||||
return Container(
|
return Container(
|
||||||
child: Text(
|
child: Text(
|
||||||
'聊天消息:$content',
|
'${name}的聊天消息:$content',
|
||||||
style: TextStyle(color: Color.fromRGBO(155, 154, 170, 1)),
|
style: TextStyle(color: Color.fromRGBO(155, 154, 170, 1)),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
@ -364,9 +487,9 @@ class _PalRoomPageState extends State<PalRoomPage> {
|
||||||
child: FractionallySizedBox(
|
child: FractionallySizedBox(
|
||||||
heightFactor: 0.5,
|
heightFactor: 0.5,
|
||||||
child: Container(
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
// decoration: BoxDecoration(
|
||||||
color: Colors.black,
|
// color: Colors.black,
|
||||||
),
|
// ),
|
||||||
child: ListView.builder(
|
child: ListView.builder(
|
||||||
reverse: true,
|
reverse: true,
|
||||||
itemCount: _infoStrings.length,
|
itemCount: _infoStrings.length,
|
||||||
|
|
@ -390,7 +513,8 @@ class _PalRoomPageState extends State<PalRoomPage> {
|
||||||
borderRadius: BorderRadius.circular(5),
|
borderRadius: BorderRadius.circular(5),
|
||||||
),
|
),
|
||||||
child: _filtering(_infoStrings[index]['type'],
|
child: _filtering(_infoStrings[index]['type'],
|
||||||
_infoStrings[index]['content']),
|
_infoStrings[index]['content'],
|
||||||
|
name: _infoStrings[index]['name']),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
@ -406,7 +530,6 @@ class _PalRoomPageState extends State<PalRoomPage> {
|
||||||
|
|
||||||
// 底部输入框控制台
|
// 底部输入框控制台
|
||||||
Widget _footer() {
|
Widget _footer() {
|
||||||
final _input = TextEditingController();
|
|
||||||
return Container(
|
return Container(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
alignment: Alignment.bottomCenter,
|
alignment: Alignment.bottomCenter,
|
||||||
|
|
@ -441,10 +564,12 @@ class _PalRoomPageState extends State<PalRoomPage> {
|
||||||
color: Color.fromRGBO(201, 201, 201, 1), size: 25.0),
|
color: Color.fromRGBO(201, 201, 201, 1), size: 25.0),
|
||||||
),
|
),
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
|
// 表情图标
|
||||||
child: Icon(Icons.tag_faces,
|
child: Icon(Icons.tag_faces,
|
||||||
color: Color.fromRGBO(201, 201, 201, 1), size: 25.0),
|
color: Color.fromRGBO(201, 201, 201, 1), size: 25.0),
|
||||||
),
|
),
|
||||||
Container(
|
Container(
|
||||||
|
// 输入框
|
||||||
width: 220,
|
width: 220,
|
||||||
padding: EdgeInsets.symmetric(horizontal: 10),
|
padding: EdgeInsets.symmetric(horizontal: 10),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
|
|
@ -452,7 +577,30 @@ class _PalRoomPageState extends State<PalRoomPage> {
|
||||||
borderRadius: BorderRadius.circular(30),
|
borderRadius: BorderRadius.circular(30),
|
||||||
),
|
),
|
||||||
child: TextField(
|
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(
|
decoration: InputDecoration(
|
||||||
isDense: true,
|
isDense: true,
|
||||||
border: InputBorder.none,
|
border: InputBorder.none,
|
||||||
|
|
@ -466,10 +614,10 @@ class _PalRoomPageState extends State<PalRoomPage> {
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
),
|
),
|
||||||
textInputAction: TextInputAction.send, // 键盘右下角图标
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
|
// 加号图标
|
||||||
child: Icon(Icons.add,
|
child: Icon(Icons.add,
|
||||||
color: Color.fromRGBO(201, 201, 201, 1), size: 25.0),
|
color: Color.fromRGBO(201, 201, 201, 1), size: 25.0),
|
||||||
),
|
),
|
||||||
|
|
@ -483,6 +631,14 @@ class _PalRoomPageState extends State<PalRoomPage> {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
resizeToAvoidBottomInset: true,
|
resizeToAvoidBottomInset: true,
|
||||||
appBar: AppBar(
|
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轴阴影
|
elevation: 0, // z轴阴影
|
||||||
titleSpacing: 0, // 标题与其他控件的间隔
|
titleSpacing: 0, // 标题与其他控件的间隔
|
||||||
backgroundColor: Color.fromRGBO(27, 28, 48, 1),
|
backgroundColor: Color.fromRGBO(27, 28, 48, 1),
|
||||||
|
|
@ -499,7 +655,7 @@ class _PalRoomPageState extends State<PalRoomPage> {
|
||||||
padding: EdgeInsets.only(right: 5),
|
padding: EdgeInsets.only(right: 5),
|
||||||
child: ClipOval(
|
child: ClipOval(
|
||||||
child: Image.asset(
|
child: Image.asset(
|
||||||
'images/palRoom/photo.png',
|
'images/palRoom/user.png',
|
||||||
width: 46,
|
width: 46,
|
||||||
height: 46,
|
height: 46,
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -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<PalRoomPage> createState() => _PalRoomPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PalRoomPageState extends State<PalRoomPage> {
|
||||||
|
final _users = <int>[]; // 用户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<void> 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(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<PalRoomPage> createState() => _PalRoomPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PalRoomPageState extends State<PalRoomPage> {
|
||||||
|
// final _users = <int>[]; // 用户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<void> 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(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:agora_rtc_engine/rtc_engine.dart';
|
import 'package:agora_rtc_engine/rtc_engine.dart';
|
||||||
import 'package:permission_handler/permission_handler.dart';
|
import 'package:permission_handler/permission_handler.dart';
|
||||||
import 'package:will_play/views/palRoom/PalRoom.dart';
|
import 'package:will_play/views/palRoom/PalRoom.dart';
|
||||||
|
import 'package:will_play/utils/auth.dart';
|
||||||
|
|
||||||
import '../../utils/http_util.dart';
|
import '../../utils/http_util.dart';
|
||||||
|
import '../../api/part.dart';
|
||||||
|
|
||||||
class PartyPage extends StatefulWidget {
|
class PartyPage extends StatefulWidget {
|
||||||
PartyPage({Key? key}) : super(key: key);
|
PartyPage({Key? key}) : super(key: key);
|
||||||
|
|
@ -126,7 +127,7 @@ class _PartyPageState extends State<PartyPage> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// tab视图页
|
// tab视图页组件
|
||||||
class ViewsWidget extends StatefulWidget {
|
class ViewsWidget extends StatefulWidget {
|
||||||
ViewsWidget({Key? key}) : super(key: key);
|
ViewsWidget({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
|
@ -135,53 +136,6 @@ class ViewsWidget extends StatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ViewsWidgetState extends State<ViewsWidget> {
|
class _ViewsWidgetState extends State<ViewsWidget> {
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return SingleChildScrollView(
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
Banner(),
|
|
||||||
Lists(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 横幅组件
|
|
||||||
class Banner extends StatefulWidget {
|
|
||||||
Banner({Key? key}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<Banner> createState() => _BannerState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _BannerState extends State<Banner> {
|
|
||||||
@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<Lists> createState() => _ListsState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ListsState extends State<Lists> {
|
|
||||||
List _list = [
|
List _list = [
|
||||||
{
|
{
|
||||||
"photo": "images/party/photo1.png",
|
"photo": "images/party/photo1.png",
|
||||||
|
|
@ -201,6 +155,15 @@ class _ListsState extends State<Lists> {
|
||||||
"sex": "1",
|
"sex": "1",
|
||||||
"role": ClientRole.Audience,
|
"role": ClientRole.Audience,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"photo": "images/party/photo1.png",
|
||||||
|
"name": "小红",
|
||||||
|
"num": "12",
|
||||||
|
"title": "成年人的避风港",
|
||||||
|
"type": "pal",
|
||||||
|
"sex": "1",
|
||||||
|
"role": ClientRole.Audience,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"photo": "images/party/photo1.png",
|
"photo": "images/party/photo1.png",
|
||||||
"name": "小黑",
|
"name": "小黑",
|
||||||
|
|
@ -219,21 +182,23 @@ class _ListsState extends State<Lists> {
|
||||||
"sex": "0",
|
"sex": "0",
|
||||||
"role": ClientRole.Audience,
|
"role": ClientRole.Audience,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"photo": "images/party/photo1.png",
|
||||||
|
"name": "小白",
|
||||||
|
"num": "122",
|
||||||
|
"title": "今日听君歌一曲",
|
||||||
|
"type": "family",
|
||||||
|
"sex": "0",
|
||||||
|
"role": ClientRole.Audience,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
int page = 1; // 当前页面
|
||||||
@override
|
int size = 10; // 每页N条
|
||||||
void initState() {
|
late int total; // 总条数
|
||||||
super.initState();
|
bool isLoadmore = false; // 加载更多
|
||||||
_getList();
|
List _data = []; // 列表数据
|
||||||
}
|
// 给ListView加一个 ScrollController 组件,通过事件监听滚动条的高度来显示和隐藏加载更多的组件
|
||||||
|
ScrollController _scrollController = new ScrollController();
|
||||||
_getList() async {
|
|
||||||
var res = await MyHttpUtil().get(
|
|
||||||
"http://101.35.117.69:9093/chat/api/room/page",
|
|
||||||
);
|
|
||||||
print(res);
|
|
||||||
// _list = res.data['content'];
|
|
||||||
}
|
|
||||||
|
|
||||||
// 判断是否开启权限方法
|
// 判断是否开启权限方法
|
||||||
Future<void> _handleCameraAndMic(Permission permission) async {
|
Future<void> _handleCameraAndMic(Permission permission) async {
|
||||||
|
|
@ -245,7 +210,7 @@ class _ListsState extends State<Lists> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 页面跳转方法
|
// 页面跳转方法
|
||||||
Future<void> onJoin(userName, role) async {
|
Future<void> _onJoin(agoraToken, userName, channelName, role) async {
|
||||||
await _handleCameraAndMic(Permission.camera);
|
await _handleCameraAndMic(Permission.camera);
|
||||||
await _handleCameraAndMic(Permission.microphone);
|
await _handleCameraAndMic(Permission.microphone);
|
||||||
|
|
||||||
|
|
@ -253,18 +218,118 @@ class _ListsState extends State<Lists> {
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => PalRoomPage(
|
builder: (context) => PalRoomPage(
|
||||||
|
agoraToken: agoraToken,
|
||||||
userName: userName,
|
userName: userName,
|
||||||
channelName: 'call',
|
channelName: channelName,
|
||||||
role: role,
|
role: role,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Widget> _tempList() {
|
// 刷新列表方法
|
||||||
var tempList = _list.map((value) {
|
_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']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载数据方法
|
||||||
|
_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 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(
|
return GestureDetector(
|
||||||
onTap: () => onJoin(value['name'], value['role']),
|
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(
|
child: Container(
|
||||||
height: 75,
|
height: 75,
|
||||||
padding: EdgeInsets.all(5),
|
padding: EdgeInsets.all(5),
|
||||||
|
|
@ -286,7 +351,7 @@ class _ListsState extends State<Lists> {
|
||||||
children: [
|
children: [
|
||||||
ClipOval(
|
ClipOval(
|
||||||
child: Image.asset(
|
child: Image.asset(
|
||||||
value['photo'],
|
'images/party/photo1.png',
|
||||||
width: 65,
|
width: 65,
|
||||||
height: 65,
|
height: 65,
|
||||||
),
|
),
|
||||||
|
|
@ -301,12 +366,14 @@ class _ListsState extends State<Lists> {
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
typeElement(type: value['type']),
|
typeElement(type: 'pal'),
|
||||||
Container(
|
Container(
|
||||||
child: Text(
|
child: Text(
|
||||||
value['title'],
|
_data[index]['name'],
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Color.fromRGBO(51, 51, 51, 1),
|
color: Color.fromRGBO(
|
||||||
|
51, 51, 51, 1),
|
||||||
fontSize: 16),
|
fontSize: 16),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
@ -322,9 +389,11 @@ class _ListsState extends State<Lists> {
|
||||||
),
|
),
|
||||||
SizedBox(width: 5),
|
SizedBox(width: 5),
|
||||||
Text(
|
Text(
|
||||||
value["num"] + '人',
|
_data[index]['id'],
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Color.fromRGBO(153, 153, 153, 1),
|
color: Color.fromRGBO(
|
||||||
|
153, 153, 153, 1),
|
||||||
fontSize: 12),
|
fontSize: 12),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
|
@ -335,14 +404,16 @@ class _ListsState extends State<Lists> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
});
|
},
|
||||||
return tempList.toList();
|
)
|
||||||
}
|
: Container(
|
||||||
|
padding: EdgeInsets.all(15),
|
||||||
@override
|
child: Center(
|
||||||
Widget build(BuildContext context) {
|
child: Text('暂无数据~'),
|
||||||
return Column(
|
),
|
||||||
children: _tempList(),
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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<PartyPage> createState() => _PartyPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PartyPageState extends State<PartyPage> {
|
||||||
|
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<ViewsWidget> createState() => _ViewsWidgetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ViewsWidgetState extends State<ViewsWidget> {
|
||||||
|
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<void> _handleCameraAndMic(Permission permission) async {
|
||||||
|
PermissionStatus status = await permission.request();
|
||||||
|
print('权限状态$status');
|
||||||
|
if (!status.isGranted) {
|
||||||
|
openAppSettings();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页面跳转方法
|
||||||
|
Future<void> _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),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
14
pubspec.lock
14
pubspec.lock
|
|
@ -50,6 +50,13 @@ packages:
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.16.0"
|
version: "1.16.0"
|
||||||
|
crypto:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: crypto
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.2"
|
||||||
cupertino_icons:
|
cupertino_icons:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|
@ -378,6 +385,13 @@ packages:
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.2"
|
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:
|
win32:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@ dependencies:
|
||||||
fluttertoast: ^8.0.6
|
fluttertoast: ^8.0.6
|
||||||
shared_preferences: ^2.0.7
|
shared_preferences: ^2.0.7
|
||||||
pretty_dio_logger: ^1.1.1
|
pretty_dio_logger: ^1.1.1
|
||||||
|
web_socket_channel: ^2.1.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|
@ -105,6 +106,7 @@ flutter:
|
||||||
- images/palRoom/add.png
|
- images/palRoom/add.png
|
||||||
- images/palRoom/face.png
|
- images/palRoom/face.png
|
||||||
- images/palRoom/photo.png
|
- images/palRoom/photo.png
|
||||||
|
- images/palRoom/user.png
|
||||||
- images/palRoom/tip.png
|
- images/palRoom/tip.png
|
||||||
- images/palRoom/voice1.png
|
- images/palRoom/voice1.png
|
||||||
- images/palRoom/voice2.png
|
- images/palRoom/voice2.png
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue