构建第一个主题
这篇教程里,我们给 foobar2000 做一个小巧的播放器页面,最后把它装成主题。页面显示当前曲目的封面、标题和艺术家,一条带已播放时间和总时长的进度条,以及上一首、播放 / 暂停、下一首三个按钮。开发期间 foobar2000 从开发服务器加载页面,文件一保存,改动就出现在窗口里。
开发服务器与构建用 Vite,现成的播放器元素来自 foo-webview-sdk 包,不需要框架。做完的项目在仓库的 examples/starter。
开始之前
需要准备:
- foobar2000 2.x,已装好 foo_ui_webview2,并把用户界面选为
Webview2 UI。两步都见安装组件。 - Node.js 20.19 或更高版本。运行
node --version查看当前版本。 - foobar2000 播放列表里有几首曲目。
下文的选项和按钮名按组件语言为中文时的文字书写。
创建项目
新建一个空文件夹 my-theme,放在 foobar2000 的 profile 文件夹之外;主题做完后,我们再把它复制进 profile。
在 my-theme 里创建 package.json:
{
"name": "my-foobar2000-theme",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"foo-webview-sdk": "2.0.0"
},
"devDependencies": {
"vite": "8.3.1"
},
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
}在 my-theme 打开终端,安装依赖:
npm installnpm 装完后,my-theme 里会多出 node_modules 文件夹和 package-lock.json 文件。
编写页面
接下来添加四个文件。先是 vite.config.js,它把开发服务器的端口固定下来:
import { defineConfig } from 'vite';
export default defineConfig({
server: {
port: 5173,
// foobar2000 loads the address typed into its preferences; if the port is
// taken, fail instead of moving to another one.
strictPort: true,
},
});然后是 index.html。每个 fb-* 标签都是 SDK 提供的播放器元素:它自己向 foobar2000 取数据,曲目或播放位置变了就自己更新。
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>My foobar2000 theme</title>
</head>
<body>
<main class="player">
<fb-cover-art class="cover"></fb-cover-art>
<div class="info">
<fb-track-text class="title" field="title" placeholder="Nothing playing"></fb-track-text>
<fb-track-text class="artist" field="artist"></fb-track-text>
</div>
<div class="time">
<fb-time-current></fb-time-current>
<fb-seek-bar class="seek"></fb-seek-bar>
<fb-time-total></fb-time-total>
</div>
<div class="controls">
<fb-prev-button></fb-prev-button>
<fb-play-button></fb-play-button>
<fb-next-button></fb-next-button>
</div>
</main>
<script type="module" src="/src/main.js"></script>
</body>
</html>新建 src 文件夹。里面的 src/main.js 注册 fb-* 元素并加载样式表:
import { registerComponents } from 'foo-webview-sdk/components';
import './style.css';
registerComponents();最后是 src/style.css。这些元素本身不带任何外观,以 ::part(...) 结尾的规则给元素内部的部件上样式,比如播放按钮里的按钮、进度条的填充段。
:root {
--accent: #4cc2ff;
color-scheme: dark;
color: #f3f3f3;
background: #202020;
font-family: 'Segoe UI', system-ui, sans-serif;
}
body {
margin: 0;
min-height: 100vh;
display: grid;
place-items: center;
}
.player {
width: 320px;
display: grid;
gap: 16px;
}
.cover {
width: 320px;
height: 320px;
border-radius: 8px;
overflow: hidden;
background: #2b2b2b;
}
.info {
display: grid;
gap: 4px;
}
.title {
font-size: 20px;
font-weight: 600;
}
.artist {
color: #bdbdbd;
}
.time {
display: flex;
align-items: center;
gap: 8px;
font-size: 12px;
font-variant-numeric: tabular-nums;
}
.seek {
flex: 1;
}
/* The seek bar draws nothing by itself: give the track a line, the fill a colour and the thumb a dot. */
.seek::part(track) {
background: linear-gradient(#ffffff33, #ffffff33) center / 100% 4px no-repeat;
}
.seek::part(fill) {
height: 4px;
border-radius: 2px;
background: var(--accent);
}
.seek::part(thumb) {
width: 12px;
height: 12px;
margin-left: -6px;
border-radius: 50%;
background: #ffffff;
}
.controls {
display: flex;
justify-content: center;
gap: 12px;
}
.controls ::part(button) {
width: 48px;
height: 48px;
border: none;
border-radius: 50%;
background: #2b2b2b;
color: inherit;
font-size: 18px;
cursor: pointer;
}
.controls fb-play-button::part(button) {
background: var(--accent);
color: #000000;
}现在项目结构如下:
my-theme/
├── node_modules/
├── src/
│ ├── main.js
│ └── style.css
├── index.html
├── package-lock.json
├── package.json
└── vite.config.js启动开发服务器
在终端运行:
npm run devVite 打印出页面的地址:
VITE v8.3.1 ready in 144 ms
➜ Local: http://localhost:5173/让它一直运行到教程结束。用浏览器打开 http://localhost:5173:封面位置是一块深灰色方块,下面写着 “Nothing playing”,进度条两端都是 0:00,再下面是三个按钮。普通浏览器背后没有 foobar2000,所以按钮在这里点了没反应。
在 foobar2000 里加载页面
在 foobar2000 中打开 File → Preferences → Display → WebView2 UI 下的 开发者 页,然后:
- 勾选
使用开发服务器(HMR 热重载)。 - 在
URL框里填http://localhost:5173。 - 按
应用。
foobar2000 窗口重新加载,显示我们的页面。播放一首曲目:封面、标题和艺术家出现,时间开始走,播放按钮变成暂停按钮。点进度条上的任意位置可以跳过去,上一首、下一首按钮也可以试试。
如果窗口显示的还是原来的主题,说明 foobar2000 没连上开发服务器,改为从 profile 加载了主题。确认终端里的 npm run dev 还在运行,然后重启 foobar2000。
改个颜色
让 foobar2000 和编辑器并排开着。把 src/style.css 开头的强调色改成你喜欢的颜色,例如:
:root {
--accent: #4cc2ff;
--accent: #ff8c42;
color-scheme: dark;
color: #f3f3f3;
background: #202020;
font-family: 'Segoe UI', system-ui, sans-serif;
}保存文件。播放按钮和进度条的填充段立刻换了颜色,页面没有重新加载。
显示播放状态
到目前为止,活都是元素自己干的。现在我们亲自调用 SDK:加一行文字显示 foobar2000 是否在播放,再加一个随机播放一首的按钮。
在 index.html 的 <main> 里、controls 那个 div 之后加入:
<div class="extra">
<span id="status">Not connected to foobar2000</span>
<button id="random" type="button">Random track</button>
</div>保存后整个页面会重新加载:Vite 能原地替换样式表,页面的 HTML 却换不了。按钮下方出现新的一行,暂时还没有样式。
在 src/style.css 末尾加上这些规则:
.extra {
display: flex;
align-items: center;
justify-content: space-between;
font-size: 12px;
color: #bdbdbd;
}
#random {
padding: 6px 12px;
border: 1px solid #ffffff33;
border-radius: 4px;
background: transparent;
color: inherit;
font: inherit;
cursor: pointer;
}再给 src/main.js 加上标出的几行。第一行导入 fb,也就是那些元素一直在用的 SDK 对象:
import fb from 'foo-webview-sdk';
import { registerComponents } from 'foo-webview-sdk/components';
import './style.css';
registerComponents();
const status = document.querySelector('#status');
function showState(state) {
status.textContent = state === 'playing' ? 'Playing' : state === 'paused' ? 'Paused' : 'Stopped';
}
fb.player.getState().then((res) => {
if (res.success) showState(res.state);
});
fb.on('playback:stateChanged', ({ state }) => showState(state));
document.querySelector('#random').addEventListener('click', () => {
fb.player.random();
});fb.player.getState() 向 foobar2000 查一次当前状态;fb.on('playback:stateChanged', ...) 在状态每次变化时再调用我们的函数。SDK 的每个调用都 resolve 一个结果对象,其中 success 字段表示调用是否成功;在 foobar2000 之外它是 false,所以普通浏览器里这一行始终显示 “Not connected to foobar2000”。
保存。这一行现在显示 “Playing”。在 foobar2000 里暂停,它变成 “Paused”;按 Random track,foobar2000 随机跳到一首曲目。
安装主题
开发服务器只在终端开着时运行。要长期使用这个主题,就得构建它,再作为模板复制进 foobar2000 的 profile。
先让 foobar2000 切回来:在 开发者 页取消勾选 使用开发服务器(HMR 热重载),按 应用。窗口显示原来的主题。
在终端按 Ctrl+C 停掉开发服务器,然后构建:
npm run buildVite 把做好的主题写进项目的 dist 文件夹:一个 index.html 和一个 assets 文件夹。
在 foobar2000 中回到 WebView2 UI 页本身(File → Preferences → Display → WebView2 UI):
- 按
管理...,选新建...。名称填my-theme并确认。foobar2000 创建模板文件夹,放入一个占位用的index.html,并在模板框里选中my-theme。 - 按
打开...,在文件资源管理器里打开模板文件夹。 - 把项目
dist文件夹里的全部内容复制进模板文件夹,覆盖原有的index.html。 - 按
应用。
窗口重新加载,显示我们的主题,这次是从模板文件夹加载的。重启 foobar2000 后它依然在,也不再需要开发服务器。
接下来
现在我们有了一个能用的主题,也有了实时修改它的办法。接下来可以看:
- 命名空间:SDK 能做的全部事情,从播放列表、媒体库到窗口和托盘图标。
- 组件:其他
fb-*元素,比如播放列表视图和音量控制。 - 把页面放进面板:怎样把页面作为面板放进 Default UI 或 Columns UI。
- 结果与失败:SDK 调用 resolve 的结果对象是什么样的。
- 偏好设置:这一路上略过的其他设置。
想从做好的代码重新开始,就从仓库复制 examples/starter。