diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts
index 31b7d05..da5a16c 100644
--- a/android/app/build.gradle.kts
+++ b/android/app/build.gradle.kts
@@ -1,10 +1,18 @@
+import java.util.Properties
+import java.io.FileInputStream
+import java.time.LocalDateTime
+import java.time.format.DateTimeFormatter
+
plugins {
id("com.android.application")
id("kotlin-android")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin")
}
-
+val keystorePropertiesFile = rootProject.file("key.properties")
+val keystoreProperties = Properties().apply {
+ load(FileInputStream(keystorePropertiesFile))
+}
android {
namespace = "com.zkwl.xueguang"
compileSdk = flutter.compileSdkVersion
@@ -18,7 +26,14 @@ android {
kotlinOptions {
jvmTarget = JavaVersion.VERSION_11.toString()
}
-
+ packagingOptions {
+ pickFirsts += setOf(
+ "lib/x86/libaosl.so",
+ "lib/x86_64/libaosl.so",
+ "lib/armeabi-v7a/libaosl.so",
+ "lib/arm64-v8a/libaosl.so"
+ )
+ }
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.zkwl.xueguang"
@@ -28,13 +43,31 @@ android {
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
+ ndk {
+ abiFilters += listOf("arm64-v8a") // 只保留 arm64
+ }
+ }
+ signingConfigs {
+ create("release") {
+ keyAlias = keystoreProperties["keyAlias"] as String
+ keyPassword = keystoreProperties["keyPassword"] as String
+ storeFile = file(keystoreProperties["storeFile"] as String)
+ storePassword = keystoreProperties["storePassword"] as String
+ }
}
-
buildTypes {
- release {
- // TODO: Add your own signing config for the release build.
- // Signing with the debug keys for now, so `flutter run --release` works.
- signingConfig = signingConfigs.getByName("debug")
+ getByName("release") {
+ signingConfig = signingConfigs.getByName("release")
+ isMinifyEnabled = true // 开启混淆和代码压缩
+ }
+ }
+ val env = project.findProperty("env")?.toString() ?: "dev"
+ val channel = project.findProperty("channel")?.toString() ?: "dev"
+ applicationVariants.all {
+ outputs.all {
+ if (this is com.android.build.gradle.internal.api.ApkVariantOutputImpl) {
+ outputFileName = "学光_${channel}_${versionName}.apk"
+ }
}
}
}
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index fa2d3e6..6854d03 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -47,6 +47,18 @@
+
+
+
+
+
+
+
+
diff --git a/assets/image/version_bg.png b/assets/image/version_bg.png
new file mode 100644
index 0000000..2d8c7e2
Binary files /dev/null and b/assets/image/version_bg.png differ
diff --git a/build.dev.sh b/build.dev.sh
new file mode 100644
index 0000000..5573722
--- /dev/null
+++ b/build.dev.sh
@@ -0,0 +1 @@
+flutter build apk -Penv=dev
diff --git a/build.prod.sh b/build.prod.sh
new file mode 100644
index 0000000..4501893
--- /dev/null
+++ b/build.prod.sh
@@ -0,0 +1,26 @@
+#!/bin/bash
+
+#当前执行目录
+current_dir=$(pwd)
+channelPath="$current_dir/build/app/outputs/apk/release"
+mkdir -p "$channelPath"
+rm -rf "$channelPath"/*
+#定义环境变量和 Android 设备品牌
+declare -a APP_CHANNELS=("release")
+
+# 遍历每个环境配置并执行构建
+for index in "${APP_CHANNELS[@]}"; do
+ ENV=${APP_CHANNELS[$index]}
+ flutter build apk -Penv=prod -Pchannel=$ENV
+ # 检查构建是否成功
+ if [ $? -ne 0 ]; then
+ echo "Build failed for ENV=$ENV"
+ exit 1
+ fi
+ apkFile=$(find $(pwd)/build/app/outputs/apk/release/ -name "*.apk" -print -quit)
+ # 获取 .apk 文件的文件名
+ apkFileName=$(basename "$apkFile")
+ mv $apkFile "$channelPath/$apkFileName"
+done
+
+echo "All builds completed successfully."
diff --git a/key.jks b/key.jks
new file mode 100644
index 0000000..2e7f4fa
Binary files /dev/null and b/key.jks differ
diff --git a/lib/data/models/meeting_room_dto.dart b/lib/data/models/meeting_room_dto.dart
new file mode 100644
index 0000000..d439023
--- /dev/null
+++ b/lib/data/models/meeting_room_dto.dart
@@ -0,0 +1,78 @@
+import 'package:app/request/dto/room/room_list_item_dto.dart';
+
+class MeetingRoomDto {
+ int id;
+ final String roomName;
+ final String startTime;
+ final String endTime;
+ String actualStartTime;
+ int roomStatus;
+ String boardUuid;
+
+ MeetingRoomDto({
+ required this.id,
+ this.roomName = "",
+ this.startTime = "",
+ this.endTime = "",
+ this.actualStartTime = "",
+ this.roomStatus = 0,
+ this.boardUuid = "",
+ });
+
+ /// 根据 RoomListItemDto 创建
+ MeetingRoomDto.fromRoomListItem(RoomListItemDto item)
+ : id = item.id,
+ roomName = item.roomName,
+ startTime = item.startTime,
+ endTime = item.endTime,
+ actualStartTime = "",
+ roomStatus = 0,
+ boardUuid = "";
+
+ /// 从 JSON 创建对象
+ factory MeetingRoomDto.fromJson(Map json) {
+ return MeetingRoomDto(
+ id: json['id'] ?? 0,
+ roomName: json['room_name'] as String,
+ startTime: json['start_time'] as String,
+ endTime: json['end_time'] as String,
+ actualStartTime: json['actual_start_time'] ?? "",
+ roomStatus: json['room_status'] ?? 0,
+ boardUuid: json['board_uuid'] ?? "",
+ );
+ }
+
+ /// 转为 JSON
+ Map toJson() {
+ return {
+ 'id': id,
+ 'room_name': roomName,
+ 'start_time': startTime,
+ 'end_time': endTime,
+ 'actual_start_time': actualStartTime,
+ 'room_status': roomStatus,
+ 'board_uuid': boardUuid,
+ };
+ }
+
+ /// 复制对象,可修改部分字段
+ MeetingRoomDto copyWith({
+ int? id,
+ String? roomName,
+ String? startTime,
+ String? endTime,
+ String? actualStartTime,
+ int? roomStatus,
+ String? boardUuid,
+ }) {
+ return MeetingRoomDto(
+ id: id ?? this.id,
+ roomName: roomName ?? this.roomName,
+ startTime: startTime ?? this.startTime,
+ endTime: endTime ?? this.endTime,
+ actualStartTime: actualStartTime ?? this.actualStartTime,
+ roomStatus: roomStatus ?? this.roomStatus,
+ boardUuid: boardUuid ?? this.boardUuid,
+ );
+ }
+}
diff --git a/lib/pages/student/home/s_home_page.dart b/lib/pages/student/home/s_home_page.dart
index 3075fb7..3c3e2b0 100644
--- a/lib/pages/student/home/s_home_page.dart
+++ b/lib/pages/student/home/s_home_page.dart
@@ -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()
],
),
),
diff --git a/lib/pages/student/home/today/s_today_card.dart b/lib/pages/student/home/today/s_today_card.dart
index 7b86f55..2e3607c 100644
--- a/lib/pages/student/home/today/s_today_card.dart
+++ b/lib/pages/student/home/today/s_today_card.dart
@@ -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 {
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 {
),
InfoItem(
label: "自习时长",
- value: "${vm.roomMinutes} 分钟",
+ value: "${formatSeconds(vm.roomMinutes * 60, 'hh小时mm分钟')} ",
icon: RemixIcons.timer_line,
color: context.success,
),
diff --git a/lib/pages/student/home/viewmodel/s_home_vm.dart b/lib/pages/student/home/viewmodel/s_home_vm.dart
index f195ab3..fa1b88d 100644
--- a/lib/pages/student/home/viewmodel/s_home_vm.dart
+++ b/lib/pages/student/home/viewmodel/s_home_vm.dart
@@ -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() {
diff --git a/lib/pages/student/home/widgets/feature_static.dart b/lib/pages/student/home/widgets/feature_static.dart
deleted file mode 100644
index 6399039..0000000
--- a/lib/pages/student/home/widgets/feature_static.dart
+++ /dev/null
@@ -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 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);
-}
diff --git a/lib/pages/student/home/widgets/tip_card.dart b/lib/pages/student/home/widgets/tip_card.dart
new file mode 100644
index 0000000..7aa9442
--- /dev/null
+++ b/lib/pages/student/home/widgets/tip_card.dart
@@ -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,
+ ),
+ ],
+ ),
+ );
+ }
+}
\ No newline at end of file
diff --git a/lib/pages/student/room/controls/bottom_bar.dart b/lib/pages/student/room/controls/bottom_bar.dart
index 1c7e64d..cde289e 100644
--- a/lib/pages/student/room/controls/bottom_bar.dart
+++ b/lib/pages/student/room/controls/bottom_bar.dart
@@ -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 createState() => _BottomBarState();
@@ -15,51 +16,77 @@ class BottomBar extends StatefulWidget {
class _BottomBarState extends State {
///显示文件
void _handShowFile() {
- showFileDialog(context);
+ final vm = context.read();
+ 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();
+
+ if (vm.roomInfo.roomStatus != 1) {
+ return SizedBox();
+ }
return Container(
decoration: BoxDecoration(
color: Color(0xff232426),
),
height: 70,
child: Consumer(
- 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,
),
],
);
- }
+ },
),
);
}
diff --git a/lib/pages/student/room/controls/top_bar.dart b/lib/pages/student/room/controls/top_bar.dart
index 08bf12c..1d62322 100644
--- a/lib/pages/student/room/controls/top_bar.dart
+++ b/lib/pages/student/room/controls/top_bar.dart
@@ -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 createState() => _TopBarState();
-
- @override
- Size get preferredSize => const Size.fromHeight(kToolbarHeight);
-}
-
-class _TopBarState extends State {
- Timer? _timer;
- int seconds = 0;
- late DateTime startTime;
-
- @override
- void initState() {
- super.initState();
- final vm = context.read();
- 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();
-
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(
+ 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);
}
diff --git a/lib/pages/student/room/s_room_page.dart b/lib/pages/student/room/s_room_page.dart
index 868f64e..161e8ca 100644
--- a/lib/pages/student/room/s_room_page.dart
+++ b/lib/pages/student/room/s_room_page.dart
@@ -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 {
@override
Widget build(BuildContext context) {
UserStore userStore = context.read();
- return ChangeNotifierProvider(
- create: (_) => StuRoomVM(
- roomInfo: widget.roomInfo,
- uid: userStore.userInfo!.id,
- ),
+ return MultiProvider(
+ providers: [
+ ChangeNotifierProvider(
+ create: (_) => StuRoomVM(
+ info: widget.roomInfo,
+ uid: userStore.userInfo!.id,
+ ),
+ ),
+ ChangeNotifierProxyProvider(
+ 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 {
child: SlideHide(
direction: SlideDirection.down,
hide: !_controlsVisible,
- child: BottomBar(),
+ child: BottomBar(
+ onTap: _toggleOverlay,
+ ),
),
),
],
diff --git a/lib/pages/student/room/video/student_video_list.dart b/lib/pages/student/room/video/student_video_list.dart
index 23f085e..4caff76 100644
--- a/lib/pages/student/room/video/student_video_list.dart
+++ b/lib/pages/student/room/video/student_video_list.dart
@@ -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();
+ 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(
diff --git a/lib/pages/student/room/video/teacher_video.dart b/lib/pages/student/room/video/teacher_video.dart
index 81dc960..00ef54e 100644
--- a/lib/pages/student/room/video/teacher_video.dart
+++ b/lib/pages/student/room/video/teacher_video.dart
@@ -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 createState() => _TeacherVideoState();
-}
-
-class _TeacherVideoState extends State {
@override
Widget build(BuildContext context) {
- final vm = context.read();
+ final vm = context.watch();
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,
+ ),
+ ),
+ ],
),
),
);
}
-}
+}
\ 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 251c917..de30d16 100644
--- a/lib/pages/student/room/viewmodel/stu_room_vm.dart
+++ b/lib/pages/student/room/viewmodel/stu_room_vm.dart
@@ -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 _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 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();
}
}
diff --git a/lib/pages/student/room/widgets/status_view.dart b/lib/pages/student/room/widgets/status_view.dart
new file mode 100644
index 0000000..2889e82
--- /dev/null
+++ b/lib/pages/student/room/widgets/status_view.dart
@@ -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();
+ final teacherInfo = vm.teacherInfo;
+
+ ///没开始
+ if (vm.roomInfo.roomStatus == 0) {
+ return Consumer(
+ 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),
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/pages/teacher/home/t_home_page.dart b/lib/pages/teacher/home/t_home_page.dart
index 8fbeb49..82395f1 100644
--- a/lib/pages/teacher/home/t_home_page.dart
+++ b/lib/pages/teacher/home/t_home_page.dart
@@ -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();
+
return Scaffold(
backgroundColor: Theme.of(context).colorScheme.surfaceContainer,
appBar: Header(),
@@ -36,6 +40,8 @@ class _HomeView extends StatelessWidget {
),
children: [
TodayCard(),
+ TipCard1(),
+ TipCard2(),
],
),
),
diff --git a/lib/pages/teacher/home/viewmodel/home_view_model.dart b/lib/pages/teacher/home/viewmodel/home_view_model.dart
index bd9eb00..d6931c5 100644
--- a/lib/pages/teacher/home/viewmodel/home_view_model.dart
+++ b/lib/pages/teacher/home/viewmodel/home_view_model.dart
@@ -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() {
diff --git a/lib/pages/teacher/home/widgets/tip_card.dart b/lib/pages/teacher/home/widgets/tip_card.dart
new file mode 100644
index 0000000..acd0a56
--- /dev/null
+++ b/lib/pages/teacher/home/widgets/tip_card.dart
@@ -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,
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/lib/pages/teacher/home/widgets/today_card.dart b/lib/pages/teacher/home/widgets/today_card.dart
index accdc89..0df6fd3 100644
--- a/lib/pages/teacher/home/widgets/today_card.dart
+++ b/lib/pages/teacher/home/widgets/today_card.dart
@@ -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 {
),
_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 {
child: Button(
text: vm.canEnterRoom ? "开始自习室" : "未到开始时间",
type: ThemeType.success,
- // disabled: !vm.canEnterRoom,
+ disabled: !vm.canEnterRoom,
onPressed: _goToRoom,
),
),
diff --git a/lib/pages/teacher/room/controls/top_bar.dart b/lib/pages/teacher/room/controls/top_bar.dart
index 6237ac6..e66e0b8 100644
--- a/lib/pages/teacher/room/controls/top_bar.dart
+++ b/lib/pages/teacher/room/controls/top_bar.dart
@@ -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();
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(
+ 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(
+ 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();
+ 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);
}
diff --git a/lib/pages/teacher/room/t_room_page.dart b/lib/pages/teacher/room/t_room_page.dart
index 2f8fa4e..6dc9af4 100644
--- a/lib/pages/teacher/room/t_room_page.dart
+++ b/lib/pages/teacher/room/t_room_page.dart
@@ -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 {
@override
Widget build(BuildContext context) {
- return ChangeNotifierProvider(
- create: (BuildContext context) {
- return TchRoomVM(
- roomInfo: widget.roomInfo,
- );
- },
+ return MultiProvider(
+ providers: [
+ ChangeNotifierProvider(
+ create: (_) => TchRoomVM(info: widget.roomInfo),
+ ),
+ ChangeNotifierProxyProvider(
+ create: (_) => CountDownVM(),
+ update: (_, tchVM, countDownVM) {
+ countDownVM!.bind(tchVM.roomInfo);
+ return countDownVM;
+ },
+ ),
+ ],
child: Scaffold(
backgroundColor: Color(0xff2c3032),
appBar: TopBar(),
diff --git a/lib/pages/teacher/room/viewmodel/tch_room_vm.dart b/lib/pages/teacher/room/viewmodel/tch_room_vm.dart
index 1b05457..9a673bf 100644
--- a/lib/pages/teacher/room/viewmodel/tch_room_vm.dart
+++ b/lib/pages/teacher/room/viewmodel/tch_room_vm.dart
@@ -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 _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? _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 data = {
+ 'target_user_id': "all",
+ "mute_type": action.value,
+ "is_mute": 1,
+ };
+ _ws.send(RoomCommand.switchStudentCamera, data);
+ }
+
//清除全部学生举手,或者是指定
void clearHandUp(int? id) {
Map 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 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() {
diff --git a/lib/pages/teacher/room/widgets/content_view.dart b/lib/pages/teacher/room/widgets/content_view.dart
index a5d6191..b99720f 100644
--- a/lib/pages/teacher/room/widgets/content_view.dart
+++ b/lib/pages/teacher/room/widgets/content_view.dart
@@ -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 {
@override
void dispose() {
super.dispose();
+ WakelockPlus.disable();
_dispose();
}
void _initRtc() async {
- UserStore userStore = context.read();
final vm = context.read();
_engine = createAgoraRtcEngine();
//初始化 RtcEngine,设置频道场景为 channelProfileLiveBroadcasting(直播场景)
@@ -41,39 +41,33 @@ class _ContentViewState extends State {
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 {
return Consumer(
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 {
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(
diff --git a/lib/pages/teacher/room/widgets/status_view.dart b/lib/pages/teacher/room/widgets/status_view.dart
index 5582c72..6ff7f25 100644
--- a/lib/pages/teacher/room/widgets/status_view.dart
+++ b/lib/pages/teacher/room/widgets/status_view.dart
@@ -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 {
- 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();
+ countVM.removeListener(_onCountDownEnd);
+ countVM.addListener(_onCountDownEnd);
}
///开播中返回拦截弹窗
@@ -72,55 +44,65 @@ class _StatusViewState extends State {
);
}
+ ///监听会议室倒计时结束的时候
+ void _onCountDownEnd() {
+ final countVM = context.read();
+ if (countVM.endCountDown == 0) {
+ EasyLoading.showToast("自习室已到结束时间,请记得关闭会议室");
+ countVM.removeListener(_onCountDownEnd);
+ }
+ }
+
@override
Widget build(BuildContext context) {
- final vm = context.watch();
+ final tchVM = context.watch();
+ 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(
+ 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, _) {
diff --git a/lib/pages/teacher/room/widgets/student_item.dart b/lib/pages/teacher/room/widgets/student_item.dart
index 85d0c85..48cc88c 100644
--- a/lib/pages/teacher/room/widgets/student_item.dart
+++ b/lib/pages/teacher/room/widgets/student_item.dart
@@ -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 {
///打开文件列表
void _openFileList() {
- showFileDialog(context, isUpload: false);
+ showFileDialog(
+ context,
+ isUpload: false,
+ files: widget.user.filesList,
+ );
}
@override
@@ -40,7 +45,6 @@ class _StudentItemState extends State {
///声音是否开启
bool isSpeakerOpen = widget.user.speekerStatus == 1;
-
return ClipRRect(
borderRadius: BorderRadius.circular(10),
child: Container(
@@ -51,15 +55,18 @@ class _StudentItemState extends State {
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 {
),
),
),
+
+ ///右上角选中
if (widget.user.userId != vm.activeSId)
Positioned(
right: 5,
@@ -99,6 +108,17 @@ class _StudentItemState extends State {
),
),
),
+
+ ///举手
+ if (widget.user.handup == 1)
+ Positioned(
+ bottom: 40,
+ child: HandRaiseButton(
+ onTap: () {
+ vm.clearHandUp(widget.user.userId);
+ },
+ ),
+ ),
],
),
),
@@ -106,6 +126,7 @@ class _StudentItemState extends State {
ColoredBox(
color: Color(0xFF232426),
child: Row(
+ spacing: 1,
children: [
_actionItem(
icon: isCameraOpen ? RemixIcons.video_on_fill : RemixIcons.video_off_fill,
diff --git a/lib/request/api/common_api.dart b/lib/request/api/common_api.dart
new file mode 100644
index 0000000..0bb5ee2
--- /dev/null
+++ b/lib/request/api/common_api.dart
@@ -0,0 +1,18 @@
+import '../dto/common/qiu_token_dto.dart';
+import '../dto/common/version_dto.dart';
+import '../network/request.dart';
+
+///获取七牛token
+/// - [fileKey]: 文件key
+Future getQiuTokenApi(String fileKey) async {
+ var response = await Request().get("/files/get_qiniu_upload_token", {
+ "file_key": fileKey,
+ });
+ return QiuTokenDto.fromJson(response);
+}
+
+///获取APP最新版本
+Future getAppVersionApi() async {
+ var response = await Request().get("/get_latest_version");
+ return VersionDto.fromJson(response);
+}
diff --git a/lib/request/api/room_api.dart b/lib/request/api/room_api.dart
index 46dad16..3a79f25 100644
--- a/lib/request/api/room_api.dart
+++ b/lib/request/api/room_api.dart
@@ -1,12 +1,12 @@
import 'package:app/request/dto/room/rtc_token_dto.dart';
import 'package:app/request/network/request.dart';
-import '../dto/room/room_info_dto.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) => RoomInfoDto.fromJson(x)));
+ return List.from(res.map((x) => RoomListItemDto .fromJson(x)));
}
///获取自习室的websocket令牌
diff --git a/lib/request/dto/common/qiu_token_dto.dart b/lib/request/dto/common/qiu_token_dto.dart
new file mode 100644
index 0000000..c6d438e
--- /dev/null
+++ b/lib/request/dto/common/qiu_token_dto.dart
@@ -0,0 +1,24 @@
+class QiuTokenDto {
+ String? uploadUrl;
+ String? upToken;
+ String? fileKey;
+ String? domain;
+
+ QiuTokenDto({this.uploadUrl, this.upToken, this.fileKey, this.domain});
+
+ Map toJson() {
+ final map = {};
+ map["upload_url"] = uploadUrl;
+ map["up_token"] = upToken;
+ map["file_key"] = fileKey;
+ map["domain"] = domain;
+ return map;
+ }
+
+ QiuTokenDto.fromJson(dynamic json) {
+ uploadUrl = json["upload_url"] ?? "";
+ upToken = json["up_token"] ?? "";
+ fileKey = json["file_key"] ?? "";
+ domain = json["domain"] ?? "";
+ }
+}
diff --git a/lib/request/dto/common/version_dto.dart b/lib/request/dto/common/version_dto.dart
new file mode 100644
index 0000000..33ad5b3
--- /dev/null
+++ b/lib/request/dto/common/version_dto.dart
@@ -0,0 +1,47 @@
+class VersionDto {
+ VersionDto({
+ required this.latestVersion,
+ required this.updatedAt,
+ required this.downloadUrl,
+ required this.updateContent,
+ required this.createdAt,
+ required this.lowVersion,
+ required this.id,
+ required this.downloadSize,
+ required this.platform,
+ });
+
+ String latestVersion;
+ DateTime updatedAt;
+ String downloadUrl;
+ List updateContent;
+ DateTime createdAt;
+ String lowVersion;
+ int id;
+ String downloadSize;
+ int platform;
+
+ factory VersionDto.fromJson(Map json) => VersionDto(
+ latestVersion: json["latest_version"],
+ updatedAt: DateTime.parse(json["updated_at"]),
+ downloadUrl: json["download_url"],
+ updateContent: List.from(json["update_content"].map((x) => x)),
+ createdAt: DateTime.parse(json["created_at"]),
+ lowVersion: json["low_version"],
+ id: json["id"],
+ downloadSize: json["download_size"],
+ platform: json["platform"],
+ );
+
+ Map toJson() => {
+ "latest_version": latestVersion,
+ "updated_at": updatedAt.toIso8601String(),
+ "download_url": downloadUrl,
+ "update_content": List.from(updateContent.map((x) => x)),
+ "created_at": createdAt.toIso8601String(),
+ "low_version": lowVersion,
+ "id": id,
+ "download_size": downloadSize,
+ "platform": platform,
+ };
+}
diff --git a/lib/request/dto/room/room_info_dto.dart b/lib/request/dto/room/room_info_dto.dart
index 16655ef..8599dd8 100644
--- a/lib/request/dto/room/room_info_dto.dart
+++ b/lib/request/dto/room/room_info_dto.dart
@@ -1,41 +1,52 @@
class RoomInfoDto {
+ final int studyRoomId;
+ final int teacherId;
+ final int teacherRtcUid;
+ final String teacherWsClientId;
+ final int roomStatus;
+ final String dataType;
+ final String roomStartTime;
+ final String roomEndTime;
+ final String boardUuid;
+
+
RoomInfoDto({
- required this.teacherBackground,
- required this.teacherAvatar,
- required this.roomName,
- required this.startTime,
- required this.teacherName,
- required this.endTime,
- required this.id,
+ required this.studyRoomId,
+ required this.teacherId,
+ required this.teacherRtcUid,
+ required this.teacherWsClientId,
+ required this.roomStatus,
+ required this.dataType,
+ required this.roomStartTime,
+ required this.roomEndTime,
+ required this.boardUuid,
});
- String teacherBackground;
- String teacherAvatar;
- String roomName;
- String startTime;
- String teacherName;
- String endTime;
- int id;
+ Map toJson() {
+ final map = {};
+ map["study_room_id"] = studyRoomId;
+ map["teacher_id"] = teacherId;
+ map["teacher_rtc_uid"] = teacherRtcUid;
+ map["teacher_ws_client_id"] = teacherWsClientId;
+ map["room_status"] = roomStatus;
+ map["data_type"] = dataType;
+ map["room_start_time"] = roomStartTime;
+ map["room_end_time"] = roomEndTime;
+ map["whiteboard_uuid"] = boardUuid;
+ return map;
+ }
- factory RoomInfoDto.fromJson(Map json) =>
- RoomInfoDto(
- teacherBackground: json["teacher_background"],
- teacherAvatar: json["teacher_avatar"],
- roomName: json["room_name"],
- startTime: json["start_time"],
- teacherName: json["teacher_name"],
- endTime: json["end_time"],
- id: json["id"],
- );
-
- Map toJson() =>
- {
- "teacher_background": teacherBackground,
- "teacher_avatar": teacherAvatar,
- "room_name": roomName,
- "start_time": startTime,
- "teacher_name": teacherName,
- "end_time": endTime,
- "id": id,
- };
+ factory RoomInfoDto.fromJson(Map json) {
+ return RoomInfoDto(
+ studyRoomId: json["study_room_id"] ?? 0,
+ teacherId: json["teacher_id"] ?? 0,
+ teacherRtcUid: json["teacher_rtc_uid"] ?? 0,
+ teacherWsClientId: json["teacher_ws_client_id"] ?? "",
+ roomStatus: json["room_status"] ?? 0,
+ dataType: json["data_type"] ?? "",
+ roomStartTime: json["room_start_time"] ?? "",
+ roomEndTime: json["room_end_time"] ?? "",
+ boardUuid: json["whiteboard_uuid"] ?? "",
+ );
+ }
}
diff --git a/lib/request/dto/room/room_list_item_dto.dart b/lib/request/dto/room/room_list_item_dto.dart
new file mode 100644
index 0000000..06de174
--- /dev/null
+++ b/lib/request/dto/room/room_list_item_dto.dart
@@ -0,0 +1,51 @@
+class RoomListItemDto {
+ RoomListItemDto({
+ required this.teacherGrade,
+ required this.roomName,
+ required this.startTime,
+ required this.teacherName,
+ required this.teacherAvatar,
+ required this.endTime,
+ required this.teacherSchoolName,
+ required this.teacherIntroduction,
+ required this.id,
+ required this.teacherMajor,
+ });
+
+ String teacherGrade;
+ String roomName;
+ String startTime;
+ String teacherName;
+ String teacherAvatar;
+ String endTime;
+ String teacherSchoolName;
+ String teacherIntroduction;
+ int id;
+ String teacherMajor;
+
+ factory RoomListItemDto.fromJson(Map json) => RoomListItemDto(
+ teacherGrade: json["teacher_grade"],
+ roomName: json["room_name"],
+ startTime: json["start_time"],
+ teacherName: json["teacher_name"],
+ teacherAvatar: json["teacher_avatar"],
+ endTime: json["end_time"],
+ teacherSchoolName: json["teacher_school_name"],
+ teacherIntroduction: json["teacher_introduction"],
+ id: json["id"],
+ teacherMajor: json["teacher_major"],
+ );
+
+ Map toJson() => {
+ "teacher_grade": teacherGrade,
+ "room_name": roomName,
+ "start_time": startTime,
+ "teacher_name": teacherName,
+ "teacher_avatar": teacherAvatar,
+ "end_time": endTime,
+ "teacher_school_name": teacherSchoolName,
+ "teacher_introduction": teacherIntroduction,
+ "id": id,
+ "teacher_major": teacherMajor,
+ };
+}
diff --git a/lib/request/dto/room/room_type_dto.dart b/lib/request/dto/room/room_type_dto.dart
deleted file mode 100644
index e67f422..0000000
--- a/lib/request/dto/room/room_type_dto.dart
+++ /dev/null
@@ -1,39 +0,0 @@
-class RoomTypeDto {
- final int studyRoomId;
- final int teacherId;
- final int teacherRtcUid;
- final String teacherWsClientId;
- final int roomStatus;
- final String dataType;
-
- RoomTypeDto({
- required this.studyRoomId,
- required this.teacherId,
- required this.teacherRtcUid,
- required this.teacherWsClientId,
- required this.roomStatus,
- required this.dataType,
- });
-
- Map toJson() {
- final map = {};
- map["study_room_id"] = studyRoomId;
- map["teacher_id"] = teacherId;
- map["teacher_rtc_uid"] = teacherRtcUid;
- map["teacher_ws_client_id"] = teacherWsClientId;
- map["room_status"] = roomStatus;
- map["data_type"] = dataType;
- return map;
- }
-
- factory RoomTypeDto.fromJson(Map json) {
- return RoomTypeDto(
- studyRoomId: json["study_room_id"] ?? 0,
- teacherId: json["teacher_id"] ?? 0,
- teacherRtcUid: json["teacher_rtc_uid"] ?? 0,
- teacherWsClientId: json["teacher_ws_client_id"] ?? "",
- roomStatus: json["room_status"] ?? 0,
- dataType: json["data_type"] ?? "",
- );
- }
-}
diff --git a/lib/request/dto/room/room_user_dto.dart b/lib/request/dto/room/room_user_dto.dart
index dd08d36..353b46f 100644
--- a/lib/request/dto/room/room_user_dto.dart
+++ b/lib/request/dto/room/room_user_dto.dart
@@ -10,7 +10,7 @@ class RoomUserDto {
/// 1是学生,2是老师
final int userType;
- final List filesList;
+ List filesList;
final String dataType;
int handup;
int online; //0离线,1在线
diff --git a/lib/request/websocket/room_protocol.dart b/lib/request/websocket/room_protocol.dart
index 089c64f..370369f 100644
--- a/lib/request/websocket/room_protocol.dart
+++ b/lib/request/websocket/room_protocol.dart
@@ -80,6 +80,12 @@ enum RoomEvent {
///老师关闭学生的麦克风
closeStudentMic("sys_control_mute_microphone"),
+ ///老师打开学生的麦克风
+ openStudentMic("sys_control_unmute_microphone"),
+
+ ///老师开启学生的摄像头
+ openStudentCamera("sys_control_unmute_camera"),
+
///老师关闭学生的摄像头
closeStudentCamera("sys_control_mute_camera"),
diff --git a/lib/request/websocket/room_websocket.dart b/lib/request/websocket/room_websocket.dart
index 8a9b7df..c45f7b5 100644
--- a/lib/request/websocket/room_websocket.dart
+++ b/lib/request/websocket/room_websocket.dart
@@ -66,7 +66,10 @@ class RoomWebSocket {
print("未识别的 action: ${jsonMap['action']},消息已忽略");
return; // 直接跳过
} else {
- logger.i("接收到事件: ${event.value}");
+ logger.i("""
+ 接收到事件: ${event.value}
+ 数据: ${jsonMap['data']}
+ """);
}
final msg = RoomMessage(event, jsonMap['data']);
_msgController.add(msg);
@@ -96,7 +99,7 @@ class RoomWebSocket {
"action": action.value,
if (params != null) ...params,
};
- if(action != RoomCommand.ping){
+ if (action != RoomCommand.ping) {
logger.i("发送指令:$msg");
}
diff --git a/lib/utils/common.dart b/lib/utils/common.dart
new file mode 100644
index 0000000..a93925d
--- /dev/null
+++ b/lib/utils/common.dart
@@ -0,0 +1,6 @@
+import 'dart:io';
+
+///判断是否是安卓
+bool isAndroid(){
+ return Platform.isAndroid;
+}
\ No newline at end of file
diff --git a/lib/utils/time.dart b/lib/utils/time.dart
index e38c72b..dbdbc8b 100644
--- a/lib/utils/time.dart
+++ b/lib/utils/time.dart
@@ -33,21 +33,41 @@ String formatDate(dynamic date, [String format = 'YYYY-MM-DD hh:mm:ss']) {
/// 将秒数格式化为 00:00 或 00:00:00
/// - [seconds]: 秒数
-String formatSeconds(int seconds) {
- final h = seconds ~/ 3600;
- final m = (seconds % 3600) ~/ 60;
- final s = seconds % 60;
+/// - [format]: 格式化字符串,默认为 "hh:mm:ss"
+String formatSeconds(
+ int seconds, [
+ String format = 'hh:mm:ss',
+]) {
+ if (seconds < 0) seconds = 0;
- String twoDigits(int n) => n.toString().padLeft(2, '0');
+ int h = seconds ~/ 3600;
+ int m = (seconds % 3600) ~/ 60;
+ int s = seconds % 60;
- if (h > 0) {
- return '${twoDigits(h)}:${twoDigits(m)}:${twoDigits(s)}';
- } else {
- return '${twoDigits(m)}:${twoDigits(s)}';
- }
+ String two(int n) => n.toString().padLeft(2, '0');
+
+ // 支持以下 token:
+ // hh = 补零小时, h = 不补零小时
+ // mm = 补零分钟, m = 不补零分钟
+ // ss = 补零秒, s = 不补零秒
+ final replacements = {
+ 'hh': two(h),
+ 'mm': two(m),
+ 'ss': two(s),
+ 'h': h.toString(),
+ 'm': m.toString(),
+ 's': s.toString(),
+ };
+
+ String result = format;
+
+ replacements.forEach((key, value) {
+ result = result.replaceAll(key, value);
+ });
+
+ return result;
}
-
/// 将 "HH", "HH:mm" 或 "HH:mm:ss" 转为当天 DateTime
DateTime parseTime(String timeStr) {
final now = DateTime.now();
diff --git a/lib/utils/transfer/download.dart b/lib/utils/transfer/download.dart
new file mode 100644
index 0000000..0a61389
--- /dev/null
+++ b/lib/utils/transfer/download.dart
@@ -0,0 +1,81 @@
+//下载文件
+import 'dart:io';
+
+import 'package:path_provider/path_provider.dart';
+
+class LocalDownload {
+ static Future getLocalFilePath(String url, String path) async {
+ Uri uri = Uri.parse(url);
+ String fileName = uri.pathSegments.last;
+ //获取下载目录
+ Directory dir = await getApplicationCacheDirectory();
+ Directory uploadPath = Directory("${dir.path}$path/");
+ return uploadPath.path + fileName;
+ }
+
+ /// 公用下载方法
+ /// url 下载网络地址
+ /// path 存储地址,如/test
+ /// onProgress 下载回调函数
+ /// onDone 下载完毕回调
+ static downLoadFile({
+ required url,
+ required path,
+ required Function(double) onProgress,
+ required Function(String) onDone,
+ }) async {
+ HttpClient client = HttpClient();
+ Uri uri = Uri.parse(url);
+ //获取本地文件路径
+ String filePath = await getLocalFilePath(url, path);
+ // 发起 get 请求
+ HttpClientRequest request = await client.getUrl(uri);
+ // 响应
+ HttpClientResponse response = await request.close();
+ int contentLength = response.contentLength; // 获取文件总大小
+ int bytesReceived = 0; // 已接收的字节数
+ List chunkList = [];
+ if (response.statusCode == 200) {
+ response.listen(
+ (List chunk) {
+ chunkList.addAll(chunk);
+ bytesReceived += chunk.length; //更新已接受的字节数
+ //进度
+ double progress = bytesReceived * 100 / contentLength * 100;
+ progress = (progress / 100).truncateToDouble();
+ onProgress(progress);
+ },
+ onDone: () async {
+ //下载完毕
+ client.close();
+ File file = File(filePath);
+ if (!file.existsSync()) {
+ file.createSync(recursive: true);
+ await file.writeAsBytes(chunkList);
+ }
+ onDone(file.path);
+ },
+ onError: () {
+ client.close();
+ },
+ cancelOnError: true,
+ );
+ }
+ }
+
+ ///获取本地地址
+ ///
+ static Future getFilePath({
+ required url,
+ required path,
+ }) async {
+ //获取本地文件路径
+ String filePath = await getLocalFilePath(url, path);
+ File file = File(filePath);
+ if (file.existsSync()) {
+ return file.path;
+ } else {
+ return '';
+ }
+ }
+}
diff --git a/lib/utils/transfer/upload.dart b/lib/utils/transfer/upload.dart
new file mode 100644
index 0000000..8eec37c
--- /dev/null
+++ b/lib/utils/transfer/upload.dart
@@ -0,0 +1,55 @@
+import 'dart:io';
+
+import 'package:app/config/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';
+import 'package:dio/dio.dart';
+import 'package:flutter_easyloading/flutter_easyloading.dart';
+
+class QinUpload {
+ ///获取七牛token
+ static Future _getQiuToken(File file, String path) async {
+ // 读取文件的字节数据
+ final fileBytes = await file.readAsBytes();
+ String fileMd5 = md5.convert(fileBytes).toString();
+ //前缀
+ var prefix = Config.getEnv() == "dev" ? "test" : "release";
+ var suffix = file.path.split(".").last;
+
+ var res = await getQiuTokenApi(
+ "xueguang/$prefix/$path/$fileMd5.$suffix",
+ );
+ return res;
+ }
+
+ ///上传文件
+ /// - [file] 文件
+ /// - [path] 目标目录
+ static Future upload({
+ required File file,
+ required String path,
+ }) async {
+ var qiuToken = await _getQiuToken(file, path);
+ //数据
+ FormData formData = FormData.fromMap({
+ "file": await MultipartFile.fromFile(file.path),
+ "token": qiuToken.upToken,
+ "fname": qiuToken.fileKey,
+ "key": qiuToken.fileKey,
+ });
+ try {
+ Dio dio = Dio();
+ Response response = await dio.post(
+ qiuToken.uploadUrl!,
+ data: formData,
+ onSendProgress: (int sent, int total) {},
+ );
+ String key = response.data['key'];
+ return "https://${qiuToken.domain}/$key";
+ } catch (e) {
+ EasyLoading.showError("上传失败");
+ return null;
+ }
+ }
+}
diff --git a/lib/widgets/base/actionSheet/action_sheet.dart b/lib/widgets/base/actionSheet/action_sheet.dart
new file mode 100644
index 0000000..b2cd915
--- /dev/null
+++ b/lib/widgets/base/actionSheet/action_sheet.dart
@@ -0,0 +1,26 @@
+import 'package:flutter/material.dart';
+
+import 'action_sheet_ui.dart';
+import 'type.dart';
+
+void showActionSheet(
+ BuildContext context, {
+ required List actions,
+ bool showCancel = false,
+ required Function(ActionSheetItem) onConfirm,
+}) {
+ showModalBottomSheet(
+ context: context,
+ backgroundColor: Colors.white,
+ clipBehavior: Clip.antiAlias,
+ builder: (context) {
+ return ActionSheetUi(
+ actions: actions,
+ showCancel: showCancel,
+ onConfirm: onConfirm,
+ );
+ },
+ );
+}
+
+
diff --git a/lib/widgets/base/actionSheet/action_sheet_ui.dart b/lib/widgets/base/actionSheet/action_sheet_ui.dart
new file mode 100644
index 0000000..a82343a
--- /dev/null
+++ b/lib/widgets/base/actionSheet/action_sheet_ui.dart
@@ -0,0 +1,65 @@
+import 'package:flutter/material.dart';
+import 'package:go_router/go_router.dart';
+
+import 'type.dart';
+
+class ActionSheetUi extends StatelessWidget {
+ final List actions;
+ final bool showCancel;
+ final double actionHeight = 50;
+ final Function(ActionSheetItem) onConfirm;
+
+ const ActionSheetUi({
+ super.key,
+ required this.actions,
+ this.showCancel = false,
+ required this.onConfirm,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return SizedBox(
+ width: double.infinity,
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ ...actions.map((item) {
+ return InkWell(
+ onTap: () {
+ onConfirm(item);
+ context.pop();
+ },
+ child: Container(
+ height: actionHeight,
+ alignment: Alignment.center,
+ child: Text(item.title),
+ ),
+ );
+ }),
+ Visibility(
+ visible: showCancel,
+ child: Column(
+ children: [
+ Container(
+ width: double.infinity,
+ height: 8,
+ color: const Color.fromRGBO(238, 239, 243, 1.0),
+ ),
+ InkWell(
+ onTap: () {
+ context.pop();
+ },
+ child: Container(
+ height: actionHeight,
+ alignment: Alignment.center,
+ child: const Text("取消"),
+ ),
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/lib/widgets/base/actionSheet/type.dart b/lib/widgets/base/actionSheet/type.dart
new file mode 100644
index 0000000..4d55b7f
--- /dev/null
+++ b/lib/widgets/base/actionSheet/type.dart
@@ -0,0 +1,18 @@
+import 'package:flutter/material.dart';
+
+/// 底部弹窗类型
+class ActionSheetItem {
+ String title; //标题
+ int value;
+ bool disabled; //是否禁用
+ Color color; //选项颜色
+ Widget? child;
+
+ ActionSheetItem({
+ required this.title,
+ this.value = 0,
+ this.disabled = false,
+ this.color = Colors.black,
+ this.child,
+ });
+}
diff --git a/lib/widgets/base/button/index.dart b/lib/widgets/base/button/index.dart
index 3c61852..c2ad5b1 100644
--- a/lib/widgets/base/button/index.dart
+++ b/lib/widgets/base/button/index.dart
@@ -6,18 +6,20 @@ import '../config/config.dart';
class Button extends StatelessWidget {
final double? width;
final String text;
+ final TextStyle textStyle;
final ThemeType type;
final BorderRadius radius;
- final VoidCallback onPressed;
+ final VoidCallback? onPressed;
final bool loading;
final bool disabled;
const Button({
super.key,
this.width,
+ this.textStyle = const TextStyle(),
this.radius = const BorderRadius.all(Radius.circular(80)),
required this.text,
- required this.onPressed,
+ this.onPressed,
this.type = ThemeType.primary,
this.loading = false,
this.disabled = false,
@@ -34,7 +36,7 @@ class Button extends StatelessWidget {
};
return Opacity(
- opacity: disabled || loading ? 0.5 : 1,
+ opacity: disabled || loading ? 0.5 : 1,
child: Container(
width: width,
decoration: bgDecoration.copyWith(borderRadius: radius),
@@ -62,7 +64,7 @@ class Button extends StatelessWidget {
),
Text(
text,
- style: TextStyle(
+ style: textStyle.copyWith(
color: type != ThemeType.info ? Colors.white : Colors.black,
),
textAlign: TextAlign.center,
diff --git a/lib/widgets/common/preview/file_previewer.dart b/lib/widgets/common/preview/file_previewer.dart
index cf7e243..6c739a2 100644
--- a/lib/widgets/common/preview/file_previewer.dart
+++ b/lib/widgets/common/preview/file_previewer.dart
@@ -40,6 +40,9 @@ class FilePreviewer extends StatelessWidget {
child = InteractiveViewer(
child: CachedNetworkImage(
imageUrl: url,
+ placeholder: (_, __) => const Center(
+ child: CircularProgressIndicator(),
+ ),
),
);
} else if (_isPdf(suffix)) {
diff --git a/lib/widgets/room/core/count_down_vm.dart b/lib/widgets/room/core/count_down_vm.dart
new file mode 100644
index 0000000..209d5a2
--- /dev/null
+++ b/lib/widgets/room/core/count_down_vm.dart
@@ -0,0 +1,93 @@
+import 'dart:async';
+
+import 'package:app/data/models/meeting_room_dto.dart';
+import 'package:app/utils/time.dart';
+import 'package:flutter/cupertino.dart';
+
+class CountDownVM extends ChangeNotifier {
+ MeetingRoomDto? roomInfo;
+
+ ///会议开始倒计时秒数
+ Timer? _startTime;
+ int _startCountDown = 0;
+
+ ///会议结束倒计时秒数
+ Timer? _endTime;
+ int _endCountDown = -1;
+
+ ///会议进行中的秒数
+ int studyTime = 0;
+
+ int get startCountDown => _startCountDown;
+
+ int get endCountDown => _endCountDown;
+
+ ///是否能开始自习室
+ bool get canEnterRoom {
+ final now = DateTime.now();
+ if (now.isAfter(parseTime(roomInfo!.startTime))) {
+ return true;
+ }
+ return false;
+ }
+
+ //绑定
+ void bind(MeetingRoomDto info) {
+ if (roomInfo == info) return;
+ roomInfo = info;
+ _startEndCountdown();
+ //如果会议室结束,停止计时器
+ if (roomInfo?.roomStatus == 2) {
+ _endTime?.cancel();
+ }
+ }
+
+ ///启动距离会议结束还有多少秒
+ void _startEndCountdown() {
+ if (roomInfo!.actualStartTime.isEmpty || roomInfo!.roomStatus != 1) return;
+ _endTime?.cancel();
+
+ DateTime endTime = parseTime(roomInfo!.endTime);
+ DateTime startTime = DateTime.parse(roomInfo!.actualStartTime);
+
+ _endCountDown = endTime.difference(startTime).inSeconds;
+
+ _endTime = Timer.periodic(Duration(seconds: 1), (timer) {
+ _endCountDown--;
+ studyTime++;
+ if (_endCountDown <= 0) {
+ _endTime?.cancel();
+ }
+ notifyListeners();
+ });
+ }
+
+ ///启动距离会议开始还有多少秒
+ void startStartCountdown() {
+ if (roomInfo?.roomStatus != 0) return;
+ _startTime?.cancel();
+ final now = DateTime.now();
+ final startTime = parseTime(roomInfo!.startTime);
+
+ _startCountDown = startTime.difference(now).inSeconds;
+ if (_startCountDown <= 0) {
+ return;
+ }
+
+ _startTime = Timer.periodic(Duration(seconds: 1), (timer) {
+ _startCountDown--;
+ if (_startCountDown <= 0) {
+ _startTime?.cancel();
+ _startTime = null;
+ }
+ notifyListeners();
+ });
+ }
+
+ @override
+ void dispose() {
+ _startTime?.cancel();
+ _endTime?.cancel();
+ super.dispose();
+ }
+}
diff --git a/lib/widgets/room/file_drawer.dart b/lib/widgets/room/file_drawer.dart
index ace88f1..5113ead 100644
--- a/lib/widgets/room/file_drawer.dart
+++ b/lib/widgets/room/file_drawer.dart
@@ -1,6 +1,14 @@
+import 'dart:io';
+
import 'package:app/config/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';
import 'package:app/widgets/base/button/index.dart';
+import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
+import 'package:flutter_easyloading/flutter_easyloading.dart';
+import 'package:image_picker/image_picker.dart';
import '../common/preview/file_previewer.dart';
@@ -8,6 +16,9 @@ import '../common/preview/file_previewer.dart';
void showFileDialog(
BuildContext context, {
bool isUpload = true,
+ String? name,
+ List files = const [],
+ ValueChanged>? onConfirm,
}) {
showGeneralDialog(
context: context,
@@ -16,7 +27,10 @@ void showFileDialog(
barrierLabel: "RightSheet",
pageBuilder: (context, animation, secondaryAnimation) {
return FileDrawer(
+ name: name,
isUpload: isUpload,
+ files: files,
+ onConfirm: onConfirm,
);
},
transitionBuilder: (context, animation, secondaryAnimation, child) {
@@ -31,15 +45,99 @@ void showFileDialog(
///文件弹窗
class FileDrawer extends StatefulWidget {
+ final String? name;
+ final List files;
final bool isUpload;
+ final ValueChanged>? onConfirm;
- const FileDrawer({super.key, this.isUpload = true});
+ const FileDrawer({
+ super.key,
+ this.name,
+ this.isUpload = true,
+ this.files = const [],
+ this.onConfirm,
+ });
@override
State createState() => _FileDrawerState();
}
class _FileDrawerState extends State {
+ ///文件列表
+ List _fileList = [];
+
+ @override
+ void initState() {
+ super.initState();
+ _fileList = List.from(widget.files);
+ }
+
+ ///打开选择面板
+ void _handOpenActionSheet() {
+ showActionSheet(
+ context,
+ showCancel: true,
+ actions: [
+ ActionSheetItem(title: "拍照", value: 1),
+ ActionSheetItem(title: "选择图片", value: 2),
+ ActionSheetItem(title: "选择PDF", value: 3),
+ ],
+ onConfirm: (res) async {
+ List filesToUpload = [];
+ try {
+ if (res.value == 1) {
+ final ImagePicker picker = ImagePicker();
+ final XFile? photo = await picker.pickImage(source: ImageSource.camera);
+
+ if (photo == null) return; // 用户取消
+ filesToUpload.add(File(photo.path));
+ } else if (res.value == 2) {
+ //选择图片
+ final result = await FilePicker.platform.pickFiles(
+ allowMultiple: true,
+ type: FileType.image,
+ );
+ if (result == null) return;
+ filesToUpload.addAll(result.paths.whereType().map((e) => File(e)));
+ } else if (res.value == 3) {
+ //选择pdf
+ final result = await FilePicker.platform.pickFiles(
+ allowMultiple: true,
+ type: FileType.custom,
+ allowedExtensions: ['pdf'],
+ );
+ if (result == null) return;
+ filesToUpload.addAll(result.paths.whereType().map((e) => File(e)));
+ }
+ if (filesToUpload.isEmpty) return;
+ // 4) 上传文件
+ EasyLoading.show(status: "文件上传中");
+
+ final uploadTasks = filesToUpload.map((file) {
+ return QinUpload.upload(
+ file: file,
+ path: "room/classroom",
+ );
+ });
+ final List uploadedPaths = (await Future.wait(
+ uploadTasks,
+ )).whereType().toList();
+
+ EasyLoading.dismiss();
+
+ // 更新 UI
+ setState(() => _fileList.addAll(uploadedPaths));
+
+ // 回调
+ widget.onConfirm?.call(uploadedPaths);
+ } catch (e) {
+ print(e);
+ EasyLoading.showToast("文件上传失败");
+ }
+ },
+ );
+ }
+
@override
Widget build(BuildContext context) {
return Align(
@@ -52,39 +150,45 @@ class _FileDrawerState extends State {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
- '上传文件列表',
+ "${widget.name ?? ""}上传文件列表",
style: Theme.of(context).textTheme.titleSmall,
),
Expanded(
- child: ListView.separated(
- padding: EdgeInsets.symmetric(vertical: 15),
- itemBuilder: (_, index) {
- return InkWell(
- onTap: () {
- showFilePreviewer(
- context,
- url: "https://doaf.asia/api/assets/1/图/65252305_p0.jpg",
- );
- },
- child: Container(
- padding: EdgeInsets.symmetric(vertical: 10, horizontal: 10),
- decoration: BoxDecoration(
- color: Theme.of(context).colorScheme.surfaceContainer,
- borderRadius: BorderRadius.circular(5),
+ child: Visibility(
+ visible: _fileList.isNotEmpty,
+ replacement: Align(
+ child: Text("未上传文件"),
+ ),
+ child: ListView.separated(
+ padding: EdgeInsets.symmetric(vertical: 15),
+ itemBuilder: (_, index) {
+ String item = _fileList[index];
+ String suffix = item.split(".").last;
+ return InkWell(
+ key: Key(item),
+ onTap: () {
+ showFilePreviewer(context, url: item);
+ },
+ child: Container(
+ padding: EdgeInsets.symmetric(vertical: 10, horizontal: 10),
+ decoration: BoxDecoration(
+ color: Theme.of(context).colorScheme.surfaceContainer,
+ borderRadius: BorderRadius.circular(5),
+ ),
+ child: Text("文件${index + 1}.$suffix", style: TextStyle(fontSize: 14)),
),
- child: Text("文件1.png", style: TextStyle(fontSize: 14)),
- ),
- );
- },
- separatorBuilder: (_, __) => SizedBox(height: 15),
- itemCount: 15,
+ );
+ },
+ separatorBuilder: (_, __) => SizedBox(height: 15),
+ itemCount: _fileList.length,
+ ),
),
),
Visibility(
visible: widget.isUpload,
child: Button(
text: "上传",
- onPressed: () {},
+ onPressed: _handOpenActionSheet,
),
),
],
@@ -93,3 +197,12 @@ class _FileDrawerState extends State {
);
}
}
+
+///数据类
+class UploadFileItem {
+ String url;
+ String name;
+ bool loading;
+
+ UploadFileItem({required this.url, this.loading = false, required this.name});
+}
diff --git a/lib/widgets/room/other_widget.dart b/lib/widgets/room/other_widget.dart
new file mode 100644
index 0000000..fa3e854
--- /dev/null
+++ b/lib/widgets/room/other_widget.dart
@@ -0,0 +1,26 @@
+import 'package:flutter/material.dart';
+
+class HandRaiseButton extends StatelessWidget {
+ final void Function() onTap;
+ const HandRaiseButton({super.key, required this.onTap});
+
+ @override
+ Widget build(BuildContext context) {
+ return InkWell(
+ onTap: onTap,
+ child: Container(
+ height: 60,
+ width: 60,
+ decoration: BoxDecoration(
+ color: Colors.black12,
+ shape: BoxShape.circle,
+ ),
+ child: Icon(
+ Icons.back_hand_rounded,
+ color: Color(0xFFFDC400),
+ size: 24,
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/widgets/room/video_surface.dart b/lib/widgets/room/video_surface.dart
index 2900951..9bbcac0 100644
--- a/lib/widgets/room/video_surface.dart
+++ b/lib/widgets/room/video_surface.dart
@@ -1,46 +1,52 @@
-import 'package:app/config/config.dart';
+import 'package:app/request/dto/room/room_user_dto.dart';
+import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
-import 'package:agora_rtc_engine/agora_rtc_engine.dart';
-import '../../request/dto/room/rtc_token_dto.dart';
-
-/// 视频画面显示状态
-enum VideoState {
- /// 正常显示视频
- normal,
-
- /// 摄像头关闭
- closed,
-
- /// 掉线 / 未连接
- offline,
-
- /// 加载中(进房、拉流等)
- loading,
-
- /// 错误状态(拉流失败等)
- error,
-}
class VideoSurface extends StatelessWidget {
- final VideoState state;
+ final RoomUserDto user;
+ final Widget child;
- const VideoSurface({super.key, this.state = VideoState.normal});
+ const VideoSurface({
+ super.key,
+ required this.user,
+ required this.child,
+ });
@override
Widget build(BuildContext context) {
- String stateText = switch (state) {
- VideoState.closed => "摄像头已关闭",
- VideoState.offline => "掉线",
- VideoState.loading => "加载中",
- VideoState.error => "错误",
- _ => "未知",
- };
- //如果不是正常
- if (state != VideoState.normal) {
+ //摄像头是否关闭
+ if (user.cameraStatus == 0) {
return Align(
- child: Text(stateText, style: TextStyle(color: Colors.white70)),
+ child: ConstrainedBox(
+ constraints: const BoxConstraints(
+ maxWidth: 70,
+ ),
+ child: AspectRatio(
+ aspectRatio: 1,
+ child: Container(
+ clipBehavior: Clip.hardEdge,
+ decoration: BoxDecoration(
+ shape: BoxShape.circle,
+ border: Border.all(color: Colors.white, width: 1),
+ ),
+ child: CachedNetworkImage(
+ imageUrl: user.avatar,
+ fit: BoxFit.cover,
+ ),
+ ),
+ ),
+ ),
);
}
- return Container();
+ if (user.online == 0) {
+ return _empty('暂时离开');
+ }
+ return child;
+ }
+
+ Widget _empty(String title) {
+ return Center(
+ child: Text(title, style: TextStyle(color: Colors.white70)),
+ );
}
}
diff --git a/lib/widgets/version/version_dialog.dart b/lib/widgets/version/version_dialog.dart
new file mode 100644
index 0000000..7046de9
--- /dev/null
+++ b/lib/widgets/version/version_dialog.dart
@@ -0,0 +1,80 @@
+
+import 'dart:convert';
+
+import 'package:app/request/api/common_api.dart';
+import 'package:app/utils/common.dart';
+import 'package:dio/dio.dart';
+import 'package:flutter/material.dart';
+import 'package:package_info_plus/package_info_plus.dart';
+
+import 'version_ui.dart';
+
+
+///显示版本更新弹窗
+void showUpdateDialog(BuildContext context) async {
+ PackageInfo packageInfo = await PackageInfo.fromPlatform();
+ //如果是安卓
+ if (isAndroid()) {
+ //版本信息
+ var versionRes = await getAppVersionApi();
+
+ //比较版本
+ int compareResult = compareVersions(
+ packageInfo.version,
+ versionRes.latestVersion,
+ );
+ if (compareResult > -1) {
+ return;
+ }
+
+ //如果是最新版本
+ showGeneralDialog(
+ context: context,
+ barrierDismissible: false,
+ barrierLabel: "Update",
+ transitionDuration: const Duration(milliseconds: 300),
+ pageBuilder: (context, animation, secondaryAnimation) {
+ return PopScope(
+ canPop: false,
+ child: AppUpdateUi(
+ version: versionRes.latestVersion,
+ updateNotice: versionRes.updateContent,
+ uploadUrl: versionRes.downloadUrl,
+ ),
+ );
+ },
+ );
+ } else {
+ var res = await Dio().get("https://itunes.apple.com/lookup?bundleId=com.curainhealth.akhome");
+ Map resJson = json.decode(res.data);
+ if (resJson['results'].length == 0) {
+ return;
+ }
+ var newVersion = resJson['results'][0]['version'];
+ //比较版本
+ int compareResult = compareVersions(
+ packageInfo.version,
+ newVersion,
+ );
+ if (compareResult > 0) {
+ return;
+ }
+
+ }
+}
+
+///比较版本号
+int compareVersions(String version1, String version2) {
+ List v1Parts = version1.split('.');
+ List v2Parts = version2.split('.');
+ int length = v1Parts.length > v2Parts.length ? v1Parts.length : v2Parts.length;
+
+ for (int i = 0; i < length; i++) {
+ int v1Part = i < v1Parts.length ? int.tryParse(v1Parts[i]) ?? 0 : 0;
+ int v2Part = i < v2Parts.length ? int.tryParse(v2Parts[i]) ?? 0 : 0;
+
+ if (v1Part > v2Part) return 1;
+ if (v1Part < v2Part) return -1;
+ }
+ return 0;
+}
diff --git a/lib/widgets/version/version_ui.dart b/lib/widgets/version/version_ui.dart
new file mode 100644
index 0000000..bad0bfb
--- /dev/null
+++ b/lib/widgets/version/version_ui.dart
@@ -0,0 +1,174 @@
+import 'package:app/widgets/base/button/index.dart';
+import 'package:app/widgets/base/tag/index.dart';
+import 'package:app_installer/app_installer.dart';
+import 'package:flutter/material.dart';
+
+import '../../utils/transfer/download.dart';
+import '../base/config/config.dart';
+
+///下载状态枚举
+enum UploadState {
+ notStarted, //未开始下载
+ downloading, //下载中
+ completed, //下载完毕
+}
+
+class AppUpdateUi extends StatefulWidget {
+ final String version;
+ final List updateNotice;
+ final String uploadUrl; //下载地址
+
+ const AppUpdateUi({
+ super.key,
+ required this.version,
+ required this.updateNotice,
+ required this.uploadUrl,
+ });
+
+ @override
+ State createState() => _UpdateUiState();
+}
+
+class _UpdateUiState extends State {
+ int _uploadProgress = 0; //下载进度
+ UploadState _uploadState = UploadState.notStarted;
+
+ @override
+ void initState() {
+ super.initState();
+ getLocalApk();
+ }
+
+ ///读取本地是否有下载记录
+ void getLocalApk() async {
+ String url = await LocalDownload.getFilePath(url: widget.uploadUrl, path: '/apk');
+ if (url.isNotEmpty) {
+ setState(() {
+ _uploadState = UploadState.completed;
+ });
+ }
+ }
+
+ ///下载apk
+ void _handUploadApk() async {
+ if (_uploadState == UploadState.notStarted) {
+ setState(() {
+ _uploadState = UploadState.downloading;
+ });
+ LocalDownload.downLoadFile(
+ url: widget.uploadUrl,
+ path: "/apk",
+ onProgress: (double double) {
+ setState(() {
+ _uploadProgress = double.toInt();
+ });
+ },
+ onDone: (apk) async {
+ setState(() {
+ _uploadState = UploadState.completed;
+ });
+ AppInstaller.installApk(apk);
+ },
+ );
+ } else if (_uploadState == UploadState.completed) {
+ String url = await LocalDownload.getFilePath(url: widget.uploadUrl, path: '/apk');
+ AppInstaller.installApk(url);
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ String text;
+ if (_uploadState == UploadState.downloading) {
+ text = "$_uploadProgress%";
+ } else if (_uploadState == UploadState.completed) {
+ text = '安装';
+ } else {
+ text = '立即升级';
+ }
+ return IntrinsicHeight(
+ child: Container(
+ color: Colors.transparent,
+ padding: EdgeInsets.symmetric(horizontal: 40),
+ alignment: Alignment.center,
+ child: ConstrainedBox(
+ constraints: BoxConstraints(
+ maxWidth: 500,
+ ),
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Stack(
+ fit: StackFit.passthrough,
+ children: [
+ Image.asset("assets/image/version_bg.png"),
+ Positioned(
+ top: 0,
+ left: 0,
+ right: 0,
+ bottom: 0,
+ child: DefaultTextStyle(
+ style: TextStyle(
+ fontSize: 16,
+ color: Colors.white,
+ fontWeight: FontWeight.w700,
+ ),
+ child: FractionalTranslation(
+ translation: Offset(0.35, 0.3),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.start,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text("发现新版本"),
+ SizedBox(width: 10),
+ Tag(
+ text: "V ${widget.version}",
+ type: ThemeType.warning,
+ ),
+ ],
+ ),
+ ),
+ ),
+ ),
+ ],
+ ),
+ Material(
+ borderRadius: BorderRadius.only(
+ bottomLeft: Radius.circular(20),
+ bottomRight: Radius.circular(20),
+ ),
+ child: Container(
+ padding: EdgeInsets.all(15),
+ width: double.infinity,
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ ...widget.updateNotice.asMap().entries.map((entry) {
+ final index = entry.key;
+ final item = entry.value;
+ return Padding(
+ padding: const EdgeInsets.symmetric(vertical: 5),
+ child: Text("${index + 1}.$item"),
+ );
+ }),
+ Container(
+ margin: EdgeInsets.only(top: 20),
+ height: 40,
+ child: Button(
+ text: text,
+ onPressed: _uploadState == UploadState.downloading
+ ? null
+ : _handUploadApk,
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/pubspec.lock b/pubspec.lock
index 382f1bd..64ea3b6 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -9,6 +9,22 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "6.5.3"
+ app_installer:
+ dependency: "direct main"
+ description:
+ name: app_installer
+ sha256: "4c3a9268b53ead9a915ef79cd3988e28c72719fb78143867f9fed4bd4d8c1cfd"
+ url: "https://pub.flutter-io.cn"
+ source: hosted
+ version: "1.3.1"
+ args:
+ dependency: transitive
+ description:
+ name: args
+ sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
+ url: "https://pub.flutter-io.cn"
+ source: hosted
+ version: "2.7.0"
async:
dependency: transitive
description:
@@ -73,8 +89,16 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.19.1"
- crypto:
+ cross_file:
dependency: transitive
+ description:
+ name: cross_file
+ sha256: "701dcfc06da0882883a2657c445103380e53e647060ad8d9dfb710c100996608"
+ url: "https://pub.flutter-io.cn"
+ source: hosted
+ version: "0.3.5+1"
+ crypto:
+ dependency: "direct main"
description:
name: crypto
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
@@ -89,6 +113,14 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.0.8"
+ dbus:
+ dependency: transitive
+ description:
+ name: dbus
+ sha256: "79e0c23480ff85dc68de79e2cd6334add97e48f7f4865d17686dd6ea81a47e8c"
+ url: "https://pub.flutter-io.cn"
+ source: hosted
+ version: "0.7.11"
dio:
dependency: "direct main"
description:
@@ -129,6 +161,46 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "7.0.1"
+ file_picker:
+ dependency: "direct main"
+ description:
+ name: file_picker
+ sha256: "7872545770c277236fd32b022767576c562ba28366204ff1a5628853cf8f2200"
+ url: "https://pub.flutter-io.cn"
+ source: hosted
+ version: "10.3.7"
+ file_selector_linux:
+ dependency: transitive
+ description:
+ name: file_selector_linux
+ sha256: "2567f398e06ac72dcf2e98a0c95df2a9edd03c2c2e0cacd4780f20cdf56263a0"
+ url: "https://pub.flutter-io.cn"
+ source: hosted
+ version: "0.9.4"
+ file_selector_macos:
+ dependency: transitive
+ description:
+ name: file_selector_macos
+ sha256: "19124ff4a3d8864fdc62072b6a2ef6c222d55a3404fe14893a3c02744907b60c"
+ url: "https://pub.flutter-io.cn"
+ source: hosted
+ version: "0.9.4+4"
+ file_selector_platform_interface:
+ dependency: transitive
+ description:
+ name: file_selector_platform_interface
+ sha256: "35e0bd61ebcdb91a3505813b055b09b79dfdc7d0aee9c09a7ba59ae4bb13dc85"
+ url: "https://pub.flutter-io.cn"
+ source: hosted
+ version: "2.7.0"
+ file_selector_windows:
+ dependency: transitive
+ description:
+ name: file_selector_windows
+ sha256: "62197474ae75893a62df75939c777763d39c2bc5f73ce5b88497208bc269abfd"
+ url: "https://pub.flutter-io.cn"
+ source: hosted
+ version: "0.9.3+5"
fixnum:
dependency: transitive
description:
@@ -187,6 +259,14 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.4.3"
+ flutter_plugin_android_lifecycle:
+ dependency: transitive
+ description:
+ name: flutter_plugin_android_lifecycle
+ sha256: c2fe1001710127dfa7da89977a08d591398370d099aacdaa6d44da7eb14b8476
+ url: "https://pub.flutter-io.cn"
+ source: hosted
+ version: "2.0.31"
flutter_screenutil:
dependency: "direct main"
description:
@@ -237,6 +317,70 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "4.1.2"
+ image_picker:
+ dependency: "direct main"
+ description:
+ name: image_picker
+ sha256: "784210112be18ea55f69d7076e2c656a4e24949fa9e76429fe53af0c0f4fa320"
+ url: "https://pub.flutter-io.cn"
+ source: hosted
+ version: "1.2.1"
+ image_picker_android:
+ dependency: transitive
+ description:
+ name: image_picker_android
+ sha256: "28f3987ca0ec702d346eae1d90eda59603a2101b52f1e234ded62cff1d5cfa6e"
+ url: "https://pub.flutter-io.cn"
+ source: hosted
+ version: "0.8.13+1"
+ image_picker_for_web:
+ dependency: transitive
+ description:
+ name: image_picker_for_web
+ sha256: "66257a3191ab360d23a55c8241c91a6e329d31e94efa7be9cf7a212e65850214"
+ url: "https://pub.flutter-io.cn"
+ source: hosted
+ version: "3.1.1"
+ image_picker_ios:
+ dependency: transitive
+ description:
+ name: image_picker_ios
+ sha256: eb06fe30bab4c4497bad449b66448f50edcc695f1c59408e78aa3a8059eb8f0e
+ url: "https://pub.flutter-io.cn"
+ source: hosted
+ version: "0.8.13"
+ image_picker_linux:
+ dependency: transitive
+ description:
+ name: image_picker_linux
+ sha256: "1f81c5f2046b9ab724f85523e4af65be1d47b038160a8c8deed909762c308ed4"
+ url: "https://pub.flutter-io.cn"
+ source: hosted
+ version: "0.2.2"
+ image_picker_macos:
+ dependency: transitive
+ description:
+ name: image_picker_macos
+ sha256: d58cd9d67793d52beefd6585b12050af0a7663c0c2a6ece0fb110a35d6955e04
+ url: "https://pub.flutter-io.cn"
+ source: hosted
+ version: "0.2.2"
+ image_picker_platform_interface:
+ dependency: transitive
+ description:
+ name: image_picker_platform_interface
+ sha256: "567e056716333a1647c64bb6bd873cff7622233a5c3f694be28a583d4715690c"
+ url: "https://pub.flutter-io.cn"
+ source: hosted
+ version: "2.11.1"
+ image_picker_windows:
+ dependency: transitive
+ description:
+ name: image_picker_windows
+ sha256: d248c86554a72b5495a31c56f060cf73a41c7ff541689327b1a7dbccc33adfae
+ url: "https://pub.flutter-io.cn"
+ source: hosted
+ version: "0.2.2"
intl:
dependency: "direct main"
description:
@@ -485,6 +629,14 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.2.1"
+ petitparser:
+ dependency: transitive
+ description:
+ name: petitparser
+ sha256: "1a97266a94f7350d30ae522c0af07890c70b8e62c71e8e3920d1db4d23c057d1"
+ url: "https://pub.flutter-io.cn"
+ source: hosted
+ version: "7.0.1"
platform:
dependency: transitive
description:
@@ -722,6 +874,22 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "15.0.0"
+ wakelock_plus:
+ dependency: "direct main"
+ description:
+ name: wakelock_plus
+ sha256: "61713aa82b7f85c21c9f4cd0a148abd75f38a74ec645fcb1e446f882c82fd09b"
+ url: "https://pub.flutter-io.cn"
+ source: hosted
+ version: "1.3.3"
+ wakelock_plus_platform_interface:
+ dependency: transitive
+ description:
+ name: wakelock_plus_platform_interface
+ sha256: "036deb14cd62f558ca3b73006d52ce049fabcdcb2eddfe0bf0fe4e8a943b5cf2"
+ url: "https://pub.flutter-io.cn"
+ source: hosted
+ version: "1.3.0"
web:
dependency: transitive
description:
@@ -746,6 +914,14 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.1.0"
+ xml:
+ dependency: transitive
+ description:
+ name: xml
+ sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025"
+ url: "https://pub.flutter-io.cn"
+ source: hosted
+ version: "6.6.1"
sdks:
dart: ">=3.8.1 <4.0.0"
- flutter: ">=3.31.0-0.0.pre"
+ flutter: ">=3.32.0"
diff --git a/pubspec.yaml b/pubspec.yaml
index 1aa9f51..1987257 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -2,7 +2,7 @@ name: app
description: "A new Flutter project."
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
-version: 1.0.0+1
+version: 1.1.1
environment:
sdk: ^3.8.1
@@ -28,6 +28,11 @@ dependencies:
flutter_cached_pdfview: ^0.4.3
skeletonizer: ^2.1.0+1
agora_rtc_engine: ^6.5.3
+ crypto: ^3.0.0
+ file_picker: ^10.3.7
+ app_installer: ^1.3.1
+ wakelock_plus: ^1.3.3
+ image_picker: ^1.2.0
dev_dependencies:
flutter_test: