自习室优化ok
This commit is contained in:
@@ -1,10 +1,18 @@
|
|||||||
|
import java.util.Properties
|
||||||
|
import java.io.FileInputStream
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
import java.time.format.DateTimeFormatter
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("com.android.application")
|
id("com.android.application")
|
||||||
id("kotlin-android")
|
id("kotlin-android")
|
||||||
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
|
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
|
||||||
id("dev.flutter.flutter-gradle-plugin")
|
id("dev.flutter.flutter-gradle-plugin")
|
||||||
}
|
}
|
||||||
|
val keystorePropertiesFile = rootProject.file("key.properties")
|
||||||
|
val keystoreProperties = Properties().apply {
|
||||||
|
load(FileInputStream(keystorePropertiesFile))
|
||||||
|
}
|
||||||
android {
|
android {
|
||||||
namespace = "com.zkwl.xueguang"
|
namespace = "com.zkwl.xueguang"
|
||||||
compileSdk = flutter.compileSdkVersion
|
compileSdk = flutter.compileSdkVersion
|
||||||
@@ -18,7 +26,14 @@ android {
|
|||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
jvmTarget = JavaVersion.VERSION_11.toString()
|
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 {
|
defaultConfig {
|
||||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||||
applicationId = "com.zkwl.xueguang"
|
applicationId = "com.zkwl.xueguang"
|
||||||
@@ -28,13 +43,31 @@ android {
|
|||||||
targetSdk = flutter.targetSdkVersion
|
targetSdk = flutter.targetSdkVersion
|
||||||
versionCode = flutter.versionCode
|
versionCode = flutter.versionCode
|
||||||
versionName = flutter.versionName
|
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 {
|
buildTypes {
|
||||||
release {
|
getByName("release") {
|
||||||
// TODO: Add your own signing config for the release build.
|
signingConfig = signingConfigs.getByName("release")
|
||||||
// Signing with the debug keys for now, so `flutter run --release` works.
|
isMinifyEnabled = true // 开启混淆和代码压缩
|
||||||
signingConfig = signingConfigs.getByName("debug")
|
}
|
||||||
|
}
|
||||||
|
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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,6 +47,18 @@
|
|||||||
<meta-data
|
<meta-data
|
||||||
android:name="flutterEmbedding"
|
android:name="flutterEmbedding"
|
||||||
android:value="2" />
|
android:value="2" />
|
||||||
|
|
||||||
|
|
||||||
|
<!-- 用于更新检查-->
|
||||||
|
<provider
|
||||||
|
android:name="androidx.core.content.FileProvider"
|
||||||
|
android:authorities="${applicationId}.fileProvider"
|
||||||
|
android:exported="false"
|
||||||
|
android:grantUriPermissions="true">
|
||||||
|
<meta-data
|
||||||
|
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||||
|
android:resource="@xml/file_paths" />
|
||||||
|
</provider>
|
||||||
</application>
|
</application>
|
||||||
<!-- Required to query activities that can process text, see:
|
<!-- Required to query activities that can process text, see:
|
||||||
https://developer.android.com/training/package-visibility and
|
https://developer.android.com/training/package-visibility and
|
||||||
|
|||||||
7
android/app/src/main/res/xml/file_paths.xml
Normal file
7
android/app/src/main/res/xml/file_paths.xml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<paths xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<!-- 允许访问缓存目录下的apk目录 -->
|
||||||
|
<cache-path
|
||||||
|
name="apk_cache"
|
||||||
|
path="apk/" />
|
||||||
|
</paths>
|
||||||
BIN
assets/image/version_bg.png
Normal file
BIN
assets/image/version_bg.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 47 KiB |
1
build.dev.sh
Normal file
1
build.dev.sh
Normal file
@@ -0,0 +1 @@
|
|||||||
|
flutter build apk -Penv=dev
|
||||||
26
build.prod.sh
Normal file
26
build.prod.sh
Normal file
@@ -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."
|
||||||
78
lib/data/models/meeting_room_dto.dart
Normal file
78
lib/data/models/meeting_room_dto.dart
Normal file
@@ -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<String, dynamic> 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<String, dynamic> 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import 'package:app/config/theme/base/app_theme_ext.dart';
|
import 'package:app/config/theme/base/app_theme_ext.dart';
|
||||||
import 'package:app/pages/student/home/viewmodel/s_home_vm.dart';
|
import 'package:app/pages/student/home/viewmodel/s_home_vm.dart';
|
||||||
import 'package:app/request/api/room_api.dart';
|
import 'package:app/widgets/version/version_dialog.dart';
|
||||||
import 'package:app/request/dto/room/room_type_dto.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
import 'widgets/tip_card.dart';
|
||||||
|
|
||||||
import 'today/s_today_card.dart';
|
import 'today/s_today_card.dart';
|
||||||
import 'widgets/user_header.dart';
|
import 'widgets/user_header.dart';
|
||||||
@@ -13,6 +13,7 @@ class SHomePage extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
showUpdateDialog(context);
|
||||||
return ChangeNotifierProvider(
|
return ChangeNotifierProvider(
|
||||||
create: (_) => SHomeVm(),
|
create: (_) => SHomeVm(),
|
||||||
child: _HomeView(),
|
child: _HomeView(),
|
||||||
@@ -21,7 +22,7 @@ class SHomePage extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _HomeView extends StatelessWidget {
|
class _HomeView extends StatelessWidget {
|
||||||
const _HomeView({super.key});
|
const _HomeView();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -35,6 +36,8 @@ class _HomeView extends StatelessWidget {
|
|||||||
padding: EdgeInsets.all(context.pagePadding),
|
padding: EdgeInsets.all(context.pagePadding),
|
||||||
children: [
|
children: [
|
||||||
STodayCard(),
|
STodayCard(),
|
||||||
|
TipCard1(),
|
||||||
|
TipCard2()
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'package:app/config/theme/base/app_theme_ext.dart';
|
|||||||
import 'package:app/pages/student/home/viewmodel/s_home_vm.dart';
|
import 'package:app/pages/student/home/viewmodel/s_home_vm.dart';
|
||||||
import 'package:app/router/route_paths.dart';
|
import 'package:app/router/route_paths.dart';
|
||||||
import 'package:app/utils/permission.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/button/index.dart';
|
||||||
import 'package:app/widgets/base/empty/index.dart';
|
import 'package:app/widgets/base/empty/index.dart';
|
||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
@@ -102,7 +103,7 @@ class _STodayCardState extends State<STodayCard> {
|
|||||||
children: [
|
children: [
|
||||||
Text(vm.roomInfo?.teacherName ?? ""),
|
Text(vm.roomInfo?.teacherName ?? ""),
|
||||||
Text(
|
Text(
|
||||||
vm.roomInfo?.teacherBackground ?? "",
|
vm.roomInfo?.teacherSchoolName ?? "",
|
||||||
style: Theme.of(context).textTheme.labelLarge,
|
style: Theme.of(context).textTheme.labelLarge,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -121,7 +122,7 @@ class _STodayCardState extends State<STodayCard> {
|
|||||||
),
|
),
|
||||||
InfoItem(
|
InfoItem(
|
||||||
label: "自习时长",
|
label: "自习时长",
|
||||||
value: "${vm.roomMinutes} 分钟",
|
value: "${formatSeconds(vm.roomMinutes * 60, 'hh小时mm分钟')} ",
|
||||||
icon: RemixIcons.timer_line,
|
icon: RemixIcons.timer_line,
|
||||||
color: context.success,
|
color: context.success,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import 'package:app/request/api/room_api.dart';
|
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:app/utils/time.dart';
|
||||||
import 'package:flutter/cupertino.dart';
|
import 'package:flutter/cupertino.dart';
|
||||||
|
|
||||||
class SHomeVm extends ChangeNotifier {
|
class SHomeVm extends ChangeNotifier {
|
||||||
RoomInfoDto? roomInfo;
|
RoomListItemDto ? roomInfo;
|
||||||
bool loading = true;
|
bool loading = true;
|
||||||
|
|
||||||
SHomeVm() {
|
SHomeVm() {
|
||||||
|
|||||||
@@ -1,52 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:remixicon/remixicon.dart';
|
|
||||||
|
|
||||||
class FeatureStatic extends StatelessWidget {
|
|
||||||
const FeatureStatic({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final List<FeatureItem> items = [
|
|
||||||
FeatureItem("视频陪学", "老师全程在线监督", RemixIcons.video_on_ai_line),
|
|
||||||
FeatureItem("举手提问", "实时互动解答疑惑", RemixIcons.hand),
|
|
||||||
FeatureItem("拍照题目", "快速上传问题截图", RemixIcons.camera_2_line),
|
|
||||||
FeatureItem("文件共享", "支持PDF等多种格式", RemixIcons.upload_2_line),
|
|
||||||
];
|
|
||||||
return Container(
|
|
||||||
margin: EdgeInsets.only(top: 15),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
spacing: 15,
|
|
||||||
children: [
|
|
||||||
Text("核心功能", style: TextStyle(fontSize: 18)),
|
|
||||||
GridView.builder(
|
|
||||||
shrinkWrap: true,
|
|
||||||
physics: NeverScrollableScrollPhysics(),
|
|
||||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
|
||||||
crossAxisCount: 4,
|
|
||||||
mainAxisSpacing: 15,
|
|
||||||
crossAxisSpacing: 15,
|
|
||||||
mainAxisExtent: 120
|
|
||||||
),
|
|
||||||
itemBuilder: (_, index) {
|
|
||||||
return Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.white
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
itemCount: items.length,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class FeatureItem {
|
|
||||||
final String title;
|
|
||||||
final String desc;
|
|
||||||
final IconData icon;
|
|
||||||
|
|
||||||
FeatureItem(this.title, this.desc, this.icon);
|
|
||||||
}
|
|
||||||
131
lib/pages/student/home/widgets/tip_card.dart
Normal file
131
lib/pages/student/home/widgets/tip_card.dart
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
import 'package:app/widgets/base/card/g_card.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:remixicon/remixicon.dart';
|
||||||
|
|
||||||
|
class TipCard1 extends StatelessWidget {
|
||||||
|
const TipCard1({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final list = [
|
||||||
|
{
|
||||||
|
"icon": RemixIcons.video_on_line,
|
||||||
|
"title": "视频陪学",
|
||||||
|
"desc": "老师全程在线监督",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"icon": RemixIcons.hand,
|
||||||
|
"title": "举手提问",
|
||||||
|
"desc": "实时互动解答疑惑",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"icon": RemixIcons.camera_ai_line,
|
||||||
|
"title": "拍照题目",
|
||||||
|
"desc": "快速上传问题截图",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"icon": RemixIcons.file_upload_line,
|
||||||
|
"title": "文件共享",
|
||||||
|
"desc": "支持PDF等多种格式",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
return Container(
|
||||||
|
margin: EdgeInsets.only(top: 15),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
spacing: 10,
|
||||||
|
children: [
|
||||||
|
Text("功能特色"),
|
||||||
|
Row(
|
||||||
|
spacing: 10,
|
||||||
|
children: list.map((t) {
|
||||||
|
final item = t as dynamic;
|
||||||
|
return Expanded(
|
||||||
|
child: GCard(
|
||||||
|
child: Column(
|
||||||
|
spacing: 5,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: EdgeInsets.all(10),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context).primaryColor.withValues(alpha: 0.2),
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
item['icon'],
|
||||||
|
color: Theme.of(context).primaryColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(item['title'],style: Theme.of(context).textTheme.bodySmall),
|
||||||
|
Text(
|
||||||
|
item['desc'],
|
||||||
|
style: Theme.of(context).textTheme.labelMedium,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class TipCard2 extends StatelessWidget {
|
||||||
|
const TipCard2({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final tipList = [
|
||||||
|
"请保持摄像头开启,确保学习状态可见",
|
||||||
|
"遇到问题可随时举手向老师提问",
|
||||||
|
"建议准备好学习资料,提高学习效率",
|
||||||
|
"自习期间请保持安静,避免打扰他人",
|
||||||
|
];
|
||||||
|
return Container(
|
||||||
|
margin: EdgeInsets.only(top: 15),
|
||||||
|
padding: EdgeInsets.all(15),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Color(0xfffffbeb),
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
border: Border.all(
|
||||||
|
color: Color(0xfffee685),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
margin: EdgeInsets.only(bottom: 10),
|
||||||
|
child: Text("温馨提示"),
|
||||||
|
),
|
||||||
|
ListView.separated(
|
||||||
|
shrinkWrap: true,
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
itemBuilder: (_, index) {
|
||||||
|
return Row(
|
||||||
|
spacing: 4,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 5,
|
||||||
|
height: 5,
|
||||||
|
decoration: BoxDecoration(shape: BoxShape.circle, color: Colors.black),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
tipList[index],
|
||||||
|
style: Theme.of(context).textTheme.labelLarge,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
separatorBuilder: (_, __) => SizedBox(height: 3),
|
||||||
|
itemCount: tipList.length,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,12 +1,13 @@
|
|||||||
import 'package:app/pages/student/room/viewmodel/stu_room_vm.dart';
|
|
||||||
import 'package:app/widgets/room/file_drawer.dart';
|
import 'package:app/widgets/room/file_drawer.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:remixicon/remixicon.dart';
|
import 'package:remixicon/remixicon.dart';
|
||||||
|
import '../viewmodel/stu_room_vm.dart';
|
||||||
|
|
||||||
class BottomBar extends StatefulWidget {
|
class BottomBar extends StatefulWidget {
|
||||||
const BottomBar({super.key});
|
final void Function()? onTap;
|
||||||
|
|
||||||
|
const BottomBar({super.key, this.onTap});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<BottomBar> createState() => _BottomBarState();
|
State<BottomBar> createState() => _BottomBarState();
|
||||||
@@ -15,51 +16,77 @@ class BottomBar extends StatefulWidget {
|
|||||||
class _BottomBarState extends State<BottomBar> {
|
class _BottomBarState extends State<BottomBar> {
|
||||||
///显示文件
|
///显示文件
|
||||||
void _handShowFile() {
|
void _handShowFile() {
|
||||||
showFileDialog(context);
|
final vm = context.read<StuRoomVM>();
|
||||||
|
if (vm.selfInfo == null) return;
|
||||||
|
showFileDialog(
|
||||||
|
context,
|
||||||
|
files: vm.selfInfo!.filesList,
|
||||||
|
onConfirm: (file) {
|
||||||
|
vm.uploadFile(file);
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final vm = context.watch<StuRoomVM>();
|
||||||
|
|
||||||
|
if (vm.roomInfo.roomStatus != 1) {
|
||||||
|
return SizedBox();
|
||||||
|
}
|
||||||
return Container(
|
return Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Color(0xff232426),
|
color: Color(0xff232426),
|
||||||
),
|
),
|
||||||
height: 70,
|
height: 70,
|
||||||
child: Consumer<StuRoomVM>(
|
child: Consumer<StuRoomVM>(
|
||||||
builder: (context,vm,_) {
|
builder: (context, vm, _) {
|
||||||
//摄像头开关
|
//摄像头开关
|
||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
BarItem(
|
BarItem(
|
||||||
title: "摄像头",
|
title: "摄像头",
|
||||||
icon: vm.cameraOpen ? RemixIcons.video_on_fill : RemixIcons.video_off_fill,
|
icon: vm.cameraClose ? RemixIcons.video_off_fill : RemixIcons.video_on_fill,
|
||||||
isOff: !vm.cameraOpen,
|
isOff: vm.cameraClose,
|
||||||
onTap: vm.changeCameraSwitch,
|
onTap: () {
|
||||||
|
vm.changeCameraSwitch(value: vm.cameraClose);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
BarItem(
|
BarItem(
|
||||||
title: "麦克风",
|
title: "麦克风",
|
||||||
icon: vm.micOpen ? RemixIcons.mic_fill : RemixIcons.mic_off_fill,
|
icon: vm.micClose ? RemixIcons.mic_off_fill : RemixIcons.mic_fill,
|
||||||
isOff: !vm.micOpen,
|
isOff: vm.micClose,
|
||||||
onTap: vm.changeMicSwitch,
|
onTap: () {
|
||||||
|
vm.changeMicSwitch(value: vm.micClose);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
BarItem(
|
BarItem(
|
||||||
title: "声音",
|
title: "声音",
|
||||||
icon: vm.speakerOpen ? RemixIcons.volume_up_fill : RemixIcons.volume_mute_fill,
|
icon: vm.speakerClose
|
||||||
isOff: !vm.speakerOpen,
|
? RemixIcons.volume_mute_fill
|
||||||
onTap: vm.changeSpeakerSwitch,
|
: RemixIcons.volume_up_fill,
|
||||||
|
isOff: vm.speakerClose,
|
||||||
|
onTap: () {
|
||||||
|
vm.changeSpeakerSwitch(value: vm.speakerClose);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
BarItem(
|
BarItem(
|
||||||
title: "举手",
|
title: "举手",
|
||||||
icon: RemixIcons.hand,
|
icon: RemixIcons.hand,
|
||||||
|
onTap: () {
|
||||||
|
vm.changeHandSwitch();
|
||||||
|
widget.onTap?.call();
|
||||||
|
},
|
||||||
),
|
),
|
||||||
BarItem(
|
BarItem(
|
||||||
title: "拍照",
|
title: "上传",
|
||||||
icon: RemixIcons.upload_2_fill,
|
icon: RemixIcons.upload_2_fill,
|
||||||
onTap: _handShowFile,
|
onTap: _handShowFile,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:app/utils/time.dart';
|
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:flutter/material.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:remixicon/remixicon.dart';
|
import 'package:remixicon/remixicon.dart';
|
||||||
|
|
||||||
import '../viewmodel/stu_room_vm.dart';
|
class TopBar extends StatelessWidget implements PreferredSizeWidget {
|
||||||
|
|
||||||
class TopBar extends StatefulWidget implements PreferredSizeWidget {
|
|
||||||
final bool showOther;
|
final bool showOther;
|
||||||
final void Function()? onOther;
|
final void Function()? onOther;
|
||||||
|
|
||||||
@@ -17,67 +16,63 @@ class TopBar extends StatefulWidget implements PreferredSizeWidget {
|
|||||||
this.onOther,
|
this.onOther,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
|
||||||
State<TopBar> createState() => _TopBarState();
|
|
||||||
|
|
||||||
@override
|
|
||||||
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
|
|
||||||
}
|
|
||||||
|
|
||||||
class _TopBarState extends State<TopBar> {
|
|
||||||
Timer? _timer;
|
|
||||||
int seconds = 0;
|
|
||||||
late DateTime startTime;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
final vm = context.read<StuRoomVM>();
|
|
||||||
startTime = parseTime(vm.roomInfo.startTime);
|
|
||||||
|
|
||||||
_timer = Timer.periodic(const Duration(seconds: 1), (_) {
|
|
||||||
final diff = DateTime.now().difference(startTime).inSeconds;
|
|
||||||
setState(() {
|
|
||||||
seconds = diff < 0 ? 0 : diff;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 你若想外面主动停,可以暴露这个方法
|
|
||||||
void stopTimer() {
|
|
||||||
_timer?.cancel();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_timer?.cancel();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final vm = context.read<StuRoomVM>();
|
|
||||||
|
|
||||||
return AppBar(
|
return AppBar(
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: Colors.white,
|
||||||
titleTextStyle: const TextStyle(color: Colors.white, fontSize: 18),
|
titleTextStyle: const TextStyle(color: Colors.white, fontSize: 18),
|
||||||
backgroundColor: const Color(0xff232426),
|
backgroundColor: const Color(0xff232426),
|
||||||
centerTitle: true,
|
centerTitle: true,
|
||||||
title: Column(
|
leadingWidth: 100,
|
||||||
children: [
|
leading: Container(
|
||||||
Text(vm.roomInfo.roomName),
|
padding: EdgeInsets.only(left: 10),
|
||||||
Text(
|
alignment: Alignment.centerLeft,
|
||||||
formatSeconds(seconds),
|
child: GestureDetector(
|
||||||
style: const TextStyle(fontSize: 12, color: Colors.white24),
|
onTap: () {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (_) {
|
||||||
|
return ConfigDialog(
|
||||||
|
content: "请确认是否退出自习室",
|
||||||
|
onCancel: () {
|
||||||
|
context.pop();
|
||||||
|
},
|
||||||
|
onConfirm: () {
|
||||||
|
context.pop();
|
||||||
|
context.pop();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: Text(
|
||||||
|
"退出自习室",
|
||||||
|
style: TextStyle(color: Colors.red),
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
|
),
|
||||||
|
title: Consumer<CountDownVM>(
|
||||||
|
builder: (context, vm, _) {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
Text(vm.roomInfo!.roomName),
|
||||||
|
Text(
|
||||||
|
formatSeconds(vm.studyTime),
|
||||||
|
style: const TextStyle(fontSize: 12, color: Colors.white24),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: widget.onOther,
|
onPressed: onOther,
|
||||||
icon: Icon(widget.showOther ? RemixIcons.team_fill : RemixIcons.team_line),
|
icon: Icon(showOther ? RemixIcons.team_fill : RemixIcons.team_line),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,18 @@
|
|||||||
import 'package:app/pages/student/room/viewmodel/stu_room_vm.dart';
|
import 'package:app/pages/student/room/widgets/status_view.dart';
|
||||||
import 'package:app/providers/user_store.dart';
|
import 'package:app/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/base/transition/slide_hide.dart';
|
||||||
|
import 'package:app/widgets/room/core/count_down_vm.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
import 'controls/bottom_bar.dart';
|
import 'controls/bottom_bar.dart';
|
||||||
import 'controls/top_bar.dart';
|
import 'controls/top_bar.dart';
|
||||||
import 'video/student_video_list.dart';
|
import 'video/student_video_list.dart';
|
||||||
import 'video/teacher_video.dart';
|
import 'viewmodel/stu_room_vm.dart';
|
||||||
|
|
||||||
class SRoomPage extends StatefulWidget {
|
class SRoomPage extends StatefulWidget {
|
||||||
final RoomInfoDto roomInfo;
|
final RoomListItemDto roomInfo;
|
||||||
|
|
||||||
const SRoomPage({super.key, required this.roomInfo});
|
const SRoomPage({super.key, required this.roomInfo});
|
||||||
|
|
||||||
@@ -36,29 +37,40 @@ class _SRoomPageState extends State<SRoomPage> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
UserStore userStore = context.read<UserStore>();
|
UserStore userStore = context.read<UserStore>();
|
||||||
return ChangeNotifierProvider<StuRoomVM>(
|
return MultiProvider(
|
||||||
create: (_) => StuRoomVM(
|
providers: [
|
||||||
roomInfo: widget.roomInfo,
|
ChangeNotifierProvider<StuRoomVM>(
|
||||||
uid: userStore.userInfo!.id,
|
create: (_) => StuRoomVM(
|
||||||
),
|
info: widget.roomInfo,
|
||||||
|
uid: userStore.userInfo!.id,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ChangeNotifierProxyProvider<StuRoomVM, CountDownVM>(
|
||||||
|
create: (_) => CountDownVM(),
|
||||||
|
update: (_, stuVM, countDownVM) {
|
||||||
|
countDownVM!.bind(stuVM.roomInfo);
|
||||||
|
return countDownVM;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
body: Stack(
|
body: Stack(
|
||||||
children: [
|
children: [
|
||||||
//底部控制显示
|
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: _toggleOverlay,
|
onTap: _toggleOverlay,
|
||||||
child: Container(color: Color(0xff2c3032)),
|
child: Container(color: Color(0xff2c3032)),
|
||||||
),
|
),
|
||||||
//老师视频画面
|
StatusView(),
|
||||||
TeacherVideo(),
|
//其他学生
|
||||||
|
|
||||||
Positioned(
|
Positioned(
|
||||||
right: 0,
|
right: 0,
|
||||||
top: 0,
|
top: 0,
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
child: Visibility(
|
child: IgnorePointer(
|
||||||
visible: _showOtherStudent,
|
child: Visibility(
|
||||||
child: StudentVideoList(),
|
visible: _showOtherStudent,
|
||||||
|
child: StudentVideoList(),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
@@ -87,7 +99,9 @@ class _SRoomPageState extends State<SRoomPage> {
|
|||||||
child: SlideHide(
|
child: SlideHide(
|
||||||
direction: SlideDirection.down,
|
direction: SlideDirection.down,
|
||||||
hide: !_controlsVisible,
|
hide: !_controlsVisible,
|
||||||
child: BottomBar(),
|
child: BottomBar(
|
||||||
|
onTap: _toggleOverlay,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
|
import 'package:agora_rtc_engine/agora_rtc_engine.dart';
|
||||||
import 'package:app/pages/student/room/viewmodel/stu_room_vm.dart';
|
import 'package:app/pages/student/room/viewmodel/stu_room_vm.dart';
|
||||||
|
import 'package:app/widgets/room/video_surface.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
@@ -8,6 +10,9 @@ class StudentVideoList extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final vm = context.watch<StuRoomVM>();
|
final vm = context.watch<StuRoomVM>();
|
||||||
|
if (vm.roomInfo.roomStatus != 1) {
|
||||||
|
return SizedBox();
|
||||||
|
}
|
||||||
return SafeArea(
|
return SafeArea(
|
||||||
child: Container(
|
child: Container(
|
||||||
width: 250,
|
width: 250,
|
||||||
@@ -26,6 +31,17 @@ class StudentVideoList extends StatelessWidget {
|
|||||||
color: Color(0xff373c3e),
|
color: Color(0xff373c3e),
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
),
|
),
|
||||||
|
child: VideoSurface(
|
||||||
|
user: item,
|
||||||
|
child: AgoraVideoView(
|
||||||
|
controller: VideoViewController(
|
||||||
|
rtcEngine: vm.engine!,
|
||||||
|
canvas: VideoCanvas(
|
||||||
|
uid: item.rtcUid,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Positioned(
|
Positioned(
|
||||||
|
|||||||
@@ -1,60 +1,83 @@
|
|||||||
import 'package:agora_rtc_engine/agora_rtc_engine.dart';
|
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:flutter/material.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
import '../viewmodel/stu_room_vm.dart';
|
import '../viewmodel/stu_room_vm.dart';
|
||||||
|
|
||||||
class TeacherVideo extends StatefulWidget {
|
class TeacherVideo extends StatelessWidget {
|
||||||
const TeacherVideo({super.key});
|
const TeacherVideo({super.key});
|
||||||
|
|
||||||
@override
|
|
||||||
State<TeacherVideo> createState() => _TeacherVideoState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _TeacherVideoState extends State<TeacherVideo> {
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final vm = context.read<StuRoomVM>();
|
final vm = context.watch<StuRoomVM>();
|
||||||
final teacherInfo = vm.teacherInfo;
|
final teacherInfo = vm.teacherInfo;
|
||||||
|
return PopScope(
|
||||||
///没开始
|
canPop: false,
|
||||||
if (vm.roomStatus == 0) {
|
onPopInvokedWithResult: (didPop, _) {
|
||||||
return _empty("自习室还没开始");
|
if (!didPop) {
|
||||||
}
|
showDialog(
|
||||||
|
context: context,
|
||||||
///开始
|
builder: (context) {
|
||||||
if (vm.roomStatus == 1 && vm.engine != null) {
|
return ConfigDialog(
|
||||||
if (teacherInfo == null) {
|
content: "是否退出自习室",
|
||||||
return _empty("老师不在自习室内");
|
onCancel: () {
|
||||||
}
|
context.pop();
|
||||||
if (teacherInfo.online == 0) {
|
},
|
||||||
return _empty("老师掉线了,请耐心等待");
|
onConfirm: () {
|
||||||
}
|
context.pop();
|
||||||
return AgoraVideoView(
|
context.pop();
|
||||||
controller: VideoViewController(
|
},
|
||||||
rtcEngine: vm.engine!,
|
);
|
||||||
canvas: VideoCanvas(
|
},
|
||||||
uid: vm.teacherInfo!.userId,
|
);
|
||||||
),
|
}
|
||||||
),
|
},
|
||||||
);
|
child: IgnorePointer(
|
||||||
}
|
child: Stack(
|
||||||
|
alignment: Alignment.bottomCenter,
|
||||||
///结束
|
children: [
|
||||||
if (vm.roomStatus == 2) {
|
VideoSurface(
|
||||||
return _empty("自习室已结束");
|
user: teacherInfo!,
|
||||||
}
|
child: AgoraVideoView(
|
||||||
return _empty("加载中");
|
controller: VideoViewController(
|
||||||
}
|
rtcEngine: vm.engine!,
|
||||||
|
canvas: VideoCanvas(
|
||||||
Widget _empty(String title) {
|
uid: teacherInfo.rtcUid,
|
||||||
return SafeArea(
|
),
|
||||||
child: Align(
|
),
|
||||||
child: Text(
|
),
|
||||||
title,
|
),
|
||||||
style: TextStyle(color: Colors.white),
|
Positioned(
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
child: Container(
|
||||||
|
width: 150,
|
||||||
|
color: Colors.black,
|
||||||
|
child: AspectRatio(
|
||||||
|
aspectRatio: 1 / 1.2,
|
||||||
|
child: AgoraVideoView(
|
||||||
|
controller: VideoViewController(
|
||||||
|
rtcEngine: vm.engine!,
|
||||||
|
canvas: const VideoCanvas(uid: 0),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (vm.selfInfo?.handup == 1)
|
||||||
|
Positioned(
|
||||||
|
bottom: 60,
|
||||||
|
child: HandRaiseButton(
|
||||||
|
onTap: vm.changeHandSwitch,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,24 +2,23 @@ import 'dart:async';
|
|||||||
|
|
||||||
import 'package:agora_rtc_engine/agora_rtc_engine.dart';
|
import 'package:agora_rtc_engine/agora_rtc_engine.dart';
|
||||||
import 'package:app/config/config.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_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/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_protocol.dart';
|
||||||
import 'package:app/request/websocket/room_websocket.dart';
|
import 'package:app/request/websocket/room_websocket.dart';
|
||||||
import 'package:flutter/cupertino.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:logger/logger.dart';
|
||||||
|
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||||
|
|
||||||
Logger log = Logger();
|
Logger log = Logger();
|
||||||
|
|
||||||
class StuRoomVM extends ChangeNotifier {
|
class StuRoomVM extends ChangeNotifier {
|
||||||
///房间信息
|
///房间信息,状态0没开始,1进行中,2已结束
|
||||||
final RoomInfoDto roomInfo;
|
late MeetingRoomDto roomInfo;
|
||||||
|
|
||||||
///房间开启状态,0没开始,1进行中,2已结束
|
|
||||||
int roomStatus = 0;
|
|
||||||
|
|
||||||
///其他学生列表,老师信息,自己信息
|
///其他学生列表,老师信息,自己信息
|
||||||
int uid;
|
int uid;
|
||||||
@@ -27,12 +26,18 @@ class StuRoomVM extends ChangeNotifier {
|
|||||||
RoomUserDto? teacherInfo;
|
RoomUserDto? teacherInfo;
|
||||||
RoomUserDto? selfInfo;
|
RoomUserDto? selfInfo;
|
||||||
|
|
||||||
///本人的摄像头、麦克风、扬声器状态是否打开了
|
// ///老师是否发送请求过来了,0关闭,1摄像头,2麦克风
|
||||||
bool get cameraOpen => selfInfo?.cameraStatus == 1;
|
// 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管理
|
///ws管理
|
||||||
final RoomWebSocket _ws = RoomWebSocket();
|
final RoomWebSocket _ws = RoomWebSocket();
|
||||||
@@ -43,12 +48,14 @@ class StuRoomVM extends ChangeNotifier {
|
|||||||
|
|
||||||
RtcEngine? get engine => _engine;
|
RtcEngine? get engine => _engine;
|
||||||
|
|
||||||
StuRoomVM({required this.roomInfo, required this.uid}) {
|
StuRoomVM({required RoomListItemDto info, required this.uid}) {
|
||||||
|
roomInfo = MeetingRoomDto.fromRoomListItem(info);
|
||||||
_startRoom();
|
_startRoom();
|
||||||
}
|
}
|
||||||
|
|
||||||
///初始化声网
|
///初始化声网
|
||||||
Future<void> _initRtc() async {
|
Future<void> _initRtc() async {
|
||||||
|
if (_engine != null) return;
|
||||||
_engine = createAgoraRtcEngine();
|
_engine = createAgoraRtcEngine();
|
||||||
//初始化 RtcEngine,设置频道场景为 channelProfileLiveBroadcasting(直播场景)
|
//初始化 RtcEngine,设置频道场景为 channelProfileLiveBroadcasting(直播场景)
|
||||||
await _engine!.initialize(
|
await _engine!.initialize(
|
||||||
@@ -57,27 +64,34 @@ class StuRoomVM extends ChangeNotifier {
|
|||||||
channelProfile: ChannelProfileType.channelProfileCommunication,
|
channelProfile: ChannelProfileType.channelProfileCommunication,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
//启动视频模块
|
_engine!.getUserInfoByUid(1);
|
||||||
|
// 启用视频模块
|
||||||
await _engine!.enableVideo();
|
await _engine!.enableVideo();
|
||||||
//加入频道
|
// 开启本地预览
|
||||||
await _engine!.joinChannel(
|
await _engine!.startPreview();
|
||||||
token: _ws.rtcToken!.token,
|
|
||||||
channelId: _ws.rtcToken!.channel,
|
WakelockPlus.enable();
|
||||||
uid: uid,
|
final status = await _engine!.getConnectionState();
|
||||||
// uid: _ws.rtcToken!.uid,
|
if (status == ConnectionStateType.connectionStateDisconnected) {
|
||||||
options: ChannelMediaOptions(
|
//加入频道
|
||||||
// 自动订阅所有视频流
|
await _engine!.joinChannel(
|
||||||
autoSubscribeVideo: true,
|
token: _ws.rtcToken!.token,
|
||||||
// 自动订阅所有音频流
|
channelId: _ws.rtcToken!.channel,
|
||||||
autoSubscribeAudio: true,
|
uid: _ws.rtcToken!.uid,
|
||||||
// 发布摄像头采集的视频
|
options: ChannelMediaOptions(
|
||||||
publishCameraTrack: true,
|
// 自动订阅所有视频流
|
||||||
// 发布麦克风采集的音频
|
autoSubscribeVideo: true,
|
||||||
publishMicrophoneTrack: true,
|
// 自动订阅所有音频流
|
||||||
// 设置用户角色为 clientRoleBroadcaster(主播)或 clientRoleAudience(观众)
|
autoSubscribeAudio: true,
|
||||||
clientRoleType: ClientRoleType.clientRoleBroadcaster,
|
// 发布摄像头采集的视频
|
||||||
),
|
publishCameraTrack: true,
|
||||||
);
|
// 发布麦克风采集的音频
|
||||||
|
publishMicrophoneTrack: true,
|
||||||
|
// 设置用户角色为 clientRoleBroadcaster(主播)或 clientRoleAudience(观众)
|
||||||
|
clientRoleType: ClientRoleType.clientRoleBroadcaster,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
///开始链接房间
|
///开始链接房间
|
||||||
@@ -94,7 +108,30 @@ class StuRoomVM extends ChangeNotifier {
|
|||||||
if (msg.event == RoomEvent.changeUser) {
|
if (msg.event == RoomEvent.changeUser) {
|
||||||
final list = RoomUserDto.listFromJson(msg.data['user_list']);
|
final list = RoomUserDto.listFromJson(msg.data['user_list']);
|
||||||
onStudentChange(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);
|
newList.add(t);
|
||||||
} else {
|
} else {
|
||||||
selfInfo = t;
|
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) {
|
void onRoomStartStatus(RoomInfoDto info) {
|
||||||
roomStatus = roomInfo.roomStatus;
|
roomInfo = roomInfo.copyWith(
|
||||||
|
roomStatus: info.roomStatus,
|
||||||
|
actualStartTime: info.roomStartTime,
|
||||||
|
boardUuid: info.boardUuid,
|
||||||
|
);
|
||||||
|
//开启摄像头
|
||||||
|
if (roomInfo.roomStatus == 1) {
|
||||||
|
_initRtc();
|
||||||
|
}
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
///控制摄像头开关
|
///控制摄像头开关
|
||||||
void changeCameraSwitch() {
|
/// - [value] 摄像头状态,true为开启,false为关闭
|
||||||
bool isOpen = selfInfo!.cameraStatus == 1;
|
/// - [fromServer] 发送指令给服务器,默认true
|
||||||
selfInfo!.cameraStatus = isOpen ? 0 : 1;
|
void changeCameraSwitch({
|
||||||
//发送指令
|
required bool value,
|
||||||
_ws.send(RoomCommand.studentActon, {
|
bool fromServer = true,
|
||||||
"mute_type": "camera",
|
}) {
|
||||||
"is_mute": isOpen ? 1 : 0,
|
//改变后的操作状态,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();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
///控制麦克风开关
|
///控制麦克风开关
|
||||||
void changeMicSwitch() {
|
/// - [value] 麦克风状态,true为开启,false为关闭
|
||||||
bool isOpen = selfInfo!.microphoneStatus == 1;
|
/// - [fromServer] 默认为true,发送指令给服务器
|
||||||
selfInfo!.microphoneStatus = isOpen ? 0 : 1;
|
void changeMicSwitch({required bool value, bool fromServer = true}) {
|
||||||
print(selfInfo!.microphoneStatus);
|
selfInfo!.microphoneStatus = value ? 1 : 0;
|
||||||
//发送指令
|
//发送指令
|
||||||
_ws.send(RoomCommand.studentActon, {
|
if (fromServer) {
|
||||||
"mute_type": "microphone",
|
_ws.send(RoomCommand.studentActon, {
|
||||||
"is_mute": isOpen ? 1 : 0,
|
"mute_type": "microphone",
|
||||||
});
|
"is_mute": value ? 0 : 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
_engine?.enableLocalAudio(value);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 控制扬声器开关
|
/// 控制扬声器开关
|
||||||
void changeSpeakerSwitch() {
|
/// - [value] 扬声器状态,true为开启,false为关闭
|
||||||
bool isOpen = selfInfo!.speekerStatus == 1;
|
/// - [fromServer] 默认为true,发送指令给服务器
|
||||||
selfInfo!.speekerStatus = isOpen ? 0 : 1;
|
void changeSpeakerSwitch({required bool value, bool fromServer = true}) {
|
||||||
|
//操作后是否是开启状态
|
||||||
|
selfInfo!.speekerStatus = value ? 1 : 0;
|
||||||
//发送指令
|
//发送指令
|
||||||
_ws.send(RoomCommand.studentActon, {
|
if (fromServer) {
|
||||||
"mute_type": "speeker",
|
_ws.send(RoomCommand.studentActon, {
|
||||||
"is_mute": isOpen ? 1 : 0,
|
"mute_type": "speeker",
|
||||||
});
|
"is_mute": value ? 0 : 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
_engine?.muteAllRemoteAudioStreams(!value);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
///控制举手
|
||||||
|
void changeHandSwitch({bool fromServer = true}) {
|
||||||
|
bool nextOpen = handClose;
|
||||||
|
selfInfo!.handup = nextOpen ? 1 : 0;
|
||||||
|
if (fromServer) {
|
||||||
|
_ws.send(RoomCommand.handUp, {
|
||||||
|
'is_handup': nextOpen ? 1 : 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
///上传文件
|
||||||
|
void uploadFile(List<String> files) {
|
||||||
|
selfInfo?.filesList.addAll(files);
|
||||||
|
_ws.send(RoomCommand.uploadFile, {
|
||||||
|
"files": selfInfo!.filesList,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
///自习室关闭
|
||||||
|
void _closeRoom() {
|
||||||
|
roomInfo.roomStatus = 2;
|
||||||
|
_dispose();
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
///销毁
|
||||||
|
void _dispose() {
|
||||||
|
_engine?.leaveChannel();
|
||||||
|
_engine?.release();
|
||||||
|
_sub?.cancel();
|
||||||
|
_ws.dispose();
|
||||||
|
WakelockPlus.disable();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
super.dispose();
|
super.dispose();
|
||||||
_sub?.cancel();
|
_dispose();
|
||||||
_ws.dispose();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
99
lib/pages/student/room/widgets/status_view.dart
Normal file
99
lib/pages/student/room/widgets/status_view.dart
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
import 'package:app/utils/time.dart';
|
||||||
|
import 'package:app/widgets/base/button/index.dart';
|
||||||
|
import 'package:app/widgets/room/core/count_down_vm.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
import '../video/teacher_video.dart';
|
||||||
|
import '../viewmodel/stu_room_vm.dart';
|
||||||
|
|
||||||
|
class StatusView extends StatelessWidget {
|
||||||
|
const StatusView({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final vm = context.watch<StuRoomVM>();
|
||||||
|
final teacherInfo = vm.teacherInfo;
|
||||||
|
|
||||||
|
///没开始
|
||||||
|
if (vm.roomInfo.roomStatus == 0) {
|
||||||
|
return Consumer<CountDownVM>(
|
||||||
|
builder: (_, countVM, __) {
|
||||||
|
if (countVM.canEnterRoom) {
|
||||||
|
return _empty("等待老师进入自习室");
|
||||||
|
} else {
|
||||||
|
countVM.startStartCountdown();
|
||||||
|
return Align(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
"未到开播时间",
|
||||||
|
style: TextStyle(color: Colors.white),
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
margin: const EdgeInsets.symmetric(vertical: 10),
|
||||||
|
child: Text(
|
||||||
|
formatSeconds(countVM.startCountDown),
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 26,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
///结束
|
||||||
|
if (vm.roomInfo.roomStatus == 2) {
|
||||||
|
return Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
spacing: 5,
|
||||||
|
children: [
|
||||||
|
_empty("自习室已结束"),
|
||||||
|
SizedBox(
|
||||||
|
width: 120,
|
||||||
|
child: Button(
|
||||||
|
text: "返回首页",
|
||||||
|
onPressed: () {
|
||||||
|
context.pop();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
///开始
|
||||||
|
if (vm.roomInfo.roomStatus == 1 && vm.engine != null) {
|
||||||
|
if (teacherInfo == null) {
|
||||||
|
return _empty("老师不在自习室内");
|
||||||
|
}
|
||||||
|
if (teacherInfo.online == 0) {
|
||||||
|
return _empty("老师暂时离开,请耐心等待");
|
||||||
|
}
|
||||||
|
return TeacherVideo();
|
||||||
|
}
|
||||||
|
|
||||||
|
return _empty("加载中");
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _empty(String title) {
|
||||||
|
return SafeArea(
|
||||||
|
child: Align(
|
||||||
|
child: Text(
|
||||||
|
title,
|
||||||
|
style: TextStyle(color: Colors.white),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
import 'package:app/config/theme/base/app_theme_ext.dart';
|
import 'package:app/config/theme/base/app_theme_ext.dart';
|
||||||
|
import 'package:app/widgets/version/version_dialog.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
import 'viewmodel/home_view_model.dart';
|
import 'viewmodel/home_view_model.dart';
|
||||||
import 'widgets/header.dart';
|
import 'widgets/header.dart';
|
||||||
|
import 'widgets/tip_card.dart';
|
||||||
import 'widgets/today_card.dart';
|
import 'widgets/today_card.dart';
|
||||||
|
|
||||||
class THomePage extends StatelessWidget {
|
class THomePage extends StatelessWidget {
|
||||||
@@ -11,6 +13,7 @@ class THomePage extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
showUpdateDialog(context);
|
||||||
return ChangeNotifierProvider(
|
return ChangeNotifierProvider(
|
||||||
create: (_) => HomeViewModel(),
|
create: (_) => HomeViewModel(),
|
||||||
child: const _HomeView(),
|
child: const _HomeView(),
|
||||||
@@ -24,6 +27,7 @@ class _HomeView extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final vm = context.read<HomeViewModel>();
|
final vm = context.read<HomeViewModel>();
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: Theme.of(context).colorScheme.surfaceContainer,
|
backgroundColor: Theme.of(context).colorScheme.surfaceContainer,
|
||||||
appBar: Header(),
|
appBar: Header(),
|
||||||
@@ -36,6 +40,8 @@ class _HomeView extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
children: [
|
children: [
|
||||||
TodayCard(),
|
TodayCard(),
|
||||||
|
TipCard1(),
|
||||||
|
TipCard2(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import 'package:app/request/api/room_api.dart';
|
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:app/utils/time.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class HomeViewModel extends ChangeNotifier {
|
class HomeViewModel extends ChangeNotifier {
|
||||||
RoomInfoDto? roomInfo;
|
RoomListItemDto ? roomInfo;
|
||||||
bool loading = true;
|
bool loading = true;
|
||||||
|
|
||||||
HomeViewModel() {
|
HomeViewModel() {
|
||||||
|
|||||||
143
lib/pages/teacher/home/widgets/tip_card.dart
Normal file
143
lib/pages/teacher/home/widgets/tip_card.dart
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
import 'package:app/widgets/base/card/g_card.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:remixicon/remixicon.dart';
|
||||||
|
|
||||||
|
class TipCard1 extends StatelessWidget {
|
||||||
|
const TipCard1({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final list = [
|
||||||
|
{
|
||||||
|
"icon": RemixIcons.video_on_line,
|
||||||
|
"title": "实时视频互动",
|
||||||
|
"subtitle": "高清视频连接,随时与学生面对面交流",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"icon": RemixIcons.file_list_line,
|
||||||
|
"title": "查看学生资料",
|
||||||
|
"subtitle": "查看学生上传的作业、题目和笔记",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"icon": RemixIcons.message_line,
|
||||||
|
"title": "灵活管控",
|
||||||
|
"subtitle": "一键控制学生的视频、音频状态",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"icon": RemixIcons.lightbulb_line,
|
||||||
|
"title": "白板演示",
|
||||||
|
"subtitle": "开启白板功能,为学生讲解疑难问题",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
return Container(
|
||||||
|
margin: EdgeInsets.only(top: 15),
|
||||||
|
child: Column(
|
||||||
|
spacing: 10,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text("核心功能"),
|
||||||
|
GridView.builder(
|
||||||
|
shrinkWrap: true,
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
|
crossAxisCount: 2,
|
||||||
|
mainAxisExtent: 80,
|
||||||
|
crossAxisSpacing: 15,
|
||||||
|
mainAxisSpacing: 15,
|
||||||
|
),
|
||||||
|
itemBuilder: (_, index) {
|
||||||
|
final item = list[index] as dynamic;
|
||||||
|
return GCard(
|
||||||
|
child: Row(
|
||||||
|
spacing: 10,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 50,
|
||||||
|
height: 50,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context).primaryColor,
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
item["icon"],
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Text(item["title"]),
|
||||||
|
Text(
|
||||||
|
item["subtitle"],
|
||||||
|
style: Theme.of(context).textTheme.labelLarge,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
itemCount: list.length,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TipCard2 extends StatelessWidget {
|
||||||
|
const TipCard2({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final tipList = [
|
||||||
|
"请确保网络环境良好,保证视频通话质量",
|
||||||
|
"建议提前5分钟进入自习室,准备教学材料",
|
||||||
|
"合理使用白板功能,帮助学生更好地理解知识点",
|
||||||
|
"关注每位学生的学习状态,及时提供帮助",
|
||||||
|
];
|
||||||
|
return Container(
|
||||||
|
margin: EdgeInsets.only(top: 15),
|
||||||
|
padding: EdgeInsets.all(15),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Color(0xfffffbeb),
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
border: Border.all(
|
||||||
|
color: Color(0xfffee685),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
margin: EdgeInsets.only(bottom: 10),
|
||||||
|
child: Text("温馨提示"),
|
||||||
|
),
|
||||||
|
ListView.separated(
|
||||||
|
shrinkWrap: true,
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
itemBuilder: (_, index) {
|
||||||
|
return Row(
|
||||||
|
spacing: 4,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 5,
|
||||||
|
height: 5,
|
||||||
|
decoration: BoxDecoration(shape: BoxShape.circle, color: Colors.black),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
tipList[index],
|
||||||
|
style: Theme.of(context).textTheme.labelLarge,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
separatorBuilder: (_, __) => SizedBox(height: 3),
|
||||||
|
itemCount: tipList.length,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,10 @@
|
|||||||
import 'package:app/router/route_paths.dart';
|
import 'package:app/router/route_paths.dart';
|
||||||
import 'package:app/utils/permission.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/button/index.dart';
|
||||||
import 'package:app/widgets/base/card/g_card.dart';
|
import 'package:app/widgets/base/card/g_card.dart';
|
||||||
import 'package:app/widgets/base/config/config.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/base/empty/index.dart';
|
||||||
import 'package:app/widgets/room/file_drawer.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
@@ -93,7 +92,7 @@ class _TodayCardState extends State<TodayCard> {
|
|||||||
),
|
),
|
||||||
_item(
|
_item(
|
||||||
title: "时长",
|
title: "时长",
|
||||||
value: "${vm.roomMinutes} 分钟",
|
value: "${formatSeconds(vm.roomMinutes * 60, 'hh小时mm分钟')} ",
|
||||||
icon: RemixIcons.book_open_line,
|
icon: RemixIcons.book_open_line,
|
||||||
color: Color(0xffac45fd),
|
color: Color(0xffac45fd),
|
||||||
),
|
),
|
||||||
@@ -106,7 +105,7 @@ class _TodayCardState extends State<TodayCard> {
|
|||||||
child: Button(
|
child: Button(
|
||||||
text: vm.canEnterRoom ? "开始自习室" : "未到开始时间",
|
text: vm.canEnterRoom ? "开始自习室" : "未到开始时间",
|
||||||
type: ThemeType.success,
|
type: ThemeType.success,
|
||||||
// disabled: !vm.canEnterRoom,
|
disabled: !vm.canEnterRoom,
|
||||||
onPressed: _goToRoom,
|
onPressed: _goToRoom,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,43 +1,23 @@
|
|||||||
|
import 'package:app/utils/time.dart';
|
||||||
|
import 'package:app/widgets/base/button/index.dart';
|
||||||
|
import 'package:app/widgets/base/config/config.dart';
|
||||||
|
import 'package:app/widgets/base/dialog/config_dialog.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/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 '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 {
|
class TopBar extends StatelessWidget implements PreferredSizeWidget {
|
||||||
const TopBar({super.key});
|
const TopBar({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
//标题子显示内容
|
final vm = context.watch<TchRoomVM>();
|
||||||
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)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return AppBar(
|
return AppBar(
|
||||||
backgroundColor: Color(0xff373c3e),
|
backgroundColor: Color(0xff373c3e),
|
||||||
@@ -46,29 +26,144 @@ class TopBar extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
spacing: 5,
|
spacing: 5,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text("高三数学重置版", style: TextStyle(color: Colors.white, fontSize: 18)),
|
Text(vm.roomInfo.roomName, style: TextStyle(color: Colors.white, fontSize: 18)),
|
||||||
Row(
|
Row(
|
||||||
spacing: 15,
|
spacing: 15,
|
||||||
children: [
|
children: [
|
||||||
infoItem(title: "剩余 1小时23分钟", icon: RemixIcons.time_line),
|
Consumer<CountDownVM>(
|
||||||
infoItem(title: "8 名学生", icon: RemixIcons.group_line),
|
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: [
|
actions: [
|
||||||
actionButton(
|
_actionButton(
|
||||||
|
context,
|
||||||
icon: RemixIcons.video_on_ai_line,
|
icon: RemixIcons.video_on_ai_line,
|
||||||
title: "关闭全部",
|
title: "关闭全部",
|
||||||
|
onPressed: () {
|
||||||
|
_closeAll(context, StudentAction.camera);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
actionButton(
|
_actionButton(
|
||||||
|
context,
|
||||||
icon: RemixIcons.volume_up_line,
|
icon: RemixIcons.volume_up_line,
|
||||||
title: "全部静音",
|
title: "全部静音",
|
||||||
|
onPressed: () {
|
||||||
|
_closeAll(context, StudentAction.speaker);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
|
Container(
|
||||||
|
margin: EdgeInsets.only(right: 15),
|
||||||
|
child: Button(
|
||||||
|
text: "白板",
|
||||||
|
textStyle: TextStyle(fontSize: 14),
|
||||||
|
onPressed: (){},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Consumer<TchRoomVM>(
|
||||||
|
builder: (context, vm, _) {
|
||||||
|
if (vm.roomInfo.roomStatus != 1) {
|
||||||
|
return SizedBox();
|
||||||
|
}
|
||||||
|
return Button(
|
||||||
|
type: ThemeType.danger,
|
||||||
|
textStyle: TextStyle(fontSize: 14),
|
||||||
|
text: "结束自习室",
|
||||||
|
onPressed: () {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (_) {
|
||||||
|
return ConfigDialog(
|
||||||
|
content: '是否结束自习室?结束后无法在进入',
|
||||||
|
onCancel: () {
|
||||||
|
context.pop();
|
||||||
|
},
|
||||||
|
onConfirm: () {
|
||||||
|
context.pop();
|
||||||
|
vm.endRoom();
|
||||||
|
EasyLoading.showToast("会议室已结束");
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
SizedBox(width: 10),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _infoItem(BuildContext context, {required String title, required IconData icon}) {
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
Icon(icon, color: Colors.white54, size: 14),
|
||||||
|
SizedBox(width: 4),
|
||||||
|
Text(title, style: TextStyle(fontSize: 12, color: Colors.white54)),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _actionButton(
|
||||||
|
BuildContext context, {
|
||||||
|
required IconData icon,
|
||||||
|
required String title,
|
||||||
|
required VoidCallback onPressed,
|
||||||
|
}) {
|
||||||
|
return Container(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 10, vertical: 5),
|
||||||
|
margin: EdgeInsets.only(right: 15),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Color(0xff4a4f4f),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: InkWell(
|
||||||
|
onTap: onPressed,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(icon, size: 16),
|
||||||
|
SizedBox(width: 8),
|
||||||
|
Text(title, style: TextStyle(fontSize: 14)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _closeAll(BuildContext context, StudentAction action) {
|
||||||
|
final vm = context.read<TchRoomVM>();
|
||||||
|
String content = (action == StudentAction.camera) ? '是否关闭所有学生的摄像头?' : '是否关闭所有学生的扬声器?';
|
||||||
|
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (_) {
|
||||||
|
return ConfigDialog(
|
||||||
|
content: content,
|
||||||
|
onCancel: () => context.pop(),
|
||||||
|
onConfirm: () {
|
||||||
|
context.pop();
|
||||||
|
vm.closeAllStudentAction(action);
|
||||||
|
EasyLoading.showToast("操作已完成");
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
|
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'package:app/request/dto/room/room_info_dto.dart';
|
import 'package:app/widgets/room/core/count_down_vm.dart';
|
||||||
|
import 'package:app/request/dto/room/room_list_item_dto.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'controls/top_bar.dart';
|
import 'controls/top_bar.dart';
|
||||||
@@ -6,7 +7,7 @@ import 'widgets/status_view.dart';
|
|||||||
import 'viewmodel/tch_room_vm.dart';
|
import 'viewmodel/tch_room_vm.dart';
|
||||||
|
|
||||||
class TRoomPage extends StatefulWidget {
|
class TRoomPage extends StatefulWidget {
|
||||||
final RoomInfoDto roomInfo;
|
final RoomListItemDto roomInfo;
|
||||||
|
|
||||||
const TRoomPage({
|
const TRoomPage({
|
||||||
super.key,
|
super.key,
|
||||||
@@ -20,12 +21,19 @@ class TRoomPage extends StatefulWidget {
|
|||||||
class _TRoomPageState extends State<TRoomPage> {
|
class _TRoomPageState extends State<TRoomPage> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ChangeNotifierProvider<TchRoomVM>(
|
return MultiProvider(
|
||||||
create: (BuildContext context) {
|
providers: [
|
||||||
return TchRoomVM(
|
ChangeNotifierProvider<TchRoomVM>(
|
||||||
roomInfo: widget.roomInfo,
|
create: (_) => TchRoomVM(info: widget.roomInfo),
|
||||||
);
|
),
|
||||||
},
|
ChangeNotifierProxyProvider<TchRoomVM, CountDownVM>(
|
||||||
|
create: (_) => CountDownVM(),
|
||||||
|
update: (_, tchVM, countDownVM) {
|
||||||
|
countDownVM!.bind(tchVM.roomInfo);
|
||||||
|
return countDownVM;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
backgroundColor: Color(0xff2c3032),
|
backgroundColor: Color(0xff2c3032),
|
||||||
appBar: TopBar(),
|
appBar: TopBar(),
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import 'dart:async';
|
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_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/room_user_dto.dart';
|
||||||
import 'package:app/request/dto/room/rtc_token_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_protocol.dart';
|
||||||
@@ -13,18 +14,17 @@ import 'type.dart';
|
|||||||
|
|
||||||
class TchRoomVM extends ChangeNotifier {
|
class TchRoomVM extends ChangeNotifier {
|
||||||
TchRoomVM({
|
TchRoomVM({
|
||||||
required this.roomInfo,
|
required RoomListItemDto info,
|
||||||
String? start,
|
|
||||||
}) {
|
}) {
|
||||||
|
roomInfo = MeetingRoomDto.fromRoomListItem(info).copyWith(roomStatus: -1);
|
||||||
_startRoom();
|
_startRoom();
|
||||||
}
|
}
|
||||||
|
|
||||||
///学生摄像头列表
|
///学生摄像头列表
|
||||||
List<RoomUserDto> _students = [];
|
List<RoomUserDto> _students = [];
|
||||||
|
|
||||||
///房间的基础信息
|
///房间的基础信息,其中状态-1加载中,0没开始,1进行中,2关闭
|
||||||
final RoomInfoDto roomInfo;
|
late MeetingRoomDto roomInfo;
|
||||||
int roomStatus = -1; // //-1加载中,0没开始,1进行中,2关闭
|
|
||||||
|
|
||||||
///老师选中的学生id
|
///老师选中的学生id
|
||||||
int activeSId = 0;
|
int activeSId = 0;
|
||||||
@@ -42,8 +42,6 @@ class TchRoomVM extends ChangeNotifier {
|
|||||||
|
|
||||||
///websocket管理
|
///websocket管理
|
||||||
final RoomWebSocket _ws = RoomWebSocket();
|
final RoomWebSocket _ws = RoomWebSocket();
|
||||||
|
|
||||||
// bool wsConnected = false; // socket连接状态
|
|
||||||
StreamSubscription<RoomMessage>? _sub;
|
StreamSubscription<RoomMessage>? _sub;
|
||||||
|
|
||||||
RtcTokenDto? get rtcToken => _ws.rtcToken;
|
RtcTokenDto? get rtcToken => _ws.rtcToken;
|
||||||
@@ -61,8 +59,8 @@ class TchRoomVM extends ChangeNotifier {
|
|||||||
// 自习室人员变化
|
// 自习室人员变化
|
||||||
if (msg.event == RoomEvent.changeUser) {
|
if (msg.event == RoomEvent.changeUser) {
|
||||||
final list = RoomUserDto.listFromJson(msg.data['user_list']);
|
final list = RoomUserDto.listFromJson(msg.data['user_list']);
|
||||||
final room = RoomTypeDto.fromJson(msg.data['room_info']);
|
final room = RoomInfoDto.fromJson(msg.data['room_info']);
|
||||||
roomStatus = room.roomStatus;
|
_updateRoomInfo(room);
|
||||||
onStudentChange(list);
|
onStudentChange(list);
|
||||||
} else if ([
|
} else if ([
|
||||||
RoomEvent.openSpeaker,
|
RoomEvent.openSpeaker,
|
||||||
@@ -74,11 +72,26 @@ class TchRoomVM extends ChangeNotifier {
|
|||||||
RoomEvent.handUp,
|
RoomEvent.handUp,
|
||||||
].contains(msg.event)) {
|
].contains(msg.event)) {
|
||||||
onSyncStudentItem(RoomUserDto.fromJson(msg.data));
|
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();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
///更新房间信息
|
||||||
|
void _updateRoomInfo(RoomInfoDto info) {
|
||||||
|
roomInfo = roomInfo.copyWith(
|
||||||
|
roomStatus: info.roomStatus,
|
||||||
|
actualStartTime: info.roomStartTime,
|
||||||
|
boardUuid: info.boardUuid,
|
||||||
|
);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
///自习室的开关
|
///自习室的开关
|
||||||
/// - [isOpen]: 是否开启
|
/// - [isOpen]: 是否开启
|
||||||
void toggleRoom({required bool isOpen}) {
|
void toggleRoom({required bool isOpen}) {
|
||||||
@@ -92,6 +105,7 @@ class TchRoomVM extends ChangeNotifier {
|
|||||||
///学生选择
|
///学生选择
|
||||||
void selectStudent(int id) {
|
void selectStudent(int id) {
|
||||||
activeSId = id;
|
activeSId = id;
|
||||||
|
clearHandUp(id);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,20 +128,38 @@ class TchRoomVM extends ChangeNotifier {
|
|||||||
student.speekerStatus = isOpen ? 0 : 1;
|
student.speekerStatus = isOpen ? 0 : 1;
|
||||||
data['is_mute'] = isOpen ? 1 : 0;
|
data['is_mute'] = isOpen ? 1 : 0;
|
||||||
} else if (action == StudentAction.camera) {
|
} else if (action == StudentAction.camera) {
|
||||||
//如果是摄像头,只能关
|
//如果是摄像头
|
||||||
if (student.cameraStatus == 0) return;
|
bool isOpen = student.cameraStatus == 1;
|
||||||
student.cameraStatus = 0;
|
student.cameraStatus = 0;
|
||||||
data['is_mute'] = 1;
|
data['is_mute'] = isOpen ? 1 : 0;
|
||||||
} else if (action == StudentAction.microphone) {
|
} else if (action == StudentAction.microphone) {
|
||||||
//如果是麦克风,只能关
|
//如果是麦克风
|
||||||
if (student.microphoneStatus == 0) return;
|
bool isOpen = student.microphoneStatus == 1;
|
||||||
student.microphoneStatus = 0;
|
student.microphoneStatus = 0;
|
||||||
data['is_mute'] = 1;
|
data['is_mute'] = isOpen ? 1 : 0;
|
||||||
}
|
}
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
_ws.send(RoomCommand.switchStudentCamera, data);
|
_ws.send(RoomCommand.switchStudentCamera, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
///关闭全部学生的摄像头或者扬声器
|
||||||
|
void closeAllStudentAction(StudentAction action) {
|
||||||
|
_students.forEach((item) {
|
||||||
|
if (action == StudentAction.speaker) {
|
||||||
|
item.speekerStatus = 0;
|
||||||
|
} else if (action == StudentAction.camera) {
|
||||||
|
item.cameraStatus = 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
notifyListeners();
|
||||||
|
Map<String, dynamic> data = {
|
||||||
|
'target_user_id': "all",
|
||||||
|
"mute_type": action.value,
|
||||||
|
"is_mute": 1,
|
||||||
|
};
|
||||||
|
_ws.send(RoomCommand.switchStudentCamera, data);
|
||||||
|
}
|
||||||
|
|
||||||
//清除全部学生举手,或者是指定
|
//清除全部学生举手,或者是指定
|
||||||
void clearHandUp(int? id) {
|
void clearHandUp(int? id) {
|
||||||
Map<String, dynamic> data = {};
|
Map<String, dynamic> data = {};
|
||||||
@@ -155,13 +187,28 @@ class TchRoomVM extends ChangeNotifier {
|
|||||||
/// 同步单个学生的最新状态
|
/// 同步单个学生的最新状态
|
||||||
void onSyncStudentItem(RoomUserDto userInfo) {
|
void onSyncStudentItem(RoomUserDto userInfo) {
|
||||||
final index = _students.indexWhere((t) => t.userId == userInfo.userId);
|
final index = _students.indexWhere((t) => t.userId == userInfo.userId);
|
||||||
print(userInfo.toString());
|
|
||||||
if (index != -1) {
|
if (index != -1) {
|
||||||
_students[index] = userInfo;
|
_students[index] = userInfo;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
///更新学生的文件
|
||||||
|
void updateStudentFile(int uId, List<String> files) {
|
||||||
|
final index = _students.indexWhere((t) => t.userId == uId);
|
||||||
|
if (index != -1) {
|
||||||
|
_students[index].filesList = files;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
///结束会议
|
||||||
|
void endRoom() {
|
||||||
|
roomInfo = roomInfo.copyWith(roomStatus: 2);
|
||||||
|
_ws.send(RoomCommand.closeRoom);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
//销毁
|
//销毁
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import 'package:agora_rtc_engine/agora_rtc_engine.dart';
|
import 'package:agora_rtc_engine/agora_rtc_engine.dart';
|
||||||
import 'package:app/config/config.dart';
|
import 'package:app/config/config.dart';
|
||||||
import 'package:app/providers/user_store.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||||
|
|
||||||
import '../viewmodel/tch_room_vm.dart';
|
import '../viewmodel/tch_room_vm.dart';
|
||||||
import 'student_item.dart';
|
import 'student_item.dart';
|
||||||
@@ -27,11 +27,11 @@ class _ContentViewState extends State<ContentView> {
|
|||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
super.dispose();
|
super.dispose();
|
||||||
|
WakelockPlus.disable();
|
||||||
_dispose();
|
_dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _initRtc() async {
|
void _initRtc() async {
|
||||||
UserStore userStore = context.read<UserStore>();
|
|
||||||
final vm = context.read<TchRoomVM>();
|
final vm = context.read<TchRoomVM>();
|
||||||
_engine = createAgoraRtcEngine();
|
_engine = createAgoraRtcEngine();
|
||||||
//初始化 RtcEngine,设置频道场景为 channelProfileLiveBroadcasting(直播场景)
|
//初始化 RtcEngine,设置频道场景为 channelProfileLiveBroadcasting(直播场景)
|
||||||
@@ -41,39 +41,33 @@ class _ContentViewState extends State<ContentView> {
|
|||||||
channelProfile: ChannelProfileType.channelProfileCommunication,
|
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!.enableVideo();
|
||||||
//加入频道
|
// 开启本地预览
|
||||||
await _engine!.joinChannel(
|
await _engine!.startPreview();
|
||||||
token: vm.rtcToken!.token,
|
|
||||||
channelId: vm.rtcToken!.channel,
|
final status = await _engine!.getConnectionState();
|
||||||
uid: userStore.userInfo!.id,
|
WakelockPlus.enable();
|
||||||
options: ChannelMediaOptions(
|
if (status == ConnectionStateType.connectionStateDisconnected) {
|
||||||
// 自动订阅所有视频流
|
//加入频道
|
||||||
autoSubscribeVideo: true,
|
await _engine!.joinChannel(
|
||||||
// 自动订阅所有音频流
|
token: vm.rtcToken!.token,
|
||||||
autoSubscribeAudio: true,
|
channelId: vm.rtcToken!.channel,
|
||||||
// 发布摄像头采集的视频
|
uid: vm.rtcToken!.uid,
|
||||||
publishCameraTrack: true,
|
options: ChannelMediaOptions(
|
||||||
// 发布麦克风采集的音频
|
// 自动订阅所有视频流
|
||||||
publishMicrophoneTrack: true,
|
autoSubscribeVideo: true,
|
||||||
// 设置用户角色为 clientRoleBroadcaster(主播)或 clientRoleAudience(观众)
|
// 自动订阅所有音频流
|
||||||
clientRoleType: ClientRoleType.clientRoleBroadcaster,
|
autoSubscribeAudio: true,
|
||||||
),
|
// 发布摄像头采集的视频
|
||||||
);
|
publishCameraTrack: true,
|
||||||
|
// 发布麦克风采集的音频
|
||||||
|
publishMicrophoneTrack: true,
|
||||||
|
// 设置用户角色为 clientRoleBroadcaster(主播)或 clientRoleAudience(观众)
|
||||||
|
clientRoleType: ClientRoleType.clientRoleBroadcaster,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//销毁
|
//销毁
|
||||||
@@ -89,8 +83,11 @@ class _ContentViewState extends State<ContentView> {
|
|||||||
return Consumer<TchRoomVM>(
|
return Consumer<TchRoomVM>(
|
||||||
builder: (context, vm, _) {
|
builder: (context, vm, _) {
|
||||||
if (vm.students.isEmpty) {
|
if (vm.students.isEmpty) {
|
||||||
return Center(
|
return Align(
|
||||||
child: Text('准备中'),
|
child: Text(
|
||||||
|
'学生还没入场',
|
||||||
|
style: TextStyle(color: Colors.white),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
//选中的学生
|
//选中的学生
|
||||||
@@ -105,9 +102,30 @@ class _ContentViewState extends State<ContentView> {
|
|||||||
spacing: 15,
|
spacing: 15,
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: StudentItem(
|
child: Stack(
|
||||||
user: activeStudent,
|
children: [
|
||||||
engine: _engine,
|
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(
|
SizedBox(
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:app/utils/time.dart';
|
import 'package:app/utils/time.dart';
|
||||||
import 'package:app/widgets/base/dialog/config_dialog.dart';
|
import 'package:app/widgets/base/dialog/config_dialog.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
import '../../../../widgets/room/core/count_down_vm.dart';
|
||||||
import 'content_view.dart';
|
import 'content_view.dart';
|
||||||
import '../viewmodel/tch_room_vm.dart';
|
import '../viewmodel/tch_room_vm.dart';
|
||||||
|
|
||||||
@@ -17,40 +17,12 @@ class StatusView extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _StatusViewState extends State<StatusView> {
|
class _StatusViewState extends State<StatusView> {
|
||||||
int _seconds = 0;
|
|
||||||
Timer? _timer;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void initState() {
|
||||||
_timer?.cancel();
|
super.initState();
|
||||||
super.dispose();
|
final countVM = context.read<CountDownVM>();
|
||||||
}
|
countVM.removeListener(_onCountDownEnd);
|
||||||
|
countVM.addListener(_onCountDownEnd);
|
||||||
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;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
///开播中返回拦截弹窗
|
///开播中返回拦截弹窗
|
||||||
@@ -72,55 +44,65 @@ class _StatusViewState extends State<StatusView> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
///监听会议室倒计时结束的时候
|
||||||
|
void _onCountDownEnd() {
|
||||||
|
final countVM = context.read<CountDownVM>();
|
||||||
|
if (countVM.endCountDown == 0) {
|
||||||
|
EasyLoading.showToast("自习室已到结束时间,请记得关闭会议室");
|
||||||
|
countVM.removeListener(_onCountDownEnd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final vm = context.watch<TchRoomVM>();
|
final tchVM = context.watch<TchRoomVM>();
|
||||||
|
var roomStatus = tchVM.roomInfo.roomStatus;
|
||||||
|
|
||||||
/// 1. 未加载
|
/// 1. 未加载
|
||||||
if (vm.roomStatus == -1) {
|
if (roomStatus == -1) {
|
||||||
return const Align(
|
return const Align(
|
||||||
child: Text("加载中", style: TextStyle(color: Colors.white)),
|
child: Text("加载中", style: TextStyle(color: Colors.white)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 2. 未开始的房间
|
/// 2. 未开始的房间
|
||||||
if (vm.roomStatus == 0) {
|
if (roomStatus == 0) {
|
||||||
if (vm.canEnterRoom) {
|
return Consumer<CountDownVM>(
|
||||||
// 到时间了 → 自动开播
|
builder: (_, countVM, __) {
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
if (countVM.canEnterRoom) {
|
||||||
vm.toggleRoom(isOpen: true);
|
tchVM.toggleRoom(isOpen: true);
|
||||||
});
|
return SizedBox();
|
||||||
} else {
|
} else {
|
||||||
// 没到时间 → 启动倒计时
|
countVM.startStartCountdown();
|
||||||
_startCountDown(parseTime(vm.roomInfo.startTime));
|
return Align(
|
||||||
}
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
return Align(
|
children: [
|
||||||
child: Column(
|
const Text(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
"未到开播时间,到点后自动开播",
|
||||||
children: [
|
style: TextStyle(color: Colors.white),
|
||||||
const Text(
|
),
|
||||||
"未到开播时间,到点后自动开播",
|
Container(
|
||||||
style: TextStyle(color: Colors.white),
|
margin: const EdgeInsets.symmetric(vertical: 10),
|
||||||
),
|
child: Text(
|
||||||
Container(
|
formatSeconds(countVM.startCountDown),
|
||||||
margin: const EdgeInsets.symmetric(vertical: 10),
|
style: const TextStyle(
|
||||||
child: Text(
|
color: Colors.white,
|
||||||
formatSeconds(_seconds),
|
fontSize: 26,
|
||||||
style: const TextStyle(
|
fontWeight: FontWeight.bold,
|
||||||
color: Colors.white,
|
),
|
||||||
fontSize: 26,
|
),
|
||||||
fontWeight: FontWeight.bold,
|
),
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
],
|
}
|
||||||
),
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 3. 已开播
|
/// 3. 已开播
|
||||||
if (vm.roomStatus == 1) {
|
if (roomStatus == 1) {
|
||||||
return PopScope(
|
return PopScope(
|
||||||
canPop: false,
|
canPop: false,
|
||||||
onPopInvokedWithResult: (didPop, _) {
|
onPopInvokedWithResult: (didPop, _) {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'package:agora_rtc_engine/agora_rtc_engine.dart';
|
|||||||
import 'package:app/pages/teacher/room/viewmodel/type.dart';
|
import 'package:app/pages/teacher/room/viewmodel/type.dart';
|
||||||
import 'package:app/request/dto/room/room_user_dto.dart';
|
import 'package:app/request/dto/room/room_user_dto.dart';
|
||||||
import 'package:app/widgets/room/file_drawer.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:app/widgets/room/video_surface.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
@@ -26,7 +27,11 @@ class StudentItem extends StatefulWidget {
|
|||||||
class _StudentItemState extends State<StudentItem> {
|
class _StudentItemState extends State<StudentItem> {
|
||||||
///打开文件列表
|
///打开文件列表
|
||||||
void _openFileList() {
|
void _openFileList() {
|
||||||
showFileDialog(context, isUpload: false);
|
showFileDialog(
|
||||||
|
context,
|
||||||
|
isUpload: false,
|
||||||
|
files: widget.user.filesList,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -40,7 +45,6 @@ class _StudentItemState extends State<StudentItem> {
|
|||||||
|
|
||||||
///声音是否开启
|
///声音是否开启
|
||||||
bool isSpeakerOpen = widget.user.speekerStatus == 1;
|
bool isSpeakerOpen = widget.user.speekerStatus == 1;
|
||||||
|
|
||||||
return ClipRRect(
|
return ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
child: Container(
|
child: Container(
|
||||||
@@ -51,15 +55,18 @@ class _StudentItemState extends State<StudentItem> {
|
|||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
child: Stack(
|
child: Stack(
|
||||||
|
alignment: Alignment.bottomCenter,
|
||||||
children: [
|
children: [
|
||||||
if (widget.engine != null)
|
if (widget.engine != null)
|
||||||
AgoraVideoView(
|
VideoSurface(
|
||||||
controller: VideoViewController(
|
user: widget.user,
|
||||||
rtcEngine: widget.engine!,
|
child: AgoraVideoView(
|
||||||
canvas: VideoCanvas(uid: widget.user.rtcUid),
|
controller: VideoViewController(
|
||||||
|
rtcEngine: widget.engine!,
|
||||||
|
canvas: VideoCanvas(uid: widget.user.rtcUid),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// VideoSurface(),
|
|
||||||
Positioned(
|
Positioned(
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
@@ -79,6 +86,8 @@ class _StudentItemState extends State<StudentItem> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
///右上角选中
|
||||||
if (widget.user.userId != vm.activeSId)
|
if (widget.user.userId != vm.activeSId)
|
||||||
Positioned(
|
Positioned(
|
||||||
right: 5,
|
right: 5,
|
||||||
@@ -99,6 +108,17 @@ class _StudentItemState extends State<StudentItem> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
///举手
|
||||||
|
if (widget.user.handup == 1)
|
||||||
|
Positioned(
|
||||||
|
bottom: 40,
|
||||||
|
child: HandRaiseButton(
|
||||||
|
onTap: () {
|
||||||
|
vm.clearHandUp(widget.user.userId);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -106,6 +126,7 @@ class _StudentItemState extends State<StudentItem> {
|
|||||||
ColoredBox(
|
ColoredBox(
|
||||||
color: Color(0xFF232426),
|
color: Color(0xFF232426),
|
||||||
child: Row(
|
child: Row(
|
||||||
|
spacing: 1,
|
||||||
children: [
|
children: [
|
||||||
_actionItem(
|
_actionItem(
|
||||||
icon: isCameraOpen ? RemixIcons.video_on_fill : RemixIcons.video_off_fill,
|
icon: isCameraOpen ? RemixIcons.video_on_fill : RemixIcons.video_off_fill,
|
||||||
|
|||||||
18
lib/request/api/common_api.dart
Normal file
18
lib/request/api/common_api.dart
Normal file
@@ -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<QiuTokenDto> getQiuTokenApi(String fileKey) async {
|
||||||
|
var response = await Request().get("/files/get_qiniu_upload_token", {
|
||||||
|
"file_key": fileKey,
|
||||||
|
});
|
||||||
|
return QiuTokenDto.fromJson(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
///获取APP最新版本
|
||||||
|
Future<VersionDto> getAppVersionApi() async {
|
||||||
|
var response = await Request().get("/get_latest_version");
|
||||||
|
return VersionDto.fromJson(response);
|
||||||
|
}
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
import 'package:app/request/dto/room/rtc_token_dto.dart';
|
import 'package:app/request/dto/room/rtc_token_dto.dart';
|
||||||
import 'package:app/request/network/request.dart';
|
import 'package:app/request/network/request.dart';
|
||||||
|
|
||||||
import '../dto/room/room_info_dto.dart';
|
import '../dto/room/room_list_item_dto.dart';
|
||||||
|
|
||||||
/// 获取房间列表
|
/// 获取房间列表
|
||||||
Future<List<RoomInfoDto>> getRoomListApi() async {
|
Future<List<RoomListItemDto >> getRoomListApi() async {
|
||||||
var res = await Request().get('/study_room/get_study_room_list');
|
var res = await Request().get('/study_room/get_study_room_list');
|
||||||
return List<RoomInfoDto>.from(res.map((x) => RoomInfoDto.fromJson(x)));
|
return List<RoomListItemDto >.from(res.map((x) => RoomListItemDto .fromJson(x)));
|
||||||
}
|
}
|
||||||
|
|
||||||
///获取自习室的websocket令牌
|
///获取自习室的websocket令牌
|
||||||
|
|||||||
24
lib/request/dto/common/qiu_token_dto.dart
Normal file
24
lib/request/dto/common/qiu_token_dto.dart
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
class QiuTokenDto {
|
||||||
|
String? uploadUrl;
|
||||||
|
String? upToken;
|
||||||
|
String? fileKey;
|
||||||
|
String? domain;
|
||||||
|
|
||||||
|
QiuTokenDto({this.uploadUrl, this.upToken, this.fileKey, this.domain});
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
final map = <String, dynamic>{};
|
||||||
|
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"] ?? "";
|
||||||
|
}
|
||||||
|
}
|
||||||
47
lib/request/dto/common/version_dto.dart
Normal file
47
lib/request/dto/common/version_dto.dart
Normal file
@@ -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<String> updateContent;
|
||||||
|
DateTime createdAt;
|
||||||
|
String lowVersion;
|
||||||
|
int id;
|
||||||
|
String downloadSize;
|
||||||
|
int platform;
|
||||||
|
|
||||||
|
factory VersionDto.fromJson(Map<dynamic, dynamic> json) => VersionDto(
|
||||||
|
latestVersion: json["latest_version"],
|
||||||
|
updatedAt: DateTime.parse(json["updated_at"]),
|
||||||
|
downloadUrl: json["download_url"],
|
||||||
|
updateContent: List<String>.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<dynamic, dynamic> toJson() => {
|
||||||
|
"latest_version": latestVersion,
|
||||||
|
"updated_at": updatedAt.toIso8601String(),
|
||||||
|
"download_url": downloadUrl,
|
||||||
|
"update_content": List<dynamic>.from(updateContent.map((x) => x)),
|
||||||
|
"created_at": createdAt.toIso8601String(),
|
||||||
|
"low_version": lowVersion,
|
||||||
|
"id": id,
|
||||||
|
"download_size": downloadSize,
|
||||||
|
"platform": platform,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,41 +1,52 @@
|
|||||||
class RoomInfoDto {
|
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({
|
RoomInfoDto({
|
||||||
required this.teacherBackground,
|
required this.studyRoomId,
|
||||||
required this.teacherAvatar,
|
required this.teacherId,
|
||||||
required this.roomName,
|
required this.teacherRtcUid,
|
||||||
required this.startTime,
|
required this.teacherWsClientId,
|
||||||
required this.teacherName,
|
required this.roomStatus,
|
||||||
required this.endTime,
|
required this.dataType,
|
||||||
required this.id,
|
required this.roomStartTime,
|
||||||
|
required this.roomEndTime,
|
||||||
|
required this.boardUuid,
|
||||||
});
|
});
|
||||||
|
|
||||||
String teacherBackground;
|
Map<String, dynamic> toJson() {
|
||||||
String teacherAvatar;
|
final map = <String, dynamic>{};
|
||||||
String roomName;
|
map["study_room_id"] = studyRoomId;
|
||||||
String startTime;
|
map["teacher_id"] = teacherId;
|
||||||
String teacherName;
|
map["teacher_rtc_uid"] = teacherRtcUid;
|
||||||
String endTime;
|
map["teacher_ws_client_id"] = teacherWsClientId;
|
||||||
int id;
|
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<dynamic, dynamic> json) =>
|
factory RoomInfoDto.fromJson(Map<String, dynamic> json) {
|
||||||
RoomInfoDto(
|
return RoomInfoDto(
|
||||||
teacherBackground: json["teacher_background"],
|
studyRoomId: json["study_room_id"] ?? 0,
|
||||||
teacherAvatar: json["teacher_avatar"],
|
teacherId: json["teacher_id"] ?? 0,
|
||||||
roomName: json["room_name"],
|
teacherRtcUid: json["teacher_rtc_uid"] ?? 0,
|
||||||
startTime: json["start_time"],
|
teacherWsClientId: json["teacher_ws_client_id"] ?? "",
|
||||||
teacherName: json["teacher_name"],
|
roomStatus: json["room_status"] ?? 0,
|
||||||
endTime: json["end_time"],
|
dataType: json["data_type"] ?? "",
|
||||||
id: json["id"],
|
roomStartTime: json["room_start_time"] ?? "",
|
||||||
);
|
roomEndTime: json["room_end_time"] ?? "",
|
||||||
|
boardUuid: json["whiteboard_uuid"] ?? "",
|
||||||
Map<dynamic, dynamic> toJson() =>
|
);
|
||||||
{
|
}
|
||||||
"teacher_background": teacherBackground,
|
|
||||||
"teacher_avatar": teacherAvatar,
|
|
||||||
"room_name": roomName,
|
|
||||||
"start_time": startTime,
|
|
||||||
"teacher_name": teacherName,
|
|
||||||
"end_time": endTime,
|
|
||||||
"id": id,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|||||||
51
lib/request/dto/room/room_list_item_dto.dart
Normal file
51
lib/request/dto/room/room_list_item_dto.dart
Normal file
@@ -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<dynamic, dynamic> 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<dynamic, dynamic> 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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<String, dynamic> toJson() {
|
|
||||||
final map = <String, dynamic>{};
|
|
||||||
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<String, dynamic> 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"] ?? "",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -10,7 +10,7 @@ class RoomUserDto {
|
|||||||
|
|
||||||
/// 1是学生,2是老师
|
/// 1是学生,2是老师
|
||||||
final int userType;
|
final int userType;
|
||||||
final List<String> filesList;
|
List<String> filesList;
|
||||||
final String dataType;
|
final String dataType;
|
||||||
int handup;
|
int handup;
|
||||||
int online; //0离线,1在线
|
int online; //0离线,1在线
|
||||||
|
|||||||
@@ -80,6 +80,12 @@ enum RoomEvent {
|
|||||||
///老师关闭学生的麦克风
|
///老师关闭学生的麦克风
|
||||||
closeStudentMic("sys_control_mute_microphone"),
|
closeStudentMic("sys_control_mute_microphone"),
|
||||||
|
|
||||||
|
///老师打开学生的麦克风
|
||||||
|
openStudentMic("sys_control_unmute_microphone"),
|
||||||
|
|
||||||
|
///老师开启学生的摄像头
|
||||||
|
openStudentCamera("sys_control_unmute_camera"),
|
||||||
|
|
||||||
///老师关闭学生的摄像头
|
///老师关闭学生的摄像头
|
||||||
closeStudentCamera("sys_control_mute_camera"),
|
closeStudentCamera("sys_control_mute_camera"),
|
||||||
|
|
||||||
|
|||||||
@@ -66,7 +66,10 @@ class RoomWebSocket {
|
|||||||
print("未识别的 action: ${jsonMap['action']},消息已忽略");
|
print("未识别的 action: ${jsonMap['action']},消息已忽略");
|
||||||
return; // 直接跳过
|
return; // 直接跳过
|
||||||
} else {
|
} else {
|
||||||
logger.i("接收到事件: ${event.value}");
|
logger.i("""
|
||||||
|
接收到事件: ${event.value}
|
||||||
|
数据: ${jsonMap['data']}
|
||||||
|
""");
|
||||||
}
|
}
|
||||||
final msg = RoomMessage(event, jsonMap['data']);
|
final msg = RoomMessage(event, jsonMap['data']);
|
||||||
_msgController.add(msg);
|
_msgController.add(msg);
|
||||||
@@ -96,7 +99,7 @@ class RoomWebSocket {
|
|||||||
"action": action.value,
|
"action": action.value,
|
||||||
if (params != null) ...params,
|
if (params != null) ...params,
|
||||||
};
|
};
|
||||||
if(action != RoomCommand.ping){
|
if (action != RoomCommand.ping) {
|
||||||
logger.i("发送指令:$msg");
|
logger.i("发送指令:$msg");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
6
lib/utils/common.dart
Normal file
6
lib/utils/common.dart
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
///判断是否是安卓
|
||||||
|
bool isAndroid(){
|
||||||
|
return Platform.isAndroid;
|
||||||
|
}
|
||||||
@@ -33,21 +33,41 @@ String formatDate(dynamic date, [String format = 'YYYY-MM-DD hh:mm:ss']) {
|
|||||||
|
|
||||||
/// 将秒数格式化为 00:00 或 00:00:00
|
/// 将秒数格式化为 00:00 或 00:00:00
|
||||||
/// - [seconds]: 秒数
|
/// - [seconds]: 秒数
|
||||||
String formatSeconds(int seconds) {
|
/// - [format]: 格式化字符串,默认为 "hh:mm:ss"
|
||||||
final h = seconds ~/ 3600;
|
String formatSeconds(
|
||||||
final m = (seconds % 3600) ~/ 60;
|
int seconds, [
|
||||||
final s = seconds % 60;
|
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) {
|
String two(int n) => n.toString().padLeft(2, '0');
|
||||||
return '${twoDigits(h)}:${twoDigits(m)}:${twoDigits(s)}';
|
|
||||||
} else {
|
// 支持以下 token:
|
||||||
return '${twoDigits(m)}:${twoDigits(s)}';
|
// 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
|
/// 将 "HH", "HH:mm" 或 "HH:mm:ss" 转为当天 DateTime
|
||||||
DateTime parseTime(String timeStr) {
|
DateTime parseTime(String timeStr) {
|
||||||
final now = DateTime.now();
|
final now = DateTime.now();
|
||||||
|
|||||||
81
lib/utils/transfer/download.dart
Normal file
81
lib/utils/transfer/download.dart
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
//下载文件
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
|
||||||
|
class LocalDownload {
|
||||||
|
static Future<String> 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<int> chunkList = [];
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
response.listen(
|
||||||
|
(List<int> 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<String> getFilePath({
|
||||||
|
required url,
|
||||||
|
required path,
|
||||||
|
}) async {
|
||||||
|
//获取本地文件路径
|
||||||
|
String filePath = await getLocalFilePath(url, path);
|
||||||
|
File file = File(filePath);
|
||||||
|
if (file.existsSync()) {
|
||||||
|
return file.path;
|
||||||
|
} else {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
55
lib/utils/transfer/upload.dart
Normal file
55
lib/utils/transfer/upload.dart
Normal file
@@ -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<QiuTokenDto> _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<String?> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
26
lib/widgets/base/actionSheet/action_sheet.dart
Normal file
26
lib/widgets/base/actionSheet/action_sheet.dart
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'action_sheet_ui.dart';
|
||||||
|
import 'type.dart';
|
||||||
|
|
||||||
|
void showActionSheet(
|
||||||
|
BuildContext context, {
|
||||||
|
required List<ActionSheetItem> 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,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
65
lib/widgets/base/actionSheet/action_sheet_ui.dart
Normal file
65
lib/widgets/base/actionSheet/action_sheet_ui.dart
Normal file
@@ -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<ActionSheetItem> 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("取消"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
18
lib/widgets/base/actionSheet/type.dart
Normal file
18
lib/widgets/base/actionSheet/type.dart
Normal file
@@ -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,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -6,18 +6,20 @@ import '../config/config.dart';
|
|||||||
class Button extends StatelessWidget {
|
class Button extends StatelessWidget {
|
||||||
final double? width;
|
final double? width;
|
||||||
final String text;
|
final String text;
|
||||||
|
final TextStyle textStyle;
|
||||||
final ThemeType type;
|
final ThemeType type;
|
||||||
final BorderRadius radius;
|
final BorderRadius radius;
|
||||||
final VoidCallback onPressed;
|
final VoidCallback? onPressed;
|
||||||
final bool loading;
|
final bool loading;
|
||||||
final bool disabled;
|
final bool disabled;
|
||||||
|
|
||||||
const Button({
|
const Button({
|
||||||
super.key,
|
super.key,
|
||||||
this.width,
|
this.width,
|
||||||
|
this.textStyle = const TextStyle(),
|
||||||
this.radius = const BorderRadius.all(Radius.circular(80)),
|
this.radius = const BorderRadius.all(Radius.circular(80)),
|
||||||
required this.text,
|
required this.text,
|
||||||
required this.onPressed,
|
this.onPressed,
|
||||||
this.type = ThemeType.primary,
|
this.type = ThemeType.primary,
|
||||||
this.loading = false,
|
this.loading = false,
|
||||||
this.disabled = false,
|
this.disabled = false,
|
||||||
@@ -34,7 +36,7 @@ class Button extends StatelessWidget {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return Opacity(
|
return Opacity(
|
||||||
opacity: disabled || loading ? 0.5 : 1,
|
opacity: disabled || loading ? 0.5 : 1,
|
||||||
child: Container(
|
child: Container(
|
||||||
width: width,
|
width: width,
|
||||||
decoration: bgDecoration.copyWith(borderRadius: radius),
|
decoration: bgDecoration.copyWith(borderRadius: radius),
|
||||||
@@ -62,7 +64,7 @@ class Button extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
text,
|
text,
|
||||||
style: TextStyle(
|
style: textStyle.copyWith(
|
||||||
color: type != ThemeType.info ? Colors.white : Colors.black,
|
color: type != ThemeType.info ? Colors.white : Colors.black,
|
||||||
),
|
),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
|
|||||||
@@ -40,6 +40,9 @@ class FilePreviewer extends StatelessWidget {
|
|||||||
child = InteractiveViewer(
|
child = InteractiveViewer(
|
||||||
child: CachedNetworkImage(
|
child: CachedNetworkImage(
|
||||||
imageUrl: url,
|
imageUrl: url,
|
||||||
|
placeholder: (_, __) => const Center(
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else if (_isPdf(suffix)) {
|
} else if (_isPdf(suffix)) {
|
||||||
|
|||||||
93
lib/widgets/room/core/count_down_vm.dart
Normal file
93
lib/widgets/room/core/count_down_vm.dart
Normal file
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,14 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:app/config/theme/base/app_theme_ext.dart';
|
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:app/widgets/base/button/index.dart';
|
||||||
|
import 'package:file_picker/file_picker.dart';
|
||||||
import 'package:flutter/material.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';
|
import '../common/preview/file_previewer.dart';
|
||||||
|
|
||||||
@@ -8,6 +16,9 @@ import '../common/preview/file_previewer.dart';
|
|||||||
void showFileDialog(
|
void showFileDialog(
|
||||||
BuildContext context, {
|
BuildContext context, {
|
||||||
bool isUpload = true,
|
bool isUpload = true,
|
||||||
|
String? name,
|
||||||
|
List<String> files = const [],
|
||||||
|
ValueChanged<List<String>>? onConfirm,
|
||||||
}) {
|
}) {
|
||||||
showGeneralDialog(
|
showGeneralDialog(
|
||||||
context: context,
|
context: context,
|
||||||
@@ -16,7 +27,10 @@ void showFileDialog(
|
|||||||
barrierLabel: "RightSheet",
|
barrierLabel: "RightSheet",
|
||||||
pageBuilder: (context, animation, secondaryAnimation) {
|
pageBuilder: (context, animation, secondaryAnimation) {
|
||||||
return FileDrawer(
|
return FileDrawer(
|
||||||
|
name: name,
|
||||||
isUpload: isUpload,
|
isUpload: isUpload,
|
||||||
|
files: files,
|
||||||
|
onConfirm: onConfirm,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
transitionBuilder: (context, animation, secondaryAnimation, child) {
|
transitionBuilder: (context, animation, secondaryAnimation, child) {
|
||||||
@@ -31,15 +45,99 @@ void showFileDialog(
|
|||||||
|
|
||||||
///文件弹窗
|
///文件弹窗
|
||||||
class FileDrawer extends StatefulWidget {
|
class FileDrawer extends StatefulWidget {
|
||||||
|
final String? name;
|
||||||
|
final List<String> files;
|
||||||
final bool isUpload;
|
final bool isUpload;
|
||||||
|
final ValueChanged<List<String>>? onConfirm;
|
||||||
|
|
||||||
const FileDrawer({super.key, this.isUpload = true});
|
const FileDrawer({
|
||||||
|
super.key,
|
||||||
|
this.name,
|
||||||
|
this.isUpload = true,
|
||||||
|
this.files = const [],
|
||||||
|
this.onConfirm,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<FileDrawer> createState() => _FileDrawerState();
|
State<FileDrawer> createState() => _FileDrawerState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _FileDrawerState extends State<FileDrawer> {
|
class _FileDrawerState extends State<FileDrawer> {
|
||||||
|
///文件列表
|
||||||
|
List<String> _fileList = [];
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_fileList = List<String>.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<File> 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<String>().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<String>().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<String> uploadedPaths = (await Future.wait(
|
||||||
|
uploadTasks,
|
||||||
|
)).whereType<String>().toList();
|
||||||
|
|
||||||
|
EasyLoading.dismiss();
|
||||||
|
|
||||||
|
// 更新 UI
|
||||||
|
setState(() => _fileList.addAll(uploadedPaths));
|
||||||
|
|
||||||
|
// 回调
|
||||||
|
widget.onConfirm?.call(uploadedPaths);
|
||||||
|
} catch (e) {
|
||||||
|
print(e);
|
||||||
|
EasyLoading.showToast("文件上传失败");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Align(
|
return Align(
|
||||||
@@ -52,39 +150,45 @@ class _FileDrawerState extends State<FileDrawer> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'上传文件列表',
|
"${widget.name ?? ""}上传文件列表",
|
||||||
style: Theme.of(context).textTheme.titleSmall,
|
style: Theme.of(context).textTheme.titleSmall,
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: ListView.separated(
|
child: Visibility(
|
||||||
padding: EdgeInsets.symmetric(vertical: 15),
|
visible: _fileList.isNotEmpty,
|
||||||
itemBuilder: (_, index) {
|
replacement: Align(
|
||||||
return InkWell(
|
child: Text("未上传文件"),
|
||||||
onTap: () {
|
),
|
||||||
showFilePreviewer(
|
child: ListView.separated(
|
||||||
context,
|
padding: EdgeInsets.symmetric(vertical: 15),
|
||||||
url: "https://doaf.asia/api/assets/1/图/65252305_p0.jpg",
|
itemBuilder: (_, index) {
|
||||||
);
|
String item = _fileList[index];
|
||||||
},
|
String suffix = item.split(".").last;
|
||||||
child: Container(
|
return InkWell(
|
||||||
padding: EdgeInsets.symmetric(vertical: 10, horizontal: 10),
|
key: Key(item),
|
||||||
decoration: BoxDecoration(
|
onTap: () {
|
||||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
showFilePreviewer(context, url: item);
|
||||||
borderRadius: BorderRadius.circular(5),
|
},
|
||||||
|
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: _fileList.length,
|
||||||
separatorBuilder: (_, __) => SizedBox(height: 15),
|
),
|
||||||
itemCount: 15,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Visibility(
|
Visibility(
|
||||||
visible: widget.isUpload,
|
visible: widget.isUpload,
|
||||||
child: Button(
|
child: Button(
|
||||||
text: "上传",
|
text: "上传",
|
||||||
onPressed: () {},
|
onPressed: _handOpenActionSheet,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -93,3 +197,12 @@ class _FileDrawerState extends State<FileDrawer> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
///数据类
|
||||||
|
class UploadFileItem {
|
||||||
|
String url;
|
||||||
|
String name;
|
||||||
|
bool loading;
|
||||||
|
|
||||||
|
UploadFileItem({required this.url, this.loading = false, required this.name});
|
||||||
|
}
|
||||||
|
|||||||
26
lib/widgets/room/other_widget.dart
Normal file
26
lib/widgets/room/other_widget.dart
Normal file
@@ -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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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: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 {
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
String stateText = switch (state) {
|
//摄像头是否关闭
|
||||||
VideoState.closed => "摄像头已关闭",
|
if (user.cameraStatus == 0) {
|
||||||
VideoState.offline => "掉线",
|
|
||||||
VideoState.loading => "加载中",
|
|
||||||
VideoState.error => "错误",
|
|
||||||
_ => "未知",
|
|
||||||
};
|
|
||||||
//如果不是正常
|
|
||||||
if (state != VideoState.normal) {
|
|
||||||
return Align(
|
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)),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
80
lib/widgets/version/version_dialog.dart
Normal file
80
lib/widgets/version/version_dialog.dart
Normal file
@@ -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<String, dynamic> 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<String> v1Parts = version1.split('.');
|
||||||
|
List<String> 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;
|
||||||
|
}
|
||||||
174
lib/widgets/version/version_ui.dart
Normal file
174
lib/widgets/version/version_ui.dart
Normal file
@@ -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<String> updateNotice;
|
||||||
|
final String uploadUrl; //下载地址
|
||||||
|
|
||||||
|
const AppUpdateUi({
|
||||||
|
super.key,
|
||||||
|
required this.version,
|
||||||
|
required this.updateNotice,
|
||||||
|
required this.uploadUrl,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<AppUpdateUi> createState() => _UpdateUiState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _UpdateUiState extends State<AppUpdateUi> {
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
180
pubspec.lock
180
pubspec.lock
@@ -9,6 +9,22 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.5.3"
|
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:
|
async:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -73,8 +89,16 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.19.1"
|
version: "1.19.1"
|
||||||
crypto:
|
cross_file:
|
||||||
dependency: transitive
|
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:
|
description:
|
||||||
name: crypto
|
name: crypto
|
||||||
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
|
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
|
||||||
@@ -89,6 +113,14 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.8"
|
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:
|
dio:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -129,6 +161,46 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "7.0.1"
|
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:
|
fixnum:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -187,6 +259,14 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.3"
|
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:
|
flutter_screenutil:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -237,6 +317,70 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.1.2"
|
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:
|
intl:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -485,6 +629,14 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.2.1"
|
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:
|
platform:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -722,6 +874,22 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "15.0.0"
|
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:
|
web:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -746,6 +914,14 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.0"
|
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:
|
sdks:
|
||||||
dart: ">=3.8.1 <4.0.0"
|
dart: ">=3.8.1 <4.0.0"
|
||||||
flutter: ">=3.31.0-0.0.pre"
|
flutter: ">=3.32.0"
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ name: app
|
|||||||
description: "A new Flutter project."
|
description: "A new Flutter project."
|
||||||
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
||||||
|
|
||||||
version: 1.0.0+1
|
version: 1.1.1
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.8.1
|
sdk: ^3.8.1
|
||||||
@@ -28,6 +28,11 @@ dependencies:
|
|||||||
flutter_cached_pdfview: ^0.4.3
|
flutter_cached_pdfview: ^0.4.3
|
||||||
skeletonizer: ^2.1.0+1
|
skeletonizer: ^2.1.0+1
|
||||||
agora_rtc_engine: ^6.5.3
|
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:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
Reference in New Issue
Block a user