自习室优化ok
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
import 'package:app/config/theme/base/app_theme_ext.dart';
|
||||
import 'package:app/pages/student/home/viewmodel/s_home_vm.dart';
|
||||
import 'package:app/request/api/room_api.dart';
|
||||
import 'package:app/request/dto/room/room_type_dto.dart';
|
||||
import 'package:app/widgets/version/version_dialog.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'widgets/tip_card.dart';
|
||||
|
||||
import 'today/s_today_card.dart';
|
||||
import 'widgets/user_header.dart';
|
||||
@@ -13,6 +13,7 @@ class SHomePage extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
showUpdateDialog(context);
|
||||
return ChangeNotifierProvider(
|
||||
create: (_) => SHomeVm(),
|
||||
child: _HomeView(),
|
||||
@@ -21,7 +22,7 @@ class SHomePage extends StatelessWidget {
|
||||
}
|
||||
|
||||
class _HomeView extends StatelessWidget {
|
||||
const _HomeView({super.key});
|
||||
const _HomeView();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -35,6 +36,8 @@ class _HomeView extends StatelessWidget {
|
||||
padding: EdgeInsets.all(context.pagePadding),
|
||||
children: [
|
||||
STodayCard(),
|
||||
TipCard1(),
|
||||
TipCard2()
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:app/config/theme/base/app_theme_ext.dart';
|
||||
import 'package:app/pages/student/home/viewmodel/s_home_vm.dart';
|
||||
import 'package:app/router/route_paths.dart';
|
||||
import 'package:app/utils/permission.dart';
|
||||
import 'package:app/utils/time.dart';
|
||||
import 'package:app/widgets/base/button/index.dart';
|
||||
import 'package:app/widgets/base/empty/index.dart';
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
@@ -102,7 +103,7 @@ class _STodayCardState extends State<STodayCard> {
|
||||
children: [
|
||||
Text(vm.roomInfo?.teacherName ?? ""),
|
||||
Text(
|
||||
vm.roomInfo?.teacherBackground ?? "",
|
||||
vm.roomInfo?.teacherSchoolName ?? "",
|
||||
style: Theme.of(context).textTheme.labelLarge,
|
||||
),
|
||||
],
|
||||
@@ -121,7 +122,7 @@ class _STodayCardState extends State<STodayCard> {
|
||||
),
|
||||
InfoItem(
|
||||
label: "自习时长",
|
||||
value: "${vm.roomMinutes} 分钟",
|
||||
value: "${formatSeconds(vm.roomMinutes * 60, 'hh小时mm分钟')} ",
|
||||
icon: RemixIcons.timer_line,
|
||||
color: context.success,
|
||||
),
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import 'package:app/request/api/room_api.dart';
|
||||
import 'package:app/request/dto/room/room_info_dto.dart';
|
||||
import 'package:app/request/dto/room/room_list_item_dto.dart';
|
||||
import 'package:app/utils/time.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
|
||||
class SHomeVm extends ChangeNotifier {
|
||||
RoomInfoDto? roomInfo;
|
||||
RoomListItemDto ? roomInfo;
|
||||
bool loading = true;
|
||||
|
||||
SHomeVm() {
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:remixicon/remixicon.dart';
|
||||
|
||||
class FeatureStatic extends StatelessWidget {
|
||||
const FeatureStatic({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final List<FeatureItem> items = [
|
||||
FeatureItem("视频陪学", "老师全程在线监督", RemixIcons.video_on_ai_line),
|
||||
FeatureItem("举手提问", "实时互动解答疑惑", RemixIcons.hand),
|
||||
FeatureItem("拍照题目", "快速上传问题截图", RemixIcons.camera_2_line),
|
||||
FeatureItem("文件共享", "支持PDF等多种格式", RemixIcons.upload_2_line),
|
||||
];
|
||||
return Container(
|
||||
margin: EdgeInsets.only(top: 15),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
spacing: 15,
|
||||
children: [
|
||||
Text("核心功能", style: TextStyle(fontSize: 18)),
|
||||
GridView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: NeverScrollableScrollPhysics(),
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 4,
|
||||
mainAxisSpacing: 15,
|
||||
crossAxisSpacing: 15,
|
||||
mainAxisExtent: 120
|
||||
),
|
||||
itemBuilder: (_, index) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white
|
||||
),
|
||||
);
|
||||
},
|
||||
itemCount: items.length,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class FeatureItem {
|
||||
final String title;
|
||||
final String desc;
|
||||
final IconData icon;
|
||||
|
||||
FeatureItem(this.title, this.desc, this.icon);
|
||||
}
|
||||
131
lib/pages/student/home/widgets/tip_card.dart
Normal file
131
lib/pages/student/home/widgets/tip_card.dart
Normal file
@@ -0,0 +1,131 @@
|
||||
import 'package:app/widgets/base/card/g_card.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:remixicon/remixicon.dart';
|
||||
|
||||
class TipCard1 extends StatelessWidget {
|
||||
const TipCard1({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final list = [
|
||||
{
|
||||
"icon": RemixIcons.video_on_line,
|
||||
"title": "视频陪学",
|
||||
"desc": "老师全程在线监督",
|
||||
},
|
||||
{
|
||||
"icon": RemixIcons.hand,
|
||||
"title": "举手提问",
|
||||
"desc": "实时互动解答疑惑",
|
||||
},
|
||||
{
|
||||
"icon": RemixIcons.camera_ai_line,
|
||||
"title": "拍照题目",
|
||||
"desc": "快速上传问题截图",
|
||||
},
|
||||
{
|
||||
"icon": RemixIcons.file_upload_line,
|
||||
"title": "文件共享",
|
||||
"desc": "支持PDF等多种格式",
|
||||
},
|
||||
];
|
||||
return Container(
|
||||
margin: EdgeInsets.only(top: 15),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
spacing: 10,
|
||||
children: [
|
||||
Text("功能特色"),
|
||||
Row(
|
||||
spacing: 10,
|
||||
children: list.map((t) {
|
||||
final item = t as dynamic;
|
||||
return Expanded(
|
||||
child: GCard(
|
||||
child: Column(
|
||||
spacing: 5,
|
||||
children: [
|
||||
Container(
|
||||
padding: EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).primaryColor.withValues(alpha: 0.2),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Icon(
|
||||
item['icon'],
|
||||
color: Theme.of(context).primaryColor,
|
||||
),
|
||||
),
|
||||
Text(item['title'],style: Theme.of(context).textTheme.bodySmall),
|
||||
Text(
|
||||
item['desc'],
|
||||
style: Theme.of(context).textTheme.labelMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class TipCard2 extends StatelessWidget {
|
||||
const TipCard2({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final tipList = [
|
||||
"请保持摄像头开启,确保学习状态可见",
|
||||
"遇到问题可随时举手向老师提问",
|
||||
"建议准备好学习资料,提高学习效率",
|
||||
"自习期间请保持安静,避免打扰他人",
|
||||
];
|
||||
return Container(
|
||||
margin: EdgeInsets.only(top: 15),
|
||||
padding: EdgeInsets.all(15),
|
||||
decoration: BoxDecoration(
|
||||
color: Color(0xfffffbeb),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(
|
||||
color: Color(0xfffee685),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
margin: EdgeInsets.only(bottom: 10),
|
||||
child: Text("温馨提示"),
|
||||
),
|
||||
ListView.separated(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemBuilder: (_, index) {
|
||||
return Row(
|
||||
spacing: 4,
|
||||
children: [
|
||||
Container(
|
||||
width: 5,
|
||||
height: 5,
|
||||
decoration: BoxDecoration(shape: BoxShape.circle, color: Colors.black),
|
||||
),
|
||||
Text(
|
||||
tipList[index],
|
||||
style: Theme.of(context).textTheme.labelLarge,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
separatorBuilder: (_, __) => SizedBox(height: 3),
|
||||
itemCount: tipList.length,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,13 @@
|
||||
import 'package:app/pages/student/room/viewmodel/stu_room_vm.dart';
|
||||
import 'package:app/widgets/room/file_drawer.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:remixicon/remixicon.dart';
|
||||
|
||||
import '../viewmodel/stu_room_vm.dart';
|
||||
|
||||
class BottomBar extends StatefulWidget {
|
||||
const BottomBar({super.key});
|
||||
final void Function()? onTap;
|
||||
|
||||
const BottomBar({super.key, this.onTap});
|
||||
|
||||
@override
|
||||
State<BottomBar> createState() => _BottomBarState();
|
||||
@@ -15,51 +16,77 @@ class BottomBar extends StatefulWidget {
|
||||
class _BottomBarState extends State<BottomBar> {
|
||||
///显示文件
|
||||
void _handShowFile() {
|
||||
showFileDialog(context);
|
||||
final vm = context.read<StuRoomVM>();
|
||||
if (vm.selfInfo == null) return;
|
||||
showFileDialog(
|
||||
context,
|
||||
files: vm.selfInfo!.filesList,
|
||||
onConfirm: (file) {
|
||||
vm.uploadFile(file);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final vm = context.watch<StuRoomVM>();
|
||||
|
||||
if (vm.roomInfo.roomStatus != 1) {
|
||||
return SizedBox();
|
||||
}
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Color(0xff232426),
|
||||
),
|
||||
height: 70,
|
||||
child: Consumer<StuRoomVM>(
|
||||
builder: (context,vm,_) {
|
||||
builder: (context, vm, _) {
|
||||
//摄像头开关
|
||||
return Row(
|
||||
children: [
|
||||
BarItem(
|
||||
title: "摄像头",
|
||||
icon: vm.cameraOpen ? RemixIcons.video_on_fill : RemixIcons.video_off_fill,
|
||||
isOff: !vm.cameraOpen,
|
||||
onTap: vm.changeCameraSwitch,
|
||||
icon: vm.cameraClose ? RemixIcons.video_off_fill : RemixIcons.video_on_fill,
|
||||
isOff: vm.cameraClose,
|
||||
onTap: () {
|
||||
vm.changeCameraSwitch(value: vm.cameraClose);
|
||||
},
|
||||
),
|
||||
BarItem(
|
||||
title: "麦克风",
|
||||
icon: vm.micOpen ? RemixIcons.mic_fill : RemixIcons.mic_off_fill,
|
||||
isOff: !vm.micOpen,
|
||||
onTap: vm.changeMicSwitch,
|
||||
icon: vm.micClose ? RemixIcons.mic_off_fill : RemixIcons.mic_fill,
|
||||
isOff: vm.micClose,
|
||||
onTap: () {
|
||||
vm.changeMicSwitch(value: vm.micClose);
|
||||
},
|
||||
),
|
||||
BarItem(
|
||||
title: "声音",
|
||||
icon: vm.speakerOpen ? RemixIcons.volume_up_fill : RemixIcons.volume_mute_fill,
|
||||
isOff: !vm.speakerOpen,
|
||||
onTap: vm.changeSpeakerSwitch,
|
||||
icon: vm.speakerClose
|
||||
? RemixIcons.volume_mute_fill
|
||||
: RemixIcons.volume_up_fill,
|
||||
isOff: vm.speakerClose,
|
||||
onTap: () {
|
||||
vm.changeSpeakerSwitch(value: vm.speakerClose);
|
||||
},
|
||||
),
|
||||
BarItem(
|
||||
title: "举手",
|
||||
icon: RemixIcons.hand,
|
||||
onTap: () {
|
||||
vm.changeHandSwitch();
|
||||
widget.onTap?.call();
|
||||
},
|
||||
),
|
||||
BarItem(
|
||||
title: "拍照",
|
||||
title: "上传",
|
||||
icon: RemixIcons.upload_2_fill,
|
||||
onTap: _handShowFile,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:app/utils/time.dart';
|
||||
import 'package:app/widgets/base/dialog/config_dialog.dart';
|
||||
import 'package:app/widgets/room/core/count_down_vm.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:remixicon/remixicon.dart';
|
||||
|
||||
import '../viewmodel/stu_room_vm.dart';
|
||||
|
||||
class TopBar extends StatefulWidget implements PreferredSizeWidget {
|
||||
class TopBar extends StatelessWidget implements PreferredSizeWidget {
|
||||
final bool showOther;
|
||||
final void Function()? onOther;
|
||||
|
||||
@@ -17,67 +16,63 @@ class TopBar extends StatefulWidget implements PreferredSizeWidget {
|
||||
this.onOther,
|
||||
});
|
||||
|
||||
@override
|
||||
State<TopBar> createState() => _TopBarState();
|
||||
|
||||
@override
|
||||
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
|
||||
}
|
||||
|
||||
class _TopBarState extends State<TopBar> {
|
||||
Timer? _timer;
|
||||
int seconds = 0;
|
||||
late DateTime startTime;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final vm = context.read<StuRoomVM>();
|
||||
startTime = parseTime(vm.roomInfo.startTime);
|
||||
|
||||
_timer = Timer.periodic(const Duration(seconds: 1), (_) {
|
||||
final diff = DateTime.now().difference(startTime).inSeconds;
|
||||
setState(() {
|
||||
seconds = diff < 0 ? 0 : diff;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// 你若想外面主动停,可以暴露这个方法
|
||||
void stopTimer() {
|
||||
_timer?.cancel();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_timer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final vm = context.read<StuRoomVM>();
|
||||
|
||||
return AppBar(
|
||||
foregroundColor: Colors.white,
|
||||
titleTextStyle: const TextStyle(color: Colors.white, fontSize: 18),
|
||||
backgroundColor: const Color(0xff232426),
|
||||
centerTitle: true,
|
||||
title: Column(
|
||||
children: [
|
||||
Text(vm.roomInfo.roomName),
|
||||
Text(
|
||||
formatSeconds(seconds),
|
||||
style: const TextStyle(fontSize: 12, color: Colors.white24),
|
||||
leadingWidth: 100,
|
||||
leading: Container(
|
||||
padding: EdgeInsets.only(left: 10),
|
||||
alignment: Alignment.centerLeft,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (_) {
|
||||
return ConfigDialog(
|
||||
content: "请确认是否退出自习室",
|
||||
onCancel: () {
|
||||
context.pop();
|
||||
},
|
||||
onConfirm: () {
|
||||
context.pop();
|
||||
context.pop();
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
child: Text(
|
||||
"退出自习室",
|
||||
style: TextStyle(color: Colors.red),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
title: Consumer<CountDownVM>(
|
||||
builder: (context, vm, _) {
|
||||
return Column(
|
||||
children: [
|
||||
Text(vm.roomInfo!.roomName),
|
||||
Text(
|
||||
formatSeconds(vm.studyTime),
|
||||
style: const TextStyle(fontSize: 12, color: Colors.white24),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
onPressed: widget.onOther,
|
||||
icon: Icon(widget.showOther ? RemixIcons.team_fill : RemixIcons.team_line),
|
||||
onPressed: onOther,
|
||||
icon: Icon(showOther ? RemixIcons.team_fill : RemixIcons.team_line),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
|
||||
}
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
import 'package:app/pages/student/room/viewmodel/stu_room_vm.dart';
|
||||
import 'package:app/pages/student/room/widgets/status_view.dart';
|
||||
import 'package:app/providers/user_store.dart';
|
||||
import 'package:app/request/dto/room/room_info_dto.dart';
|
||||
import 'package:app/request/dto/room/room_list_item_dto.dart';
|
||||
import 'package:app/widgets/base/transition/slide_hide.dart';
|
||||
import 'package:app/widgets/room/core/count_down_vm.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'controls/bottom_bar.dart';
|
||||
import 'controls/top_bar.dart';
|
||||
import 'video/student_video_list.dart';
|
||||
import 'video/teacher_video.dart';
|
||||
import 'viewmodel/stu_room_vm.dart';
|
||||
|
||||
class SRoomPage extends StatefulWidget {
|
||||
final RoomInfoDto roomInfo;
|
||||
final RoomListItemDto roomInfo;
|
||||
|
||||
const SRoomPage({super.key, required this.roomInfo});
|
||||
|
||||
@@ -36,29 +37,40 @@ class _SRoomPageState extends State<SRoomPage> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
UserStore userStore = context.read<UserStore>();
|
||||
return ChangeNotifierProvider<StuRoomVM>(
|
||||
create: (_) => StuRoomVM(
|
||||
roomInfo: widget.roomInfo,
|
||||
uid: userStore.userInfo!.id,
|
||||
),
|
||||
return MultiProvider(
|
||||
providers: [
|
||||
ChangeNotifierProvider<StuRoomVM>(
|
||||
create: (_) => StuRoomVM(
|
||||
info: widget.roomInfo,
|
||||
uid: userStore.userInfo!.id,
|
||||
),
|
||||
),
|
||||
ChangeNotifierProxyProvider<StuRoomVM, CountDownVM>(
|
||||
create: (_) => CountDownVM(),
|
||||
update: (_, stuVM, countDownVM) {
|
||||
countDownVM!.bind(stuVM.roomInfo);
|
||||
return countDownVM;
|
||||
},
|
||||
),
|
||||
],
|
||||
child: Scaffold(
|
||||
body: Stack(
|
||||
children: [
|
||||
//底部控制显示
|
||||
GestureDetector(
|
||||
onTap: _toggleOverlay,
|
||||
child: Container(color: Color(0xff2c3032)),
|
||||
),
|
||||
//老师视频画面
|
||||
TeacherVideo(),
|
||||
|
||||
StatusView(),
|
||||
//其他学生
|
||||
Positioned(
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
child: Visibility(
|
||||
visible: _showOtherStudent,
|
||||
child: StudentVideoList(),
|
||||
child: IgnorePointer(
|
||||
child: Visibility(
|
||||
visible: _showOtherStudent,
|
||||
child: StudentVideoList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -87,7 +99,9 @@ class _SRoomPageState extends State<SRoomPage> {
|
||||
child: SlideHide(
|
||||
direction: SlideDirection.down,
|
||||
hide: !_controlsVisible,
|
||||
child: BottomBar(),
|
||||
child: BottomBar(
|
||||
onTap: _toggleOverlay,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import 'package:agora_rtc_engine/agora_rtc_engine.dart';
|
||||
import 'package:app/pages/student/room/viewmodel/stu_room_vm.dart';
|
||||
import 'package:app/widgets/room/video_surface.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
@@ -8,6 +10,9 @@ class StudentVideoList extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final vm = context.watch<StuRoomVM>();
|
||||
if (vm.roomInfo.roomStatus != 1) {
|
||||
return SizedBox();
|
||||
}
|
||||
return SafeArea(
|
||||
child: Container(
|
||||
width: 250,
|
||||
@@ -26,6 +31,17 @@ class StudentVideoList extends StatelessWidget {
|
||||
color: Color(0xff373c3e),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: VideoSurface(
|
||||
user: item,
|
||||
child: AgoraVideoView(
|
||||
controller: VideoViewController(
|
||||
rtcEngine: vm.engine!,
|
||||
canvas: VideoCanvas(
|
||||
uid: item.rtcUid,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
|
||||
@@ -1,60 +1,83 @@
|
||||
import 'package:agora_rtc_engine/agora_rtc_engine.dart';
|
||||
import 'package:app/widgets/base/dialog/config_dialog.dart';
|
||||
import 'package:app/widgets/room/other_widget.dart';
|
||||
import 'package:app/widgets/room/video_surface.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../viewmodel/stu_room_vm.dart';
|
||||
|
||||
class TeacherVideo extends StatefulWidget {
|
||||
class TeacherVideo extends StatelessWidget {
|
||||
const TeacherVideo({super.key});
|
||||
|
||||
@override
|
||||
State<TeacherVideo> createState() => _TeacherVideoState();
|
||||
}
|
||||
|
||||
class _TeacherVideoState extends State<TeacherVideo> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final vm = context.read<StuRoomVM>();
|
||||
final vm = context.watch<StuRoomVM>();
|
||||
final teacherInfo = vm.teacherInfo;
|
||||
|
||||
///没开始
|
||||
if (vm.roomStatus == 0) {
|
||||
return _empty("自习室还没开始");
|
||||
}
|
||||
|
||||
///开始
|
||||
if (vm.roomStatus == 1 && vm.engine != null) {
|
||||
if (teacherInfo == null) {
|
||||
return _empty("老师不在自习室内");
|
||||
}
|
||||
if (teacherInfo.online == 0) {
|
||||
return _empty("老师掉线了,请耐心等待");
|
||||
}
|
||||
return AgoraVideoView(
|
||||
controller: VideoViewController(
|
||||
rtcEngine: vm.engine!,
|
||||
canvas: VideoCanvas(
|
||||
uid: vm.teacherInfo!.userId,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
///结束
|
||||
if (vm.roomStatus == 2) {
|
||||
return _empty("自习室已结束");
|
||||
}
|
||||
return _empty("加载中");
|
||||
}
|
||||
|
||||
Widget _empty(String title) {
|
||||
return SafeArea(
|
||||
child: Align(
|
||||
child: Text(
|
||||
title,
|
||||
style: TextStyle(color: Colors.white),
|
||||
return PopScope(
|
||||
canPop: false,
|
||||
onPopInvokedWithResult: (didPop, _) {
|
||||
if (!didPop) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return ConfigDialog(
|
||||
content: "是否退出自习室",
|
||||
onCancel: () {
|
||||
context.pop();
|
||||
},
|
||||
onConfirm: () {
|
||||
context.pop();
|
||||
context.pop();
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
child: IgnorePointer(
|
||||
child: Stack(
|
||||
alignment: Alignment.bottomCenter,
|
||||
children: [
|
||||
VideoSurface(
|
||||
user: teacherInfo!,
|
||||
child: AgoraVideoView(
|
||||
controller: VideoViewController(
|
||||
rtcEngine: vm.engine!,
|
||||
canvas: VideoCanvas(
|
||||
uid: teacherInfo.rtcUid,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 0,
|
||||
left: 0,
|
||||
child: Container(
|
||||
width: 150,
|
||||
color: Colors.black,
|
||||
child: AspectRatio(
|
||||
aspectRatio: 1 / 1.2,
|
||||
child: AgoraVideoView(
|
||||
controller: VideoViewController(
|
||||
rtcEngine: vm.engine!,
|
||||
canvas: const VideoCanvas(uid: 0),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (vm.selfInfo?.handup == 1)
|
||||
Positioned(
|
||||
bottom: 60,
|
||||
child: HandRaiseButton(
|
||||
onTap: vm.changeHandSwitch,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,24 +2,23 @@ import 'dart:async';
|
||||
|
||||
import 'package:agora_rtc_engine/agora_rtc_engine.dart';
|
||||
import 'package:app/config/config.dart';
|
||||
import 'package:app/providers/user_store.dart';
|
||||
import 'package:app/data/models/meeting_room_dto.dart';
|
||||
import 'package:app/request/dto/room/room_list_item_dto.dart';
|
||||
import 'package:app/request/dto/room/room_info_dto.dart';
|
||||
import 'package:app/request/dto/room/room_type_dto.dart';
|
||||
import 'package:app/request/dto/room/room_user_dto.dart';
|
||||
import 'package:app/request/dto/room/rtc_token_dto.dart';
|
||||
import 'package:app/request/websocket/room_protocol.dart';
|
||||
import 'package:app/request/websocket/room_websocket.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||
import 'package:logger/logger.dart';
|
||||
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||
|
||||
Logger log = Logger();
|
||||
|
||||
class StuRoomVM extends ChangeNotifier {
|
||||
///房间信息
|
||||
final RoomInfoDto roomInfo;
|
||||
|
||||
///房间开启状态,0没开始,1进行中,2已结束
|
||||
int roomStatus = 0;
|
||||
///房间信息,状态0没开始,1进行中,2已结束
|
||||
late MeetingRoomDto roomInfo;
|
||||
|
||||
///其他学生列表,老师信息,自己信息
|
||||
int uid;
|
||||
@@ -27,12 +26,18 @@ class StuRoomVM extends ChangeNotifier {
|
||||
RoomUserDto? teacherInfo;
|
||||
RoomUserDto? selfInfo;
|
||||
|
||||
///本人的摄像头、麦克风、扬声器状态是否打开了
|
||||
bool get cameraOpen => selfInfo?.cameraStatus == 1;
|
||||
// ///老师是否发送请求过来了,0关闭,1摄像头,2麦克风
|
||||
// bool cameraReq = false;
|
||||
// bool micReq = false;
|
||||
|
||||
bool get micOpen => selfInfo?.microphoneStatus == 1;
|
||||
///本人的摄像头、麦克风、扬声器、举手状态是否关闭了
|
||||
bool get cameraClose => selfInfo?.cameraStatus == 0;
|
||||
|
||||
bool get speakerOpen => selfInfo?.speekerStatus == 1;
|
||||
bool get micClose => selfInfo?.microphoneStatus == 0;
|
||||
|
||||
bool get speakerClose => selfInfo?.speekerStatus == 0;
|
||||
|
||||
bool get handClose => selfInfo?.handup == 0;
|
||||
|
||||
///ws管理
|
||||
final RoomWebSocket _ws = RoomWebSocket();
|
||||
@@ -43,12 +48,14 @@ class StuRoomVM extends ChangeNotifier {
|
||||
|
||||
RtcEngine? get engine => _engine;
|
||||
|
||||
StuRoomVM({required this.roomInfo, required this.uid}) {
|
||||
StuRoomVM({required RoomListItemDto info, required this.uid}) {
|
||||
roomInfo = MeetingRoomDto.fromRoomListItem(info);
|
||||
_startRoom();
|
||||
}
|
||||
|
||||
///初始化声网
|
||||
Future<void> _initRtc() async {
|
||||
if (_engine != null) return;
|
||||
_engine = createAgoraRtcEngine();
|
||||
//初始化 RtcEngine,设置频道场景为 channelProfileLiveBroadcasting(直播场景)
|
||||
await _engine!.initialize(
|
||||
@@ -57,27 +64,34 @@ class StuRoomVM extends ChangeNotifier {
|
||||
channelProfile: ChannelProfileType.channelProfileCommunication,
|
||||
),
|
||||
);
|
||||
//启动视频模块
|
||||
_engine!.getUserInfoByUid(1);
|
||||
// 启用视频模块
|
||||
await _engine!.enableVideo();
|
||||
//加入频道
|
||||
await _engine!.joinChannel(
|
||||
token: _ws.rtcToken!.token,
|
||||
channelId: _ws.rtcToken!.channel,
|
||||
uid: uid,
|
||||
// uid: _ws.rtcToken!.uid,
|
||||
options: ChannelMediaOptions(
|
||||
// 自动订阅所有视频流
|
||||
autoSubscribeVideo: true,
|
||||
// 自动订阅所有音频流
|
||||
autoSubscribeAudio: true,
|
||||
// 发布摄像头采集的视频
|
||||
publishCameraTrack: true,
|
||||
// 发布麦克风采集的音频
|
||||
publishMicrophoneTrack: true,
|
||||
// 设置用户角色为 clientRoleBroadcaster(主播)或 clientRoleAudience(观众)
|
||||
clientRoleType: ClientRoleType.clientRoleBroadcaster,
|
||||
),
|
||||
);
|
||||
// 开启本地预览
|
||||
await _engine!.startPreview();
|
||||
|
||||
WakelockPlus.enable();
|
||||
final status = await _engine!.getConnectionState();
|
||||
if (status == ConnectionStateType.connectionStateDisconnected) {
|
||||
//加入频道
|
||||
await _engine!.joinChannel(
|
||||
token: _ws.rtcToken!.token,
|
||||
channelId: _ws.rtcToken!.channel,
|
||||
uid: _ws.rtcToken!.uid,
|
||||
options: ChannelMediaOptions(
|
||||
// 自动订阅所有视频流
|
||||
autoSubscribeVideo: true,
|
||||
// 自动订阅所有音频流
|
||||
autoSubscribeAudio: true,
|
||||
// 发布摄像头采集的视频
|
||||
publishCameraTrack: true,
|
||||
// 发布麦克风采集的音频
|
||||
publishMicrophoneTrack: true,
|
||||
// 设置用户角色为 clientRoleBroadcaster(主播)或 clientRoleAudience(观众)
|
||||
clientRoleType: ClientRoleType.clientRoleBroadcaster,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
///开始链接房间
|
||||
@@ -94,7 +108,30 @@ class StuRoomVM extends ChangeNotifier {
|
||||
if (msg.event == RoomEvent.changeUser) {
|
||||
final list = RoomUserDto.listFromJson(msg.data['user_list']);
|
||||
onStudentChange(list);
|
||||
onRoomStartStatus(RoomTypeDto.fromJson(msg.data['room_info']));
|
||||
onRoomStartStatus(RoomInfoDto.fromJson(msg.data['room_info']));
|
||||
} else if (msg.event == RoomEvent.closeStudentCamera) {
|
||||
changeCameraSwitch(fromServer: false, value: false);
|
||||
} else if (msg.event == RoomEvent.closeStudentMic) {
|
||||
changeMicSwitch(fromServer: false, value: false);
|
||||
} else if ([
|
||||
RoomEvent.closeStudentSpeaker,
|
||||
RoomEvent.openStudentSpeaker,
|
||||
].contains(msg.event)) {
|
||||
//控制扬声器
|
||||
changeSpeakerSwitch(
|
||||
value: msg.event == RoomEvent.openStudentSpeaker,
|
||||
fromServer: false,
|
||||
);
|
||||
} else if (msg.event == RoomEvent.openStudentMic) {
|
||||
EasyLoading.showToast("老师请求打开麦克风");
|
||||
// 打开麦克风
|
||||
} else if (msg.event == RoomEvent.openStudentCamera) {
|
||||
EasyLoading.showToast("老师请求打开摄像头");
|
||||
// 打开摄像头
|
||||
} else if (msg.event == RoomEvent.clearHandUp) {
|
||||
changeHandSwitch();
|
||||
} else if (msg.event == RoomEvent.closeRoom) {
|
||||
_closeRoom();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -112,6 +149,10 @@ class StuRoomVM extends ChangeNotifier {
|
||||
newList.add(t);
|
||||
} else {
|
||||
selfInfo = t;
|
||||
//同步声网的状态
|
||||
changeCameraSwitch(value: selfInfo!.cameraStatus == 1, fromServer: false);
|
||||
changeMicSwitch(value: selfInfo!.microphoneStatus == 1, fromServer: false);
|
||||
changeSpeakerSwitch(value: selfInfo!.speekerStatus == 1, fromServer: false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -120,52 +161,111 @@ class StuRoomVM extends ChangeNotifier {
|
||||
}
|
||||
|
||||
///设置房间开启状态
|
||||
void onRoomStartStatus(RoomTypeDto roomInfo) {
|
||||
roomStatus = roomInfo.roomStatus;
|
||||
void onRoomStartStatus(RoomInfoDto info) {
|
||||
roomInfo = roomInfo.copyWith(
|
||||
roomStatus: info.roomStatus,
|
||||
actualStartTime: info.roomStartTime,
|
||||
boardUuid: info.boardUuid,
|
||||
);
|
||||
//开启摄像头
|
||||
if (roomInfo.roomStatus == 1) {
|
||||
_initRtc();
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
///控制摄像头开关
|
||||
void changeCameraSwitch() {
|
||||
bool isOpen = selfInfo!.cameraStatus == 1;
|
||||
selfInfo!.cameraStatus = isOpen ? 0 : 1;
|
||||
//发送指令
|
||||
_ws.send(RoomCommand.studentActon, {
|
||||
"mute_type": "camera",
|
||||
"is_mute": isOpen ? 1 : 0,
|
||||
});
|
||||
/// - [value] 摄像头状态,true为开启,false为关闭
|
||||
/// - [fromServer] 发送指令给服务器,默认true
|
||||
void changeCameraSwitch({
|
||||
required bool value,
|
||||
bool fromServer = true,
|
||||
}) {
|
||||
//改变后的操作状态,true表示开,false关
|
||||
selfInfo!.cameraStatus = value ? 1 : 0;
|
||||
// //发送指令
|
||||
if (fromServer) {
|
||||
_ws.send(RoomCommand.studentActon, {
|
||||
"mute_type": "camera",
|
||||
"is_mute": value ? 0 : 1,
|
||||
});
|
||||
}
|
||||
_engine?.enableLocalVideo(value);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
///控制麦克风开关
|
||||
void changeMicSwitch() {
|
||||
bool isOpen = selfInfo!.microphoneStatus == 1;
|
||||
selfInfo!.microphoneStatus = isOpen ? 0 : 1;
|
||||
print(selfInfo!.microphoneStatus);
|
||||
/// - [value] 麦克风状态,true为开启,false为关闭
|
||||
/// - [fromServer] 默认为true,发送指令给服务器
|
||||
void changeMicSwitch({required bool value, bool fromServer = true}) {
|
||||
selfInfo!.microphoneStatus = value ? 1 : 0;
|
||||
//发送指令
|
||||
_ws.send(RoomCommand.studentActon, {
|
||||
"mute_type": "microphone",
|
||||
"is_mute": isOpen ? 1 : 0,
|
||||
});
|
||||
if (fromServer) {
|
||||
_ws.send(RoomCommand.studentActon, {
|
||||
"mute_type": "microphone",
|
||||
"is_mute": value ? 0 : 1,
|
||||
});
|
||||
}
|
||||
_engine?.enableLocalAudio(value);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 控制扬声器开关
|
||||
void changeSpeakerSwitch() {
|
||||
bool isOpen = selfInfo!.speekerStatus == 1;
|
||||
selfInfo!.speekerStatus = isOpen ? 0 : 1;
|
||||
/// - [value] 扬声器状态,true为开启,false为关闭
|
||||
/// - [fromServer] 默认为true,发送指令给服务器
|
||||
void changeSpeakerSwitch({required bool value, bool fromServer = true}) {
|
||||
//操作后是否是开启状态
|
||||
selfInfo!.speekerStatus = value ? 1 : 0;
|
||||
//发送指令
|
||||
_ws.send(RoomCommand.studentActon, {
|
||||
"mute_type": "speeker",
|
||||
"is_mute": isOpen ? 1 : 0,
|
||||
});
|
||||
if (fromServer) {
|
||||
_ws.send(RoomCommand.studentActon, {
|
||||
"mute_type": "speeker",
|
||||
"is_mute": value ? 0 : 1,
|
||||
});
|
||||
}
|
||||
_engine?.muteAllRemoteAudioStreams(!value);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
///控制举手
|
||||
void changeHandSwitch({bool fromServer = true}) {
|
||||
bool nextOpen = handClose;
|
||||
selfInfo!.handup = nextOpen ? 1 : 0;
|
||||
if (fromServer) {
|
||||
_ws.send(RoomCommand.handUp, {
|
||||
'is_handup': nextOpen ? 1 : 0,
|
||||
});
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
///上传文件
|
||||
void uploadFile(List<String> files) {
|
||||
selfInfo?.filesList.addAll(files);
|
||||
_ws.send(RoomCommand.uploadFile, {
|
||||
"files": selfInfo!.filesList,
|
||||
});
|
||||
}
|
||||
|
||||
///自习室关闭
|
||||
void _closeRoom() {
|
||||
roomInfo.roomStatus = 2;
|
||||
_dispose();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
///销毁
|
||||
void _dispose() {
|
||||
_engine?.leaveChannel();
|
||||
_engine?.release();
|
||||
_sub?.cancel();
|
||||
_ws.dispose();
|
||||
WakelockPlus.disable();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
_sub?.cancel();
|
||||
_ws.dispose();
|
||||
_dispose();
|
||||
}
|
||||
}
|
||||
|
||||
99
lib/pages/student/room/widgets/status_view.dart
Normal file
99
lib/pages/student/room/widgets/status_view.dart
Normal file
@@ -0,0 +1,99 @@
|
||||
import 'package:app/utils/time.dart';
|
||||
import 'package:app/widgets/base/button/index.dart';
|
||||
import 'package:app/widgets/room/core/count_down_vm.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../video/teacher_video.dart';
|
||||
import '../viewmodel/stu_room_vm.dart';
|
||||
|
||||
class StatusView extends StatelessWidget {
|
||||
const StatusView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final vm = context.watch<StuRoomVM>();
|
||||
final teacherInfo = vm.teacherInfo;
|
||||
|
||||
///没开始
|
||||
if (vm.roomInfo.roomStatus == 0) {
|
||||
return Consumer<CountDownVM>(
|
||||
builder: (_, countVM, __) {
|
||||
if (countVM.canEnterRoom) {
|
||||
return _empty("等待老师进入自习室");
|
||||
} else {
|
||||
countVM.startStartCountdown();
|
||||
return Align(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Text(
|
||||
"未到开播时间",
|
||||
style: TextStyle(color: Colors.white),
|
||||
),
|
||||
Container(
|
||||
margin: const EdgeInsets.symmetric(vertical: 10),
|
||||
child: Text(
|
||||
formatSeconds(countVM.startCountDown),
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 26,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
///结束
|
||||
if (vm.roomInfo.roomStatus == 2) {
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
spacing: 5,
|
||||
children: [
|
||||
_empty("自习室已结束"),
|
||||
SizedBox(
|
||||
width: 120,
|
||||
child: Button(
|
||||
text: "返回首页",
|
||||
onPressed: () {
|
||||
context.pop();
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
///开始
|
||||
if (vm.roomInfo.roomStatus == 1 && vm.engine != null) {
|
||||
if (teacherInfo == null) {
|
||||
return _empty("老师不在自习室内");
|
||||
}
|
||||
if (teacherInfo.online == 0) {
|
||||
return _empty("老师暂时离开,请耐心等待");
|
||||
}
|
||||
return TeacherVideo();
|
||||
}
|
||||
|
||||
return _empty("加载中");
|
||||
}
|
||||
|
||||
Widget _empty(String title) {
|
||||
return SafeArea(
|
||||
child: Align(
|
||||
child: Text(
|
||||
title,
|
||||
style: TextStyle(color: Colors.white),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
import 'package:app/config/theme/base/app_theme_ext.dart';
|
||||
import 'package:app/widgets/version/version_dialog.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'viewmodel/home_view_model.dart';
|
||||
import 'widgets/header.dart';
|
||||
import 'widgets/tip_card.dart';
|
||||
import 'widgets/today_card.dart';
|
||||
|
||||
class THomePage extends StatelessWidget {
|
||||
@@ -11,6 +13,7 @@ class THomePage extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
showUpdateDialog(context);
|
||||
return ChangeNotifierProvider(
|
||||
create: (_) => HomeViewModel(),
|
||||
child: const _HomeView(),
|
||||
@@ -24,6 +27,7 @@ class _HomeView extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final vm = context.read<HomeViewModel>();
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Theme.of(context).colorScheme.surfaceContainer,
|
||||
appBar: Header(),
|
||||
@@ -36,6 +40,8 @@ class _HomeView extends StatelessWidget {
|
||||
),
|
||||
children: [
|
||||
TodayCard(),
|
||||
TipCard1(),
|
||||
TipCard2(),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import 'package:app/request/api/room_api.dart';
|
||||
import 'package:app/request/dto/room/room_info_dto.dart';
|
||||
import 'package:app/request/dto/room/room_list_item_dto.dart';
|
||||
import 'package:app/utils/time.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class HomeViewModel extends ChangeNotifier {
|
||||
RoomInfoDto? roomInfo;
|
||||
RoomListItemDto ? roomInfo;
|
||||
bool loading = true;
|
||||
|
||||
HomeViewModel() {
|
||||
|
||||
143
lib/pages/teacher/home/widgets/tip_card.dart
Normal file
143
lib/pages/teacher/home/widgets/tip_card.dart
Normal file
@@ -0,0 +1,143 @@
|
||||
import 'package:app/widgets/base/card/g_card.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:remixicon/remixicon.dart';
|
||||
|
||||
class TipCard1 extends StatelessWidget {
|
||||
const TipCard1({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final list = [
|
||||
{
|
||||
"icon": RemixIcons.video_on_line,
|
||||
"title": "实时视频互动",
|
||||
"subtitle": "高清视频连接,随时与学生面对面交流",
|
||||
},
|
||||
{
|
||||
"icon": RemixIcons.file_list_line,
|
||||
"title": "查看学生资料",
|
||||
"subtitle": "查看学生上传的作业、题目和笔记",
|
||||
},
|
||||
{
|
||||
"icon": RemixIcons.message_line,
|
||||
"title": "灵活管控",
|
||||
"subtitle": "一键控制学生的视频、音频状态",
|
||||
},
|
||||
{
|
||||
"icon": RemixIcons.lightbulb_line,
|
||||
"title": "白板演示",
|
||||
"subtitle": "开启白板功能,为学生讲解疑难问题",
|
||||
},
|
||||
];
|
||||
return Container(
|
||||
margin: EdgeInsets.only(top: 15),
|
||||
child: Column(
|
||||
spacing: 10,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text("核心功能"),
|
||||
GridView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 2,
|
||||
mainAxisExtent: 80,
|
||||
crossAxisSpacing: 15,
|
||||
mainAxisSpacing: 15,
|
||||
),
|
||||
itemBuilder: (_, index) {
|
||||
final item = list[index] as dynamic;
|
||||
return GCard(
|
||||
child: Row(
|
||||
spacing: 10,
|
||||
children: [
|
||||
Container(
|
||||
width: 50,
|
||||
height: 50,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).primaryColor,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Icon(
|
||||
item["icon"],
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(item["title"]),
|
||||
Text(
|
||||
item["subtitle"],
|
||||
style: Theme.of(context).textTheme.labelLarge,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
itemCount: list.length,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class TipCard2 extends StatelessWidget {
|
||||
const TipCard2({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final tipList = [
|
||||
"请确保网络环境良好,保证视频通话质量",
|
||||
"建议提前5分钟进入自习室,准备教学材料",
|
||||
"合理使用白板功能,帮助学生更好地理解知识点",
|
||||
"关注每位学生的学习状态,及时提供帮助",
|
||||
];
|
||||
return Container(
|
||||
margin: EdgeInsets.only(top: 15),
|
||||
padding: EdgeInsets.all(15),
|
||||
decoration: BoxDecoration(
|
||||
color: Color(0xfffffbeb),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(
|
||||
color: Color(0xfffee685),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
margin: EdgeInsets.only(bottom: 10),
|
||||
child: Text("温馨提示"),
|
||||
),
|
||||
ListView.separated(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemBuilder: (_, index) {
|
||||
return Row(
|
||||
spacing: 4,
|
||||
children: [
|
||||
Container(
|
||||
width: 5,
|
||||
height: 5,
|
||||
decoration: BoxDecoration(shape: BoxShape.circle, color: Colors.black),
|
||||
),
|
||||
Text(
|
||||
tipList[index],
|
||||
style: Theme.of(context).textTheme.labelLarge,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
separatorBuilder: (_, __) => SizedBox(height: 3),
|
||||
itemCount: tipList.length,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,10 @@
|
||||
import 'package:app/router/route_paths.dart';
|
||||
import 'package:app/utils/permission.dart';
|
||||
import 'package:app/utils/time.dart';
|
||||
import 'package:app/widgets/base/button/index.dart';
|
||||
import 'package:app/widgets/base/card/g_card.dart';
|
||||
import 'package:app/widgets/base/config/config.dart';
|
||||
import 'package:app/widgets/base/dialog/config_dialog.dart';
|
||||
import 'package:app/widgets/base/empty/index.dart';
|
||||
import 'package:app/widgets/room/file_drawer.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
@@ -93,7 +92,7 @@ class _TodayCardState extends State<TodayCard> {
|
||||
),
|
||||
_item(
|
||||
title: "时长",
|
||||
value: "${vm.roomMinutes} 分钟",
|
||||
value: "${formatSeconds(vm.roomMinutes * 60, 'hh小时mm分钟')} ",
|
||||
icon: RemixIcons.book_open_line,
|
||||
color: Color(0xffac45fd),
|
||||
),
|
||||
@@ -106,7 +105,7 @@ class _TodayCardState extends State<TodayCard> {
|
||||
child: Button(
|
||||
text: vm.canEnterRoom ? "开始自习室" : "未到开始时间",
|
||||
type: ThemeType.success,
|
||||
// disabled: !vm.canEnterRoom,
|
||||
disabled: !vm.canEnterRoom,
|
||||
onPressed: _goToRoom,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,43 +1,23 @@
|
||||
import 'package:app/utils/time.dart';
|
||||
import 'package:app/widgets/base/button/index.dart';
|
||||
import 'package:app/widgets/base/config/config.dart';
|
||||
import 'package:app/widgets/base/dialog/config_dialog.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:remixicon/remixicon.dart';
|
||||
|
||||
import '../../../../widgets/room/core/count_down_vm.dart';
|
||||
import '../viewmodel/tch_room_vm.dart';
|
||||
import '../viewmodel/type.dart';
|
||||
|
||||
class TopBar extends StatelessWidget implements PreferredSizeWidget {
|
||||
const TopBar({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
//标题子显示内容
|
||||
Widget infoItem({required String title, required IconData icon}) {
|
||||
return Row(
|
||||
spacing: 4,
|
||||
children: [
|
||||
Icon(icon, color: Colors.white54, size: 14),
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(fontSize: 12, color: Colors.white54),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
//操作按钮
|
||||
Widget actionButton({required IconData icon, required String title}) {
|
||||
return Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 10, vertical: 5),
|
||||
margin: EdgeInsets.only(right: 15),
|
||||
decoration: BoxDecoration(
|
||||
color: Color(0xff4a4f4f),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
spacing: 8,
|
||||
children: [
|
||||
Icon(icon, size: 16),
|
||||
Text(title, style: TextStyle(fontSize: 14)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
final vm = context.watch<TchRoomVM>();
|
||||
|
||||
return AppBar(
|
||||
backgroundColor: Color(0xff373c3e),
|
||||
@@ -46,29 +26,144 @@ class TopBar extends StatelessWidget implements PreferredSizeWidget {
|
||||
spacing: 5,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text("高三数学重置版", style: TextStyle(color: Colors.white, fontSize: 18)),
|
||||
Text(vm.roomInfo.roomName, style: TextStyle(color: Colors.white, fontSize: 18)),
|
||||
Row(
|
||||
spacing: 15,
|
||||
children: [
|
||||
infoItem(title: "剩余 1小时23分钟", icon: RemixIcons.time_line),
|
||||
infoItem(title: "8 名学生", icon: RemixIcons.group_line),
|
||||
Consumer<CountDownVM>(
|
||||
builder: (context, countVM, __) {
|
||||
return _infoItem(
|
||||
context,
|
||||
title: "剩余 ${formatSeconds(countVM.endCountDown)}",
|
||||
icon: RemixIcons.time_line,
|
||||
);
|
||||
},
|
||||
),
|
||||
_infoItem(
|
||||
context,
|
||||
title: "${vm.students.length} 名学生",
|
||||
icon: RemixIcons.group_line,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
actionButton(
|
||||
_actionButton(
|
||||
context,
|
||||
icon: RemixIcons.video_on_ai_line,
|
||||
title: "关闭全部",
|
||||
onPressed: () {
|
||||
_closeAll(context, StudentAction.camera);
|
||||
},
|
||||
),
|
||||
actionButton(
|
||||
_actionButton(
|
||||
context,
|
||||
icon: RemixIcons.volume_up_line,
|
||||
title: "全部静音",
|
||||
onPressed: () {
|
||||
_closeAll(context, StudentAction.speaker);
|
||||
},
|
||||
),
|
||||
Container(
|
||||
margin: EdgeInsets.only(right: 15),
|
||||
child: Button(
|
||||
text: "白板",
|
||||
textStyle: TextStyle(fontSize: 14),
|
||||
onPressed: (){},
|
||||
),
|
||||
),
|
||||
Consumer<TchRoomVM>(
|
||||
builder: (context, vm, _) {
|
||||
if (vm.roomInfo.roomStatus != 1) {
|
||||
return SizedBox();
|
||||
}
|
||||
return Button(
|
||||
type: ThemeType.danger,
|
||||
textStyle: TextStyle(fontSize: 14),
|
||||
text: "结束自习室",
|
||||
onPressed: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (_) {
|
||||
return ConfigDialog(
|
||||
content: '是否结束自习室?结束后无法在进入',
|
||||
onCancel: () {
|
||||
context.pop();
|
||||
},
|
||||
onConfirm: () {
|
||||
context.pop();
|
||||
vm.endRoom();
|
||||
EasyLoading.showToast("会议室已结束");
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
SizedBox(width: 10),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _infoItem(BuildContext context, {required String title, required IconData icon}) {
|
||||
return Row(
|
||||
children: [
|
||||
Icon(icon, color: Colors.white54, size: 14),
|
||||
SizedBox(width: 4),
|
||||
Text(title, style: TextStyle(fontSize: 12, color: Colors.white54)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _actionButton(
|
||||
BuildContext context, {
|
||||
required IconData icon,
|
||||
required String title,
|
||||
required VoidCallback onPressed,
|
||||
}) {
|
||||
return Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 10, vertical: 5),
|
||||
margin: EdgeInsets.only(right: 15),
|
||||
decoration: BoxDecoration(
|
||||
color: Color(0xff4a4f4f),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: InkWell(
|
||||
onTap: onPressed,
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(icon, size: 16),
|
||||
SizedBox(width: 8),
|
||||
Text(title, style: TextStyle(fontSize: 14)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _closeAll(BuildContext context, StudentAction action) {
|
||||
final vm = context.read<TchRoomVM>();
|
||||
String content = (action == StudentAction.camera) ? '是否关闭所有学生的摄像头?' : '是否关闭所有学生的扬声器?';
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (_) {
|
||||
return ConfigDialog(
|
||||
content: content,
|
||||
onCancel: () => context.pop(),
|
||||
onConfirm: () {
|
||||
context.pop();
|
||||
vm.closeAllStudentAction(action);
|
||||
EasyLoading.showToast("操作已完成");
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:app/request/dto/room/room_info_dto.dart';
|
||||
import 'package:app/widgets/room/core/count_down_vm.dart';
|
||||
import 'package:app/request/dto/room/room_list_item_dto.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'controls/top_bar.dart';
|
||||
@@ -6,7 +7,7 @@ import 'widgets/status_view.dart';
|
||||
import 'viewmodel/tch_room_vm.dart';
|
||||
|
||||
class TRoomPage extends StatefulWidget {
|
||||
final RoomInfoDto roomInfo;
|
||||
final RoomListItemDto roomInfo;
|
||||
|
||||
const TRoomPage({
|
||||
super.key,
|
||||
@@ -20,12 +21,19 @@ class TRoomPage extends StatefulWidget {
|
||||
class _TRoomPageState extends State<TRoomPage> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ChangeNotifierProvider<TchRoomVM>(
|
||||
create: (BuildContext context) {
|
||||
return TchRoomVM(
|
||||
roomInfo: widget.roomInfo,
|
||||
);
|
||||
},
|
||||
return MultiProvider(
|
||||
providers: [
|
||||
ChangeNotifierProvider<TchRoomVM>(
|
||||
create: (_) => TchRoomVM(info: widget.roomInfo),
|
||||
),
|
||||
ChangeNotifierProxyProvider<TchRoomVM, CountDownVM>(
|
||||
create: (_) => CountDownVM(),
|
||||
update: (_, tchVM, countDownVM) {
|
||||
countDownVM!.bind(tchVM.roomInfo);
|
||||
return countDownVM;
|
||||
},
|
||||
),
|
||||
],
|
||||
child: Scaffold(
|
||||
backgroundColor: Color(0xff2c3032),
|
||||
appBar: TopBar(),
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:app/data/models/meeting_room_dto.dart';
|
||||
import 'package:app/request/dto/room/room_list_item_dto.dart';
|
||||
import 'package:app/request/dto/room/room_info_dto.dart';
|
||||
import 'package:app/request/dto/room/room_type_dto.dart';
|
||||
import 'package:app/request/dto/room/room_user_dto.dart';
|
||||
import 'package:app/request/dto/room/rtc_token_dto.dart';
|
||||
import 'package:app/request/websocket/room_protocol.dart';
|
||||
@@ -13,18 +14,17 @@ import 'type.dart';
|
||||
|
||||
class TchRoomVM extends ChangeNotifier {
|
||||
TchRoomVM({
|
||||
required this.roomInfo,
|
||||
String? start,
|
||||
required RoomListItemDto info,
|
||||
}) {
|
||||
roomInfo = MeetingRoomDto.fromRoomListItem(info).copyWith(roomStatus: -1);
|
||||
_startRoom();
|
||||
}
|
||||
|
||||
///学生摄像头列表
|
||||
List<RoomUserDto> _students = [];
|
||||
|
||||
///房间的基础信息
|
||||
final RoomInfoDto roomInfo;
|
||||
int roomStatus = -1; // //-1加载中,0没开始,1进行中,2关闭
|
||||
///房间的基础信息,其中状态-1加载中,0没开始,1进行中,2关闭
|
||||
late MeetingRoomDto roomInfo;
|
||||
|
||||
///老师选中的学生id
|
||||
int activeSId = 0;
|
||||
@@ -42,8 +42,6 @@ class TchRoomVM extends ChangeNotifier {
|
||||
|
||||
///websocket管理
|
||||
final RoomWebSocket _ws = RoomWebSocket();
|
||||
|
||||
// bool wsConnected = false; // socket连接状态
|
||||
StreamSubscription<RoomMessage>? _sub;
|
||||
|
||||
RtcTokenDto? get rtcToken => _ws.rtcToken;
|
||||
@@ -61,8 +59,8 @@ class TchRoomVM extends ChangeNotifier {
|
||||
// 自习室人员变化
|
||||
if (msg.event == RoomEvent.changeUser) {
|
||||
final list = RoomUserDto.listFromJson(msg.data['user_list']);
|
||||
final room = RoomTypeDto.fromJson(msg.data['room_info']);
|
||||
roomStatus = room.roomStatus;
|
||||
final room = RoomInfoDto.fromJson(msg.data['room_info']);
|
||||
_updateRoomInfo(room);
|
||||
onStudentChange(list);
|
||||
} else if ([
|
||||
RoomEvent.openSpeaker,
|
||||
@@ -74,11 +72,26 @@ class TchRoomVM extends ChangeNotifier {
|
||||
RoomEvent.handUp,
|
||||
].contains(msg.event)) {
|
||||
onSyncStudentItem(RoomUserDto.fromJson(msg.data));
|
||||
} else if (msg.event == RoomEvent.fileUploadComplete) {
|
||||
updateStudentFile(
|
||||
msg.data['user_id'],
|
||||
(msg.data['flies'] as List).map((e) => e.toString()).toList(),
|
||||
);
|
||||
}
|
||||
});
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
///更新房间信息
|
||||
void _updateRoomInfo(RoomInfoDto info) {
|
||||
roomInfo = roomInfo.copyWith(
|
||||
roomStatus: info.roomStatus,
|
||||
actualStartTime: info.roomStartTime,
|
||||
boardUuid: info.boardUuid,
|
||||
);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
///自习室的开关
|
||||
/// - [isOpen]: 是否开启
|
||||
void toggleRoom({required bool isOpen}) {
|
||||
@@ -92,6 +105,7 @@ class TchRoomVM extends ChangeNotifier {
|
||||
///学生选择
|
||||
void selectStudent(int id) {
|
||||
activeSId = id;
|
||||
clearHandUp(id);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@@ -114,20 +128,38 @@ class TchRoomVM extends ChangeNotifier {
|
||||
student.speekerStatus = isOpen ? 0 : 1;
|
||||
data['is_mute'] = isOpen ? 1 : 0;
|
||||
} else if (action == StudentAction.camera) {
|
||||
//如果是摄像头,只能关
|
||||
if (student.cameraStatus == 0) return;
|
||||
//如果是摄像头
|
||||
bool isOpen = student.cameraStatus == 1;
|
||||
student.cameraStatus = 0;
|
||||
data['is_mute'] = 1;
|
||||
data['is_mute'] = isOpen ? 1 : 0;
|
||||
} else if (action == StudentAction.microphone) {
|
||||
//如果是麦克风,只能关
|
||||
if (student.microphoneStatus == 0) return;
|
||||
//如果是麦克风
|
||||
bool isOpen = student.microphoneStatus == 1;
|
||||
student.microphoneStatus = 0;
|
||||
data['is_mute'] = 1;
|
||||
data['is_mute'] = isOpen ? 1 : 0;
|
||||
}
|
||||
notifyListeners();
|
||||
_ws.send(RoomCommand.switchStudentCamera, data);
|
||||
}
|
||||
|
||||
///关闭全部学生的摄像头或者扬声器
|
||||
void closeAllStudentAction(StudentAction action) {
|
||||
_students.forEach((item) {
|
||||
if (action == StudentAction.speaker) {
|
||||
item.speekerStatus = 0;
|
||||
} else if (action == StudentAction.camera) {
|
||||
item.cameraStatus = 0;
|
||||
}
|
||||
});
|
||||
notifyListeners();
|
||||
Map<String, dynamic> data = {
|
||||
'target_user_id': "all",
|
||||
"mute_type": action.value,
|
||||
"is_mute": 1,
|
||||
};
|
||||
_ws.send(RoomCommand.switchStudentCamera, data);
|
||||
}
|
||||
|
||||
//清除全部学生举手,或者是指定
|
||||
void clearHandUp(int? id) {
|
||||
Map<String, dynamic> data = {};
|
||||
@@ -155,13 +187,28 @@ class TchRoomVM extends ChangeNotifier {
|
||||
/// 同步单个学生的最新状态
|
||||
void onSyncStudentItem(RoomUserDto userInfo) {
|
||||
final index = _students.indexWhere((t) => t.userId == userInfo.userId);
|
||||
print(userInfo.toString());
|
||||
if (index != -1) {
|
||||
_students[index] = userInfo;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
///更新学生的文件
|
||||
void updateStudentFile(int uId, List<String> files) {
|
||||
final index = _students.indexWhere((t) => t.userId == uId);
|
||||
if (index != -1) {
|
||||
_students[index].filesList = files;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
///结束会议
|
||||
void endRoom() {
|
||||
roomInfo = roomInfo.copyWith(roomStatus: 2);
|
||||
_ws.send(RoomCommand.closeRoom);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
//销毁
|
||||
@override
|
||||
void dispose() {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import 'package:agora_rtc_engine/agora_rtc_engine.dart';
|
||||
import 'package:app/config/config.dart';
|
||||
import 'package:app/providers/user_store.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||
|
||||
import '../viewmodel/tch_room_vm.dart';
|
||||
import 'student_item.dart';
|
||||
@@ -27,11 +27,11 @@ class _ContentViewState extends State<ContentView> {
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
WakelockPlus.disable();
|
||||
_dispose();
|
||||
}
|
||||
|
||||
void _initRtc() async {
|
||||
UserStore userStore = context.read<UserStore>();
|
||||
final vm = context.read<TchRoomVM>();
|
||||
_engine = createAgoraRtcEngine();
|
||||
//初始化 RtcEngine,设置频道场景为 channelProfileLiveBroadcasting(直播场景)
|
||||
@@ -41,39 +41,33 @@ class _ContentViewState extends State<ContentView> {
|
||||
channelProfile: ChannelProfileType.channelProfileCommunication,
|
||||
),
|
||||
);
|
||||
//添加回调
|
||||
_engine!.registerEventHandler(
|
||||
RtcEngineEventHandler(
|
||||
// 成功加入频道回调
|
||||
onJoinChannelSuccess: (RtcConnection connection, int elapsed) {
|
||||
setState(() {});
|
||||
},
|
||||
// 远端用户或主播加入当前频道回调
|
||||
onUserJoined: (RtcConnection connection, int remoteUid, int elapsed) {},
|
||||
// 远端用户或主播离开当前频道回调
|
||||
onUserOffline: (RtcConnection connection, int remoteUid, UserOfflineReasonType reason) {},
|
||||
),
|
||||
);
|
||||
//启动视频模块
|
||||
// 启用视频模块
|
||||
await _engine!.enableVideo();
|
||||
//加入频道
|
||||
await _engine!.joinChannel(
|
||||
token: vm.rtcToken!.token,
|
||||
channelId: vm.rtcToken!.channel,
|
||||
uid: userStore.userInfo!.id,
|
||||
options: ChannelMediaOptions(
|
||||
// 自动订阅所有视频流
|
||||
autoSubscribeVideo: true,
|
||||
// 自动订阅所有音频流
|
||||
autoSubscribeAudio: true,
|
||||
// 发布摄像头采集的视频
|
||||
publishCameraTrack: true,
|
||||
// 发布麦克风采集的音频
|
||||
publishMicrophoneTrack: true,
|
||||
// 设置用户角色为 clientRoleBroadcaster(主播)或 clientRoleAudience(观众)
|
||||
clientRoleType: ClientRoleType.clientRoleBroadcaster,
|
||||
),
|
||||
);
|
||||
// 开启本地预览
|
||||
await _engine!.startPreview();
|
||||
|
||||
final status = await _engine!.getConnectionState();
|
||||
WakelockPlus.enable();
|
||||
if (status == ConnectionStateType.connectionStateDisconnected) {
|
||||
//加入频道
|
||||
await _engine!.joinChannel(
|
||||
token: vm.rtcToken!.token,
|
||||
channelId: vm.rtcToken!.channel,
|
||||
uid: vm.rtcToken!.uid,
|
||||
options: ChannelMediaOptions(
|
||||
// 自动订阅所有视频流
|
||||
autoSubscribeVideo: true,
|
||||
// 自动订阅所有音频流
|
||||
autoSubscribeAudio: true,
|
||||
// 发布摄像头采集的视频
|
||||
publishCameraTrack: true,
|
||||
// 发布麦克风采集的音频
|
||||
publishMicrophoneTrack: true,
|
||||
// 设置用户角色为 clientRoleBroadcaster(主播)或 clientRoleAudience(观众)
|
||||
clientRoleType: ClientRoleType.clientRoleBroadcaster,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
//销毁
|
||||
@@ -89,8 +83,11 @@ class _ContentViewState extends State<ContentView> {
|
||||
return Consumer<TchRoomVM>(
|
||||
builder: (context, vm, _) {
|
||||
if (vm.students.isEmpty) {
|
||||
return Center(
|
||||
child: Text('准备中'),
|
||||
return Align(
|
||||
child: Text(
|
||||
'学生还没入场',
|
||||
style: TextStyle(color: Colors.white),
|
||||
),
|
||||
);
|
||||
}
|
||||
//选中的学生
|
||||
@@ -105,9 +102,30 @@ class _ContentViewState extends State<ContentView> {
|
||||
spacing: 15,
|
||||
children: [
|
||||
Expanded(
|
||||
child: StudentItem(
|
||||
user: activeStudent,
|
||||
engine: _engine,
|
||||
child: Stack(
|
||||
children: [
|
||||
StudentItem(
|
||||
user: activeStudent,
|
||||
engine: _engine,
|
||||
),
|
||||
Positioned(
|
||||
top: 0,
|
||||
left: 0,
|
||||
child: Container(
|
||||
width: 150,
|
||||
color: Colors.black,
|
||||
child: AspectRatio(
|
||||
aspectRatio: 1 / 1.2,
|
||||
child: AgoraVideoView(
|
||||
controller: VideoViewController(
|
||||
rtcEngine: _engine!,
|
||||
canvas: const VideoCanvas(uid: 0),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:app/utils/time.dart';
|
||||
import 'package:app/widgets/base/dialog/config_dialog.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../../../../widgets/room/core/count_down_vm.dart';
|
||||
import 'content_view.dart';
|
||||
import '../viewmodel/tch_room_vm.dart';
|
||||
|
||||
@@ -17,40 +17,12 @@ class StatusView extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _StatusViewState extends State<StatusView> {
|
||||
int _seconds = 0;
|
||||
Timer? _timer;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_timer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _startCountDown(DateTime startTime) {
|
||||
// 避免重复计时器
|
||||
if (_timer != null) return;
|
||||
|
||||
final now = DateTime.now();
|
||||
int diff = startTime.difference(now).inSeconds;
|
||||
|
||||
if (diff <= 0) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_seconds = diff;
|
||||
});
|
||||
|
||||
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_seconds--;
|
||||
});
|
||||
|
||||
if (_seconds <= 0) {
|
||||
_timer?.cancel();
|
||||
_timer = null;
|
||||
}
|
||||
});
|
||||
void initState() {
|
||||
super.initState();
|
||||
final countVM = context.read<CountDownVM>();
|
||||
countVM.removeListener(_onCountDownEnd);
|
||||
countVM.addListener(_onCountDownEnd);
|
||||
}
|
||||
|
||||
///开播中返回拦截弹窗
|
||||
@@ -72,55 +44,65 @@ class _StatusViewState extends State<StatusView> {
|
||||
);
|
||||
}
|
||||
|
||||
///监听会议室倒计时结束的时候
|
||||
void _onCountDownEnd() {
|
||||
final countVM = context.read<CountDownVM>();
|
||||
if (countVM.endCountDown == 0) {
|
||||
EasyLoading.showToast("自习室已到结束时间,请记得关闭会议室");
|
||||
countVM.removeListener(_onCountDownEnd);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final vm = context.watch<TchRoomVM>();
|
||||
final tchVM = context.watch<TchRoomVM>();
|
||||
var roomStatus = tchVM.roomInfo.roomStatus;
|
||||
|
||||
/// 1. 未加载
|
||||
if (vm.roomStatus == -1) {
|
||||
if (roomStatus == -1) {
|
||||
return const Align(
|
||||
child: Text("加载中", style: TextStyle(color: Colors.white)),
|
||||
);
|
||||
}
|
||||
|
||||
/// 2. 未开始的房间
|
||||
if (vm.roomStatus == 0) {
|
||||
if (vm.canEnterRoom) {
|
||||
// 到时间了 → 自动开播
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
vm.toggleRoom(isOpen: true);
|
||||
});
|
||||
} else {
|
||||
// 没到时间 → 启动倒计时
|
||||
_startCountDown(parseTime(vm.roomInfo.startTime));
|
||||
}
|
||||
|
||||
return Align(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Text(
|
||||
"未到开播时间,到点后自动开播",
|
||||
style: TextStyle(color: Colors.white),
|
||||
),
|
||||
Container(
|
||||
margin: const EdgeInsets.symmetric(vertical: 10),
|
||||
child: Text(
|
||||
formatSeconds(_seconds),
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 26,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
if (roomStatus == 0) {
|
||||
return Consumer<CountDownVM>(
|
||||
builder: (_, countVM, __) {
|
||||
if (countVM.canEnterRoom) {
|
||||
tchVM.toggleRoom(isOpen: true);
|
||||
return SizedBox();
|
||||
} else {
|
||||
countVM.startStartCountdown();
|
||||
return Align(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Text(
|
||||
"未到开播时间,到点后自动开播",
|
||||
style: TextStyle(color: Colors.white),
|
||||
),
|
||||
Container(
|
||||
margin: const EdgeInsets.symmetric(vertical: 10),
|
||||
child: Text(
|
||||
formatSeconds(countVM.startCountDown),
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 26,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// 3. 已开播
|
||||
if (vm.roomStatus == 1) {
|
||||
if (roomStatus == 1) {
|
||||
return PopScope(
|
||||
canPop: false,
|
||||
onPopInvokedWithResult: (didPop, _) {
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:agora_rtc_engine/agora_rtc_engine.dart';
|
||||
import 'package:app/pages/teacher/room/viewmodel/type.dart';
|
||||
import 'package:app/request/dto/room/room_user_dto.dart';
|
||||
import 'package:app/widgets/room/file_drawer.dart';
|
||||
import 'package:app/widgets/room/other_widget.dart';
|
||||
import 'package:app/widgets/room/video_surface.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
@@ -26,7 +27,11 @@ class StudentItem extends StatefulWidget {
|
||||
class _StudentItemState extends State<StudentItem> {
|
||||
///打开文件列表
|
||||
void _openFileList() {
|
||||
showFileDialog(context, isUpload: false);
|
||||
showFileDialog(
|
||||
context,
|
||||
isUpload: false,
|
||||
files: widget.user.filesList,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -40,7 +45,6 @@ class _StudentItemState extends State<StudentItem> {
|
||||
|
||||
///声音是否开启
|
||||
bool isSpeakerOpen = widget.user.speekerStatus == 1;
|
||||
|
||||
return ClipRRect(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
child: Container(
|
||||
@@ -51,15 +55,18 @@ class _StudentItemState extends State<StudentItem> {
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: Stack(
|
||||
alignment: Alignment.bottomCenter,
|
||||
children: [
|
||||
if (widget.engine != null)
|
||||
AgoraVideoView(
|
||||
controller: VideoViewController(
|
||||
rtcEngine: widget.engine!,
|
||||
canvas: VideoCanvas(uid: widget.user.rtcUid),
|
||||
VideoSurface(
|
||||
user: widget.user,
|
||||
child: AgoraVideoView(
|
||||
controller: VideoViewController(
|
||||
rtcEngine: widget.engine!,
|
||||
canvas: VideoCanvas(uid: widget.user.rtcUid),
|
||||
),
|
||||
),
|
||||
),
|
||||
// VideoSurface(),
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
@@ -79,6 +86,8 @@ class _StudentItemState extends State<StudentItem> {
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
///右上角选中
|
||||
if (widget.user.userId != vm.activeSId)
|
||||
Positioned(
|
||||
right: 5,
|
||||
@@ -99,6 +108,17 @@ class _StudentItemState extends State<StudentItem> {
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
///举手
|
||||
if (widget.user.handup == 1)
|
||||
Positioned(
|
||||
bottom: 40,
|
||||
child: HandRaiseButton(
|
||||
onTap: () {
|
||||
vm.clearHandUp(widget.user.userId);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -106,6 +126,7 @@ class _StudentItemState extends State<StudentItem> {
|
||||
ColoredBox(
|
||||
color: Color(0xFF232426),
|
||||
child: Row(
|
||||
spacing: 1,
|
||||
children: [
|
||||
_actionItem(
|
||||
icon: isCameraOpen ? RemixIcons.video_on_fill : RemixIcons.video_off_fill,
|
||||
|
||||
Reference in New Issue
Block a user