diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 6854d03..584d9aa 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -20,6 +20,7 @@ "http://xueguang.test.tuzuu.com"; + ///声网APPid static String get swAppId => "011c2fd2e1854511a80c1aebded4eee7"; } diff --git a/lib/global/event_bus.dart b/lib/global/event_bus.dart new file mode 100644 index 0000000..4eecb13 --- /dev/null +++ b/lib/global/event_bus.dart @@ -0,0 +1,33 @@ +import 'dart:async'; + +/// 全局事件总线 +class EventBus { + EventBus._(); + + static final _instance = EventBus._(); + + factory EventBus() => _instance; + + + // StreamController,广播模式可以让多个地方监听 + final StreamController _controller = StreamController.broadcast(); + + /// 发送事件 + void fire(GlobalEvent event) { + _controller.add(event); + } + + /// 监听事件 + Stream get stream => _controller.stream; + + /// 关闭流(一般应用生命周期结束时调用) + void dispose() { + _controller.close(); + } +} + +/// 事件类型枚举 +enum GlobalEvent { + unauthorized, // 401 需要重新登录 + // 以后可以扩展其他事件 +} diff --git a/lib/config/theme/base/app_colors_base.dart b/lib/global/theme/base/app_colors_base.dart similarity index 100% rename from lib/config/theme/base/app_colors_base.dart rename to lib/global/theme/base/app_colors_base.dart diff --git a/lib/config/theme/base/app_text_style.dart b/lib/global/theme/base/app_text_style.dart similarity index 100% rename from lib/config/theme/base/app_text_style.dart rename to lib/global/theme/base/app_text_style.dart diff --git a/lib/config/theme/base/app_theme_ext.dart b/lib/global/theme/base/app_theme_ext.dart similarity index 100% rename from lib/config/theme/base/app_theme_ext.dart rename to lib/global/theme/base/app_theme_ext.dart diff --git a/lib/config/theme/theme.dart b/lib/global/theme/theme.dart similarity index 100% rename from lib/config/theme/theme.dart rename to lib/global/theme/theme.dart diff --git a/lib/config/theme/themes/light_theme.dart b/lib/global/theme/themes/light_theme.dart similarity index 100% rename from lib/config/theme/themes/light_theme.dart rename to lib/global/theme/themes/light_theme.dart diff --git a/lib/main.dart b/lib/main.dart index 63a01e7..7f00c5e 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,12 +1,18 @@ +import 'dart:async'; + +import 'package:app/global/event_bus.dart'; import 'package:app/providers/user_store.dart'; +import 'package:app/router/route_paths.dart'; import 'package:flutter/material.dart'; import 'package:flutter_easyloading/flutter_easyloading.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:app/router/routes.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:go_router/go_router.dart'; import 'package:provider/provider.dart'; -import 'config/theme/theme.dart'; +import 'global/theme/theme.dart'; +import 'global/theme/themes/light_theme.dart'; void main() { runApp( @@ -19,9 +25,26 @@ void main() { ); } -class MyApp extends StatelessWidget { +class MyApp extends StatefulWidget { const MyApp({super.key}); + @override + State createState() => _MyAppState(); +} + +class _MyAppState extends State { + @override + void initState() { + super.initState(); + // 1. 监听 401 + EventBus().stream.listen((event) { + if (event == GlobalEvent.unauthorized) { + final ctx = navigatorKey.currentState?.context; + ctx?.go(RoutePaths.login); + } + }); + } + @override Widget build(BuildContext context) { return ScreenUtilInit( diff --git a/lib/pages/student/home/s_home_page.dart b/lib/pages/student/home/s_home_page.dart index 3c3e2b0..864037d 100644 --- a/lib/pages/student/home/s_home_page.dart +++ b/lib/pages/student/home/s_home_page.dart @@ -1,4 +1,4 @@ -import 'package:app/config/theme/base/app_theme_ext.dart'; +import 'package:app/global/theme/base/app_theme_ext.dart'; import 'package:app/pages/student/home/viewmodel/s_home_vm.dart'; import 'package:app/widgets/version/version_dialog.dart'; import 'package:flutter/material.dart'; diff --git a/lib/pages/student/home/today/banner_info.dart b/lib/pages/student/home/today/banner_info.dart index 210295c..4293643 100644 --- a/lib/pages/student/home/today/banner_info.dart +++ b/lib/pages/student/home/today/banner_info.dart @@ -1,4 +1,4 @@ -import 'package:app/config/theme/base/app_theme_ext.dart'; +import 'package:app/global/theme/base/app_theme_ext.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; diff --git a/lib/pages/student/home/today/s_today_card.dart b/lib/pages/student/home/today/s_today_card.dart index 2e3607c..907f7b0 100644 --- a/lib/pages/student/home/today/s_today_card.dart +++ b/lib/pages/student/home/today/s_today_card.dart @@ -1,4 +1,4 @@ -import 'package:app/config/theme/base/app_theme_ext.dart'; +import 'package:app/global/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'; diff --git a/lib/pages/student/room/controls/top_bar.dart b/lib/pages/student/room/controls/top_bar.dart index 1d62322..e72f06e 100644 --- a/lib/pages/student/room/controls/top_bar.dart +++ b/lib/pages/student/room/controls/top_bar.dart @@ -1,6 +1,11 @@ +import 'package:app/pages/student/room/viewmodel/stu_room_vm.dart'; +import 'package:app/providers/user_store.dart'; import 'package:app/utils/time.dart'; +import 'package:app/widgets/base/button/index.dart'; import 'package:app/widgets/base/dialog/config_dialog.dart'; +import 'package:app/widgets/room/board/board_manager.dart'; import 'package:app/widgets/room/core/count_down_vm.dart'; +import 'package:app/widgets/room/other_widget.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:provider/provider.dart'; @@ -18,6 +23,8 @@ class TopBar extends StatelessWidget implements PreferredSizeWidget { @override Widget build(BuildContext context) { + final vm = context.watch(); + final userStore = context.read(); return AppBar( foregroundColor: Colors.white, titleTextStyle: const TextStyle(color: Colors.white, fontSize: 18), @@ -58,16 +65,38 @@ class TopBar extends StatelessWidget implements PreferredSizeWidget { Text(vm.roomInfo!.roomName), Text( formatSeconds(vm.studyTime), - style: const TextStyle(fontSize: 12, color: Colors.white24), + style: const TextStyle( + fontSize: 12, + color: Colors.white24, + ), ), ], ); }, ), actions: [ - IconButton( - onPressed: onOther, - icon: Icon(showOther ? RemixIcons.team_fill : RemixIcons.team_line), + ActionButton( + icon: showOther ? RemixIcons.team_fill : RemixIcons.team_line, + text: showOther ? "隐藏学生" : '显示学生', + onTap: onOther, + ), + Visibility( + visible: vm.roomInfo.roomStatus == 1, + child: ActionButton( + color: Theme.of(context).primaryColor, + icon: RemixIcons.artboard_line, + text: "进入白板", + onTap: () { + final boardManager = BoardManager(); + final vm = context.read(); + boardManager.showBoardDialog( + context, + uid: userStore.userInfo!.name, + roomId: vm.roomInfo.id, + isTeacher: true, + ); + }, + ), ), ], ); diff --git a/lib/pages/student/room/video/teacher_video.dart b/lib/pages/student/room/video/teacher_video.dart index 00ef54e..d9a32de 100644 --- a/lib/pages/student/room/video/teacher_video.dart +++ b/lib/pages/student/room/video/teacher_video.dart @@ -52,13 +52,13 @@ class TeacherVideo extends StatelessWidget { ), ), Positioned( - top: 0, - left: 0, + top: 30, + left: 10, child: Container( - width: 150, + width: 200, color: Colors.black, child: AspectRatio( - aspectRatio: 1 / 1.2, + aspectRatio: 16 / 9, child: AgoraVideoView( controller: VideoViewController( rtcEngine: vm.engine!, @@ -80,4 +80,4 @@ class TeacherVideo extends StatelessWidget { ), ); } -} \ No newline at end of file +} diff --git a/lib/pages/student/room/viewmodel/stu_room_vm.dart b/lib/pages/student/room/viewmodel/stu_room_vm.dart index de30d16..c5a08da 100644 --- a/lib/pages/student/room/viewmodel/stu_room_vm.dart +++ b/lib/pages/student/room/viewmodel/stu_room_vm.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'package:agora_rtc_engine/agora_rtc_engine.dart'; -import 'package:app/config/config.dart'; +import 'package:app/global/config.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'; @@ -138,8 +138,9 @@ class StuRoomVM extends ChangeNotifier { ///学生人员变化事件,(如加入、退出、掉线) void onStudentChange(List list) { + final lineList = list.where((t) => t.online == 1); List newList = []; - for (var t in list) { + for (var t in lineList) { //设置老师 if (t.userType == 2) { teacherInfo = t; diff --git a/lib/pages/teacher/home/t_home_page.dart b/lib/pages/teacher/home/t_home_page.dart index 82395f1..bf4d892 100644 --- a/lib/pages/teacher/home/t_home_page.dart +++ b/lib/pages/teacher/home/t_home_page.dart @@ -1,4 +1,4 @@ -import 'package:app/config/theme/base/app_theme_ext.dart'; +import 'package:app/global/theme/base/app_theme_ext.dart'; import 'package:app/widgets/version/version_dialog.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -13,7 +13,7 @@ class THomePage extends StatelessWidget { @override Widget build(BuildContext context) { - showUpdateDialog(context); + // showUpdateDialog(context); return ChangeNotifierProvider( create: (_) => HomeViewModel(), child: const _HomeView(), diff --git a/lib/pages/teacher/home/widgets/header.dart b/lib/pages/teacher/home/widgets/header.dart index 1d19764..ac38e50 100644 --- a/lib/pages/teacher/home/widgets/header.dart +++ b/lib/pages/teacher/home/widgets/header.dart @@ -1,4 +1,4 @@ -import 'package:app/config/theme/base/app_theme_ext.dart'; +import 'package:app/global/theme/base/app_theme_ext.dart'; import 'package:app/providers/user_store.dart'; import 'package:app/router/route_paths.dart'; import 'package:flutter/material.dart'; diff --git a/lib/pages/teacher/room/controls/top_bar.dart b/lib/pages/teacher/room/controls/top_bar.dart index e66e0b8..9e90e2f 100644 --- a/lib/pages/teacher/room/controls/top_bar.dart +++ b/lib/pages/teacher/room/controls/top_bar.dart @@ -1,7 +1,9 @@ +import 'package:app/global/theme/theme.dart'; +import 'package:app/providers/user_store.dart'; 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:app/widgets/room/board/board_manager.dart'; +import 'package:app/widgets/room/other_widget.dart'; import 'package:flutter/material.dart'; import 'package:flutter_easyloading/flutter_easyloading.dart'; import 'package:go_router/go_router.dart'; @@ -17,6 +19,7 @@ class TopBar extends StatelessWidget implements PreferredSizeWidget { @override Widget build(BuildContext context) { + final userStore = context.read(); final vm = context.watch(); return AppBar( @@ -49,28 +52,35 @@ class TopBar extends StatelessWidget implements PreferredSizeWidget { ], ), actions: [ - _actionButton( - context, + ActionButton( icon: RemixIcons.video_on_ai_line, - title: "关闭全部", - onPressed: () { + text: "关闭全部", + onTap: () { _closeAll(context, StudentAction.camera); }, ), - _actionButton( - context, + ActionButton( icon: RemixIcons.volume_up_line, - title: "全部静音", - onPressed: () { + text: "全部静音", + onTap: () { _closeAll(context, StudentAction.speaker); }, ), - Container( - margin: EdgeInsets.only(right: 15), - child: Button( - text: "白板", - textStyle: TextStyle(fontSize: 14), - onPressed: (){}, + Visibility( + visible: vm.roomInfo.roomStatus == 1, + child: ActionButton( + text: "打开白板", + color: Theme.of(context).primaryColor, + icon: RemixIcons.artboard_line, + onTap: () { + final boardManager = BoardManager(); + boardManager.showBoardDialog( + context, + uid: userStore.userInfo!.name, + roomId: vm.roomInfo.id, + isTeacher: true, + ); + }, ), ), Consumer( @@ -78,11 +88,11 @@ class TopBar extends StatelessWidget implements PreferredSizeWidget { if (vm.roomInfo.roomStatus != 1) { return SizedBox(); } - return Button( - type: ThemeType.danger, - textStyle: TextStyle(fontSize: 14), + return ActionButton( + icon: RemixIcons.close_line, + color: context.danger, text: "结束自习室", - onPressed: () { + onTap: () { showDialog( context: context, builder: (_) { @@ -103,12 +113,15 @@ class TopBar extends StatelessWidget implements PreferredSizeWidget { ); }, ), - SizedBox(width: 10), ], ); } - Widget _infoItem(BuildContext context, {required String title, required IconData icon}) { + Widget _infoItem( + BuildContext context, { + required String title, + required IconData icon, + }) { return Row( children: [ Icon(icon, color: Colors.white54, size: 14), @@ -118,32 +131,6 @@ class TopBar extends StatelessWidget implements PreferredSizeWidget { ); } - 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(); String content = (action == StudentAction.camera) ? '是否关闭所有学生的摄像头?' : '是否关闭所有学生的扬声器?'; diff --git a/lib/pages/teacher/room/viewmodel/tch_room_vm.dart b/lib/pages/teacher/room/viewmodel/tch_room_vm.dart index 9a673bf..4958239 100644 --- a/lib/pages/teacher/room/viewmodel/tch_room_vm.dart +++ b/lib/pages/teacher/room/viewmodel/tch_room_vm.dart @@ -176,7 +176,13 @@ class TchRoomVM extends ChangeNotifier { ///学生人员变化事件,(如加入、退出、掉线) void onStudentChange(List list) { - _students = list.where((t) => t.userType != 2).toList(); + _students = list.where((t) => t.userType != 2 && t.online == 1).toList(); + if (activeSId != 0) { + final it = _students.where((t) => t.userId == activeSId).firstOrNull; + if (it == null) { + activeSId = 0; + } + } // 如果当前没有学生,则选择第一个 if (activeSId == 0 && _students.isNotEmpty) { activeSId = _students.first.userId; diff --git a/lib/pages/teacher/room/widgets/content_view.dart b/lib/pages/teacher/room/widgets/content_view.dart index b99720f..328c28c 100644 --- a/lib/pages/teacher/room/widgets/content_view.dart +++ b/lib/pages/teacher/room/widgets/content_view.dart @@ -1,5 +1,5 @@ import 'package:agora_rtc_engine/agora_rtc_engine.dart'; -import 'package:app/config/config.dart'; +import 'package:app/global/config.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:wakelock_plus/wakelock_plus.dart'; @@ -109,13 +109,13 @@ class _ContentViewState extends State { engine: _engine, ), Positioned( - top: 0, - left: 0, + top: 10, + right: 10, child: Container( - width: 150, + width: 200, color: Colors.black, child: AspectRatio( - aspectRatio: 1 / 1.2, + aspectRatio: 16 / 9, child: AgoraVideoView( controller: VideoViewController( rtcEngine: _engine!, @@ -128,21 +128,24 @@ class _ContentViewState extends State { ], ), ), - SizedBox( - width: 300, - child: ListView.separated( - itemBuilder: (_, index) { - var item = otherStudents.elementAt(index); - return SizedBox( - height: 250, - child: StudentItem( - user: item, - engine: _engine, - ), - ); - }, - separatorBuilder: (_, __) => SizedBox(height: 15), - itemCount: otherStudents.length, + Visibility( + visible: otherStudents.isNotEmpty, + child: SizedBox( + width: 300, + child: ListView.separated( + itemBuilder: (_, index) { + var item = otherStudents.elementAt(index); + return SizedBox( + height: 250, + child: StudentItem( + user: item, + engine: _engine, + ), + ); + }, + separatorBuilder: (_, __) => SizedBox(height: 15), + itemCount: otherStudents.length, + ), ), ), ], diff --git a/lib/pages/teacher/room/widgets/student_item.dart b/lib/pages/teacher/room/widgets/student_item.dart index 48cc88c..d6155da 100644 --- a/lib/pages/teacher/room/widgets/student_item.dart +++ b/lib/pages/teacher/room/widgets/student_item.dart @@ -26,9 +26,10 @@ class StudentItem extends StatefulWidget { class _StudentItemState extends State { ///打开文件列表 - void _openFileList() { + void _openFileList({required String name}) { showFileDialog( context, + name: name, isUpload: false, files: widget.user.filesList, ); @@ -160,7 +161,12 @@ class _StudentItemState extends State { ); }, ), - _actionItem(icon: RemixIcons.file_list_3_fill, onTap: _openFileList), + _actionItem( + icon: RemixIcons.file_list_3_fill, + onTap: (){ + _openFileList(name: widget.user.userName); + }, + ), ], ), ), diff --git a/lib/providers/user_store.dart b/lib/providers/user_store.dart index 912bfa1..c433ba7 100644 --- a/lib/providers/user_store.dart +++ b/lib/providers/user_store.dart @@ -7,9 +7,9 @@ class UserStore extends ChangeNotifier { UserInfoDto? userInfo; String token = ""; - Future init() async{ + Future init() async { token = await getToken(); - await setUserInfo(); + await setUserInfo(); notifyListeners(); } @@ -41,4 +41,10 @@ class UserStore extends ChangeNotifier { token = ''; notifyListeners(); } + + ///强制退出(不调用接口、不通知 UI) + static Future forceLogout() async { + await Storage.remove('token'); + await Storage.remove('user_info'); + } } diff --git a/lib/request/api/room_api.dart b/lib/request/api/room_api.dart index 3a79f25..dd94e25 100644 --- a/lib/request/api/room_api.dart +++ b/lib/request/api/room_api.dart @@ -1,12 +1,13 @@ +import 'package:app/request/dto/room/board_token_dto.dart'; import 'package:app/request/dto/room/rtc_token_dto.dart'; import 'package:app/request/network/request.dart'; import '../dto/room/room_list_item_dto.dart'; /// 获取房间列表 -Future> getRoomListApi() async { +Future> getRoomListApi() async { var res = await Request().get('/study_room/get_study_room_list'); - return List.from(res.map((x) => RoomListItemDto .fromJson(x))); + return List.from(res.map((x) => RoomListItemDto.fromJson(x))); } ///获取自习室的websocket令牌 @@ -24,3 +25,11 @@ Future getRtcTokenApi(int roomId) async { }); return RtcTokenDto.fromJson(res); } + +///获取白板的令牌 +Future getBoardTokenApi(int roomId) async { + var res = await Request().get('/study_room/get_whiteboard_token', { + "study_room_id": roomId, + }); + return BoardTokenDto.fromJson(res); +} diff --git a/lib/request/dto/room/board_token_dto.dart b/lib/request/dto/room/board_token_dto.dart new file mode 100644 index 0000000..c295daa --- /dev/null +++ b/lib/request/dto/room/board_token_dto.dart @@ -0,0 +1,32 @@ + +class BoardTokenDto { + BoardTokenDto({ + required this.expiresAt, + required this.whiteboardAppid, + required this.whiteboardUuid, + required this.expiresIn, + required this.whiteboardToken, + }); + + DateTime expiresAt; + String whiteboardAppid; + String whiteboardUuid; + int expiresIn; + String whiteboardToken; + + factory BoardTokenDto.fromJson(Map json) => BoardTokenDto( + expiresAt: DateTime.parse(json["expires_at"]), + whiteboardAppid: json["whiteboard_appid"], + whiteboardUuid: json["whiteboard_uuid"], + expiresIn: json["expires_in"], + whiteboardToken: json["whiteboard_token"], + ); + + Map toJson() => { + "expires_at": expiresAt.toIso8601String(), + "whiteboard_appid": whiteboardAppid, + "whiteboard_uuid": whiteboardUuid, + "expires_in": expiresIn, + "whiteboard_token": whiteboardToken, + }; +} diff --git a/lib/request/network/interceptor.dart b/lib/request/network/interceptor.dart index 7230020..53c38ed 100644 --- a/lib/request/network/interceptor.dart +++ b/lib/request/network/interceptor.dart @@ -1,34 +1,46 @@ +import 'package:app/global/event_bus.dart'; import 'package:app/providers/user_store.dart'; +import 'package:app/router/route_paths.dart'; +import 'package:app/router/routes.dart'; import 'package:dio/dio.dart'; import 'package:flutter_easyloading/flutter_easyloading.dart'; +import 'package:go_router/go_router.dart'; +import 'package:provider/provider.dart'; import '../dto/base_dto.dart'; ///请求拦截器 -void onRequest( - RequestOptions options, - RequestInterceptorHandler handler, -) async { +void onRequest(RequestOptions options, + RequestInterceptorHandler handler,) async { String token = await UserStore.getToken(); options.headers['Authorization'] = 'Bearer $token'; return handler.next(options); } ///响应拦截器 -void onResponse( - Response response, - ResponseInterceptorHandler handler, -) { +void onResponse(Response response, + ResponseInterceptorHandler handler,) async { var apiResponse = ApiDto.fromJson(response.data); if (apiResponse.code == 1) { response.data = apiResponse.data; handler.next(response); + } else if (apiResponse.code == 401) { + // final bus = EventBus(); + // bus.fire(GlobalEvent.unauthorized); + // final context = navigatorKey.currentState?.context; + // print("dsd"); + // if (context != null) { + // // UserStore userStore = context.read(); + // // userStore.logout(); + // context.go(RoutePaths.login); + // } + handler.next(response); } else { handler.reject( DioException( requestOptions: response.requestOptions, response: response, - error: {'code': 0, 'message': apiResponse.message}, + error: {'code': apiResponse.code, 'message': apiResponse.message}, ), ); showError(apiResponse.message); @@ -36,15 +48,18 @@ void onResponse( } ///错误响应 -void onError( - DioException e, - ErrorInterceptorHandler handler, -) { +void onError(DioException e, + ErrorInterceptorHandler handler,) async { var title = ""; if (e.type == DioExceptionType.connectionTimeout) { title = "请求超时"; } else if (e.type == DioExceptionType.badResponse) { - if (e.response?.statusCode == 404) { + if (e.response?.statusCode == 401) { + final bus = EventBus(); + bus.fire(GlobalEvent.unauthorized); + title = "登录信息已失效,请重新登录"; + } + else if (e.response?.statusCode == 404) { title = "接口404不存在"; } else { title = "500"; diff --git a/lib/request/network/request.dart b/lib/request/network/request.dart index fb9f9f5..cd77b69 100644 --- a/lib/request/network/request.dart +++ b/lib/request/network/request.dart @@ -1,5 +1,5 @@ import 'package:dio/dio.dart'; -import 'package:app/config/config.dart'; +import 'package:app/global/config.dart'; import 'interceptor.dart'; diff --git a/lib/request/websocket/room_websocket.dart b/lib/request/websocket/room_websocket.dart index c45f7b5..6e2f4bd 100644 --- a/lib/request/websocket/room_websocket.dart +++ b/lib/request/websocket/room_websocket.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; -import 'package:app/config/config.dart'; +import 'package:app/global/config.dart'; import 'package:app/request/api/room_api.dart'; import 'package:app/request/websocket/room_protocol.dart'; import 'package:logger/logger.dart'; diff --git a/lib/router/routes.dart b/lib/router/routes.dart index cf60c3a..c001cd4 100644 --- a/lib/router/routes.dart +++ b/lib/router/routes.dart @@ -3,6 +3,7 @@ import 'package:app/router/modules/student_routes.dart'; import 'package:app/router/modules/teacher_routes.dart'; import 'package:app/router/route_paths.dart'; import 'package:app/router/router_config.dart'; +import 'package:flutter/material.dart' hide RouterConfig; import 'package:go_router/go_router.dart'; List routeConfigs = [ @@ -21,8 +22,12 @@ List routes = routeConfigs.map((item) { ); }).toList(); +GlobalKey navigatorKey = GlobalKey(); + + //变量命名 GoRouter goRouter = GoRouter( + navigatorKey: navigatorKey, initialLocation: RoutePaths.splash, routes: routes, ); diff --git a/lib/utils/transfer/upload.dart b/lib/utils/transfer/upload.dart index 8eec37c..4ee453d 100644 --- a/lib/utils/transfer/upload.dart +++ b/lib/utils/transfer/upload.dart @@ -1,6 +1,6 @@ import 'dart:io'; -import 'package:app/config/config.dart'; +import 'package:app/global/config.dart'; import 'package:app/request/api/common_api.dart'; import 'package:app/request/dto/common/qiu_token_dto.dart'; import 'package:crypto/crypto.dart'; diff --git a/lib/widgets/base/button/index.dart b/lib/widgets/base/button/index.dart index c2ad5b1..255e650 100644 --- a/lib/widgets/base/button/index.dart +++ b/lib/widgets/base/button/index.dart @@ -1,8 +1,9 @@ -import 'package:app/config/theme/theme.dart'; +import 'package:app/global/theme/theme.dart'; import 'package:flutter/material.dart'; import '../config/config.dart'; + class Button extends StatelessWidget { final double? width; final String text; @@ -12,14 +13,16 @@ class Button extends StatelessWidget { final VoidCallback? onPressed; final bool loading; final bool disabled; + final Widget? icon; const Button({ super.key, + this.icon, this.width, this.textStyle = const TextStyle(), this.radius = const BorderRadius.all(Radius.circular(80)), required this.text, - this.onPressed, + this.onPressed, this.type = ThemeType.primary, this.loading = false, this.disabled = false, diff --git a/lib/widgets/base/card/g_card.dart b/lib/widgets/base/card/g_card.dart index 99e4ba5..8af1242 100644 --- a/lib/widgets/base/card/g_card.dart +++ b/lib/widgets/base/card/g_card.dart @@ -1,4 +1,4 @@ -import 'package:app/config/theme/base/app_theme_ext.dart'; +import 'package:app/global/theme/base/app_theme_ext.dart'; import 'package:flutter/material.dart'; class GCard extends StatelessWidget { diff --git a/lib/widgets/base/tag/index.dart b/lib/widgets/base/tag/index.dart index a4e4b52..da0b953 100644 --- a/lib/widgets/base/tag/index.dart +++ b/lib/widgets/base/tag/index.dart @@ -1,9 +1,10 @@ -import 'package:app/config/theme/base/app_theme_ext.dart'; +import 'package:app/global/theme/base/app_theme_ext.dart'; import 'package:flutter/material.dart'; import '../config/color.dart'; import '../config/config.dart'; + class Tag extends StatelessWidget { final String text; final Color? color; diff --git a/lib/widgets/room/board/board_dialog.dart b/lib/widgets/room/board/board_dialog.dart new file mode 100644 index 0000000..7027bfe --- /dev/null +++ b/lib/widgets/room/board/board_dialog.dart @@ -0,0 +1,73 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:remixicon/remixicon.dart'; +import 'package:webview_flutter/webview_flutter.dart'; + + +class BoardDialog extends StatefulWidget { + final WebViewController controller; + const BoardDialog({super.key, required this.controller}); + + @override + State createState() => _BoardDialogState(); +} + +class _BoardDialogState extends State { + bool _loading = true; + + @override + void initState() { + super.initState(); + + // 绑定加载事件 + widget.controller.setNavigationDelegate( + NavigationDelegate( + onPageStarted: (_) { + setState(() => _loading = true); + }, + onPageFinished: (_) { + setState(() => _loading = false); + }, + ), + ); + } + + @override + Widget build(BuildContext context) { + return Container( + color: Colors.white, + padding: const EdgeInsets.all(16), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('白板', style: Theme.of(context).textTheme.titleSmall), + IconButton( + onPressed: () => context.pop(), + icon: const Icon(RemixIcons.close_circle_fill), + ), + ], + ), + Expanded( + child: Stack( + children: [ + WebViewWidget( + controller: widget.controller, + ), + if (_loading) + Container( + color: Colors.white, + child: const Center( + child: CircularProgressIndicator(), + ), + ), + ], + ), + ), + ], + ), + ); + } +} + diff --git a/lib/widgets/room/board/board_manager.dart b/lib/widgets/room/board/board_manager.dart new file mode 100644 index 0000000..57a7d87 --- /dev/null +++ b/lib/widgets/room/board/board_manager.dart @@ -0,0 +1,101 @@ +import 'package:app/global/config.dart'; +import 'package:app/request/api/room_api.dart'; +import 'package:app/request/dto/room/board_token_dto.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_easyloading/flutter_easyloading.dart'; +import 'package:webview_flutter/webview_flutter.dart'; + +import 'board_dialog.dart'; + +class BoardManager { + BoardManager._(); + + static final BoardManager _instance = BoardManager._(); + + factory BoardManager() => _instance; + + WebViewController? _webViewController; + BoardTokenDto? _boardToken; + + ///获取token + Future _fetchToken(int roomId) async { + final now = DateTime.now(); + + if (_boardToken == null) { + EasyLoading.show(status: "开启白板中"); + _boardToken = await getBoardTokenApi(roomId); + } else { + //离过期差多少小时 + int remainSeconds = _boardToken!.expiresAt.difference(now).inSeconds; + // 剩余不足 2 小时 = 7200 秒 + if (remainSeconds <= 7200) { + EasyLoading.show(status: "开启白板中"); + _boardToken = await getBoardTokenApi(roomId); + } + } + EasyLoading.dismiss(); + } + + ///生成完整 URL + /// -[uid] 用户 + /// -[roomId] 房间id + /// -[isTeacher] 是否是老师 + Future buildUrl({ + required String uid, + required int roomId, + required bool isTeacher, + }) async { + await _fetchToken(roomId); + + Map params = { + "appid": _boardToken!.whiteboardAppid, + "uid": uid, + "uuid": _boardToken!.whiteboardUuid, + "room_token": _boardToken!.whiteboardToken, + "write": isTeacher ? 1 : 0, + }; + String paramStr = params.entries.map((e) => "${e.key}=${e.value}").join("&"); + + return "${Config.webUrl}/board?$paramStr"; + } + + /// 获取全局唯一 WebViewController + Future getController(String url) async { + if (_webViewController != null) return _webViewController!; + + _webViewController = WebViewController() + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setNavigationDelegate( + // ⭐ 加上加载监听(BoardDialog 再 listen 一次也没关系) + NavigationDelegate( + onPageStarted: (_) {}, + onPageFinished: (_) {}, + ), + ) + ..loadRequest(Uri.parse(url)); + + return _webViewController!; + } + + ///打开白板 + /// -[uid] 用户名+id + /// -[roomId] 房间id + /// -[isTeacher] 是否是老师 + Future showBoardDialog( + BuildContext context, { + required String uid, + required int roomId, + required bool isTeacher, + }) async { + String url = await buildUrl( + uid: uid, + roomId: roomId, + isTeacher: isTeacher, + ); + final controller = await getController(url); + showDialog( + context: context, + builder: (_) => BoardDialog(controller: controller), + ); + } +} diff --git a/lib/widgets/room/file_drawer.dart b/lib/widgets/room/file_drawer.dart index 5113ead..e812372 100644 --- a/lib/widgets/room/file_drawer.dart +++ b/lib/widgets/room/file_drawer.dart @@ -1,6 +1,6 @@ import 'dart:io'; -import 'package:app/config/theme/base/app_theme_ext.dart'; +import 'package:app/global/theme/base/app_theme_ext.dart'; import 'package:app/utils/transfer/upload.dart'; import 'package:app/widgets/base/actionSheet/action_sheet.dart'; import 'package:app/widgets/base/actionSheet/type.dart'; @@ -150,7 +150,7 @@ class _FileDrawerState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - "${widget.name ?? ""}上传文件列表", + "${widget.name ?? ""}文件列表", style: Theme.of(context).textTheme.titleSmall, ), Expanded( diff --git a/lib/widgets/room/other_widget.dart b/lib/widgets/room/other_widget.dart index fa3e854..45a81e4 100644 --- a/lib/widgets/room/other_widget.dart +++ b/lib/widgets/room/other_widget.dart @@ -1,7 +1,9 @@ import 'package:flutter/material.dart'; +/// 举手按钮 class HandRaiseButton extends StatelessWidget { final void Function() onTap; + const HandRaiseButton({super.key, required this.onTap}); @override @@ -24,3 +26,41 @@ class HandRaiseButton extends StatelessWidget { ); } } + +///topBar的操作按钮 +class ActionButton extends StatelessWidget { + final IconData icon; + final String text; + final Color? color; + final void Function()? onTap; + + const ActionButton({ + super.key, + required this.icon, + required this.text, + this.color, + this.onTap, + }); + + @override + Widget build(BuildContext context) { + return Container( + padding: EdgeInsets.symmetric(horizontal: 10, vertical: 5), + margin: EdgeInsets.only(right: 15), + decoration: BoxDecoration( + color: color ?? Color(0xff4a4f4f), + borderRadius: BorderRadius.circular(8), + ), + child: InkWell( + onTap: onTap, + child: Row( + children: [ + Icon(icon, size: 16), + SizedBox(width: 8), + Text(text, style: TextStyle(fontSize: 14)), + ], + ), + ), + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index 64ea3b6..4a500b4 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -898,6 +898,38 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.1.1" + webview_flutter: + dependency: "direct main" + description: + name: webview_flutter + sha256: c3e4fe614b1c814950ad07186007eff2f2e5dd2935eba7b9a9a1af8e5885f1ba + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.13.0" + webview_flutter_android: + dependency: transitive + description: + name: webview_flutter_android + sha256: "9a25f6b4313978ba1c2cda03a242eea17848174912cfb4d2d8ee84a556f248e3" + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.10.1" + webview_flutter_platform_interface: + dependency: transitive + description: + name: webview_flutter_platform_interface + sha256: "63d26ee3aca7256a83ccb576a50272edd7cfc80573a4305caa98985feb493ee0" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.14.0" + webview_flutter_wkwebview: + dependency: transitive + description: + name: webview_flutter_wkwebview + sha256: fb46db8216131a3e55bcf44040ca808423539bc6732e7ed34fb6d8044e3d512f + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.23.0" win32: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 1987257..825cf5f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -33,6 +33,7 @@ dependencies: app_installer: ^1.3.1 wakelock_plus: ^1.3.3 image_picker: ^1.2.0 + webview_flutter: ^4.13.0 dev_dependencies: flutter_test: