第 36 章 迷你播放器、桌面歌词悬浮窗
本章目标
- 新建第二个 WebviewWindow:mini-player,340×140,透明+无边框。
- 新建第三个窗口:lyric-overlay,透明、置顶、所有工作区可见。
- 窗口间通信:事件广播 + 共享
AppState。
一、窗口配置
tauri.conf.json 的 app.windows 可以声明多个,但更灵活的是在 Rust 里 WebviewWindowBuilder 按需建。
// core/window/mini.rs
use tauri::{WebviewWindowBuilder, WebviewUrl, AppHandle};
pub fn open_mini(app: &AppHandle) -> tauri::Result<()> {
if let Some(w) = app.get_webview_window("mini") { w.set_focus()?; return Ok(()); }
let win = WebviewWindowBuilder::new(app, "mini", WebviewUrl::App("mini.html".into()))
.title("CloudTone Mini")
.inner_size(340.0, 140.0)
.decorations(false)
.transparent(true)
.always_on_top(true)
.resizable(false)
.shadow(false)
.skip_taskbar(true)
.build()?;
// 让迷你窗口默认右下角
if let Some(monitor) = win.current_monitor()? {
let size = monitor.size();
win.set_position(tauri::PhysicalPosition::new(
(size.width as i32) - 360,
(size.height as i32) - 180,
))?;
}
Ok(())
}
pub fn open_lyric(app: &AppHandle) -> tauri::Result<()> {
if let Some(w) = app.get_webview_window("lyric") { w.set_focus()?; return Ok(()); }
WebviewWindowBuilder::new(app, "lyric", WebviewUrl::App("lyric.html".into()))
.title("桌面歌词")
.inner_size(1000.0, 120.0)
.decorations(false)
.transparent(true)
.always_on_top(true)
.skip_taskbar(true)
.position(100.0, 50.0)
.build()?;
Ok(())
}
二、Vite 多入口
vite.config.ts:
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { resolve } from "node:path";
export default defineConfig({
plugins: [react()],
build: {
rollupOptions: {
input: {
main: resolve(__dirname, "index.html"),
mini: resolve(__dirname, "mini.html"),
lyric: resolve(__dirname, "lyric.html"),
},
},
},
});
根目录新增 mini.html / lyric.html,各自引用不同的 entry TSX:
<!-- mini.html -->
<!DOCTYPE html><html><head><meta charset="UTF-8"><title>Mini</title></head>
<body><div id="root"></div><script type="module" src="/src/mini/main.tsx"></script></body></html>
// src/mini/main.tsx
import ReactDOM from "react-dom/client";
import "../index.css";
import { MiniApp } from "./MiniApp";
ReactDOM.createRoot(document.getElementById("root")!).render(<MiniApp />);
三、迷你播放器 UI
// src/mini/MiniApp.tsx
import { usePlayerSync, usePlayer } from "@/features/player/player";
export function MiniApp() {
usePlayerSync();
const { currentSong, isPlaying, toggle, next, prev } = usePlayer();
return (
<div data-tauri-drag-region
className="h-screen w-screen bg-[rgba(20,20,30,0.85)] backdrop-blur-xl rounded-xl p-3 flex items-center gap-3 text-white">
<img src={`cover://${currentSong?.fileHash ?? 'fallback'}`} className="w-14 h-14 rounded-lg" />
<div className="flex-1 min-w-0">
<div className="truncate font-medium">{currentSong?.title ?? "未在播放"}</div>
<div className="truncate text-text-secondary text-xs">{currentSong?.artist}</div>
</div>
<div className="flex items-center gap-2">
<button onClick={prev}><SkipBack size={18} /></button>
<button onClick={toggle}>{isPlaying ? <Pause size={22} /> : <Play size={22} />}</button>
<button onClick={next}><SkipForward size={18} /></button>
</div>
</div>
);
}
data-tauri-drag-region 是 Tauri 的拖动提示:任何设置了它的元素,用户拖动窗口就能移动。
四、让所有窗口共享播放状态
播放器后端通过 broadcast::Sender<PlayerEvent> 发事件,Tauri 层做桥接:
// setup 里
let handle = app.handle().clone();
let mut rx = player.subscribe();
tokio::spawn(async move {
while let Ok(ev) = rx.recv().await {
let _ = handle.emit("player:event", &ev);
}
});
前端 (所有窗口) 用同一个 usePlayerSync 监听 player:event 即可,无需区分窗口。
五、歌词悬浮窗
lyric.html 只渲染一行/两行歌词:
export function LyricOverlay() {
usePlayerSync();
const { currentSong, progressSec } = usePlayer();
const { data: lines = [] } = useLyrics(currentSong?.id);
const line = useMemo(() => findActive(lines, progressSec * 1000), [lines, progressSec]);
const next = lines[Math.max(line + 1, 0)];
return (
<div data-tauri-drag-region
className="h-screen w-screen flex flex-col items-center justify-center text-center select-none">
<div className="text-4xl font-bold text-white drop-shadow-lg">{lines[line]?.text}</div>
<div className="text-xl text-white/60 mt-1">{next?.text}</div>
</div>
);
}
六、点击穿透(仅桌面歌词)
用户希望鼠标穿透歌词悬浮窗。Tauri 2.x:
if let Some(w) = app.get_webview_window("lyric") {
w.set_ignore_cursor_events(true)?;
}
进入"锁定模式"时开启,前端切换状态时调用 command。
七、托盘菜单控制
在 Tray 里加入:
Submenu::with_items(app, "窗口", true, &[
&MenuItem::with_id(app, "open_mini", "打开迷你播放器", true, None::<&str>)?,
&MenuItem::with_id(app, "open_lyric", "打开桌面歌词", true, None::<&str>)?,
])?
本章小结
- Tauri 多窗口是它对 Electron 的一大优势:单进程多 WebView,资源共享。
- 透明 + 无边框 + always_on_top + 拖动区域 = 漂亮的悬浮窗。
- 状态靠后端事件广播保持同步。
动手时刻
- 打开迷你播放器,拖到屏幕角落。
- 打开桌面歌词,切换穿透模式。
下一章:全局媒体键与系统媒体中心。