- 重构 Redis 投屏状态存储,统一使用 screenShareOwnerUid 字段 - 移除冗余的 screenShareUid 字段,简化房间状态数据结构 - 新增 client_mute_all_students 消息类型支持全员禁麦功能 - 实现创建者权限验证的全员禁麦逻辑,排除老师角色用户 - 优化 Tauri 桌面应用悬浮窗创建接口,支持状态参数传递 - 添加悬浮窗关闭功能及教师端静音状态同步机制 - 修复学生端界面中投屏状态变量命名不一致问题 - 统一按钮样式规范,提升界面组件一致性
182 lines
7.2 KiB
Rust
182 lines
7.2 KiB
Rust
use std::sync::Arc;
|
||
|
||
use crate::{
|
||
constants::common::*,
|
||
window::{
|
||
WindowLabel,
|
||
derive::FloatListInfo,
|
||
util::{get_monitor_for_window, show_windows_by_label},
|
||
},
|
||
};
|
||
use tauri::{AppHandle, LogicalPosition, LogicalSize, Runtime, WebviewUrl, WebviewWindowBuilder, utils::config::BackgroundThrottlingPolicy};
|
||
|
||
/// 创建悬浮列表窗口(内部实现)
|
||
pub async fn create_float_list_window_impl<R: Runtime>(app: Arc<AppHandle<R>>, is_teacher_muted: bool) -> Result<(), String> {
|
||
if show_windows_by_label(app.as_ref(), WindowLabel::FLOAT_LIST) {
|
||
return Ok(());
|
||
}
|
||
// 悬浮列表尺寸
|
||
let float_info = calculate_float_info_sync();
|
||
// 获取屏幕尺寸,优先为 main 窗口所在屏幕,主窗口没有打开时,才找主屏幕
|
||
let monitor = get_monitor_for_window(app.as_ref())?;
|
||
// 获取屏幕缩放因子
|
||
let scale_factor = monitor.scale_factor();
|
||
let page_size: LogicalSize<f64> = monitor.work_area().size.to_logical(scale_factor);
|
||
let screen_width = page_size.width;
|
||
let screen_height = page_size.height;
|
||
// 构建 URL 时附加状态参数
|
||
let url = format!("floating-list-window.html?isTeacherMuted={}", is_teacher_muted);
|
||
|
||
let label = WindowLabel::FLOAT_LIST.as_ref();
|
||
// 创建悬浮球窗口,直接在初始化时设置好位置和尺寸
|
||
let window = WebviewWindowBuilder::new(app.as_ref(), label, WebviewUrl::App(url.into()))
|
||
.title("悬浮列表")
|
||
.inner_size(float_info.width as f64, float_info.height as f64)
|
||
.max_inner_size(float_info.width as f64, float_info.height as f64)
|
||
.min_inner_size(float_info.width as f64, float_info.height as f64)
|
||
.position(screen_width - float_info.width as f64, 0.0)
|
||
.transparent(true)
|
||
.visible_on_all_workspaces(true)
|
||
.background_throttling(BackgroundThrottlingPolicy::Disabled)
|
||
.decorations(false)
|
||
.shadow(false)
|
||
.focusable(true)
|
||
.focused(false)
|
||
.accept_first_mouse(true)
|
||
.skip_taskbar(true)
|
||
.always_on_top(true)
|
||
.devtools(true)
|
||
.build();
|
||
|
||
let window = match window {
|
||
Ok(w) => w,
|
||
Err(e) => {
|
||
log::error!("{} {}", MSG_FAILED_CREATE_FLOATING_LIST, e);
|
||
return Err(format!("{} {}", MSG_FAILED_CREATE_FLOATING_LIST, e));
|
||
}
|
||
};
|
||
|
||
#[cfg(not(target_os = "linux"))]
|
||
{
|
||
let _ = window.set_resizable(false);
|
||
}
|
||
#[cfg(target_os = "windows")]
|
||
{
|
||
let _ = window.set_size(LogicalSize::new(float_info.width as f64, float_info.height as f64));
|
||
}
|
||
|
||
// 窗口显示后等待 200 毫秒再执行动画
|
||
tokio::spawn(async move {
|
||
tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;
|
||
let _ = animate_to_bottom_right(window, screen_width as i32, screen_height as i32).await;
|
||
});
|
||
Ok(())
|
||
}
|
||
|
||
/// 关闭悬浮列表窗口(内部实现)
|
||
pub fn close_float_list_window_impl<R: Runtime>(app: Arc<AppHandle<R>>) -> Result<(), String> {
|
||
use tauri::Manager;
|
||
|
||
// 尝试获取悬浮列表窗口
|
||
if let Some(window) = app.get_webview_window(WindowLabel::FLOAT_LIST.as_ref()) {
|
||
// 关闭窗口
|
||
window.close().map_err(|e| format!("关闭悬浮列表窗口失败:{}", e))?;
|
||
log::info!("已成功关闭悬浮列表窗口");
|
||
} else {
|
||
log::warn!("悬浮列表窗口不存在,无需关闭");
|
||
}
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// 根据用户登录状态和课程选择情况动态计算弹出窗口高度
|
||
pub fn calculate_float_info_sync() -> FloatListInfo {
|
||
// 默认基础项目数
|
||
let item_num = 4;
|
||
// 每个item高度
|
||
let item_height = 64;
|
||
// 总高度 = item高度 * 项目数 + 额外用于移动按钮和时间部分的高度
|
||
let popup_height = item_height * item_num + 60;
|
||
let popup_width = 60;
|
||
|
||
// 返回计算得到的弹出窗口尺寸
|
||
FloatListInfo {
|
||
width: popup_width as u32,
|
||
height: popup_height as u32,
|
||
}
|
||
}
|
||
|
||
/// 使用抛物线动画将窗口移动到右下角
|
||
pub async fn animate_to_bottom_right<R: Runtime>(window: tauri::WebviewWindow<R>, screen_width: i32, screen_height: i32) -> Result<(), String> {
|
||
// 获取当前窗口位置和大小
|
||
let current_position_physical = window.outer_position().map_err(|e| format!("获取窗口位置失败: {}", e))?;
|
||
let current_size_physical = window.outer_size().map_err(|e| format!("获取窗口大小失败: {}", e))?;
|
||
let scale_factor = window.scale_factor().map_err(|e| format!("获取缩放因子失败: {}", e))?;
|
||
|
||
// 转换为逻辑位置和大小
|
||
let current_position = current_position_physical.to_logical::<i32>(scale_factor);
|
||
let current_size = current_size_physical.to_logical::<i32>(scale_factor);
|
||
|
||
let start_x = current_position.x;
|
||
let start_y = current_position.y;
|
||
|
||
// 计算目标位置(右下角)
|
||
// 注意:screen_width 和 screen_height 已经在调用方考虑了缩放因子
|
||
let end_x = screen_width - current_size.width;
|
||
let end_y = screen_height - current_size.height - 120;
|
||
|
||
// 计算移动距离
|
||
let distance_x = (end_x - start_x) as f64;
|
||
let distance_y = (end_y - start_y) as f64;
|
||
|
||
// 动画参数
|
||
let duration = 800; // 总动画时间(毫秒)
|
||
let start_time = std::time::Instant::now();
|
||
|
||
// 抛物线控制参数
|
||
// 抛物线顶点高度偏移量(负值表示向下偏移,形成开口向上的抛物线)
|
||
let apex_offset = -(distance_y.abs() * 0.3).min(200.0);
|
||
|
||
// 执行动画
|
||
loop {
|
||
let elapsed = start_time.elapsed().as_millis() as u64;
|
||
if elapsed >= duration {
|
||
break;
|
||
}
|
||
|
||
let progress = (elapsed as f64) / (duration as f64);
|
||
|
||
// 使用抛物线轨迹计算位置
|
||
// 水平方向匀速运动
|
||
let current_x = (start_x as f64 + distance_x * progress).round() as i32;
|
||
|
||
// 垂直方向抛物线运动(开口向上,波峰向下)
|
||
// y = apex_offset * (4*x^2 - 4*x) 形成开口向上的抛物线,谷点在 x=0.5 处
|
||
let parabolic_factor = apex_offset * (4.0 * progress * progress - 4.0 * progress);
|
||
let current_y = (start_y as f64 + distance_y * progress + parabolic_factor).round() as i32;
|
||
|
||
// 更新窗口位置,使用 LogicalPosition
|
||
if let Err(_e) = window.set_position(LogicalPosition::new(current_x as f64, current_y as f64)) {
|
||
break;
|
||
}
|
||
|
||
// 短暂休眠以控制动画帧率
|
||
tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
|
||
}
|
||
|
||
// 确保最终位置准确, 再次获取窗口大小和缩放因子
|
||
let float_info = calculate_float_info_sync();
|
||
let current_size_physical = window.outer_size().map_err(|e| format!("获取窗口大小失败: {}", e))?;
|
||
let scale_factor = window.scale_factor().map_err(|e| format!("获取缩放因子失败: {}", e))?;
|
||
let current_size = current_size_physical.to_logical::<i32>(scale_factor);
|
||
let _ = window.set_size(LogicalSize::new(float_info.width as f64, float_info.height as f64));
|
||
let end_x = screen_width - current_size.width;
|
||
let end_y = screen_height - current_size.height - 120;
|
||
|
||
if let Err(e) = window.set_position(LogicalPosition::new(end_x as f64, end_y as f64)) {
|
||
return Err(format!("设置最终窗口位置失败: {}", e));
|
||
}
|
||
|
||
Ok(())
|
||
}
|