修复微软edge-tts报403的bug,优化UI界面

This commit is contained in:
Cx330 committed 2026-10-13 18:13:47 +08:00
1 parent e39f9025ce
commit b081bba33c
6 files changed
+1242 -1170

No files matched your search

+445 -412
View File
@@ -1,452 +1,485 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>广播系统</title>
<link rel="stylesheet" href="/static/style.css">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<meta name="theme-color" content="#F0F1EE">
<title>喇叭</title>
<link rel="stylesheet" href="/static/style.css?v=2">
</head>
<body>
<div class="container">
<div class="header">
<h1>📢 Web广播系统</h1>
<button class="theme-toggle" onclick="toggleTheme()" title="切换主题">
<span id="theme-icon">🌙</span>
</button>
</div>
<div class="main-content">
<div class="form-group">
<textarea
id="text"
placeholder="输入广播内容..."
maxlength="500"
></textarea>
<span class="char-count">0/500</span>
</div>
<div class="shell">
<div class="form-group">
<label for="voice">选择声音</label>
<select id="voice">
{% for k,v in voices.items() %}
<option>{{ k }}</option>
{% endfor %}
</select>
</div>
<header class="top">
<h1 class="wordmark">喇叭</h1>
<div class="top-actions">
<span class="live-dot" aria-hidden="true"></span>
<button class="ghost-btn" id="theme-btn" type="button">夜间</button>
</div>
</header>
<div class="form-group">
<label class="checkbox-label">
<input type="checkbox" id="loop" onchange="toggleInterval()">
<span>循环播放</span>
</label>
</div>
<div class="form-group interval-group" id="interval-group" style="display: none;">
<label for="interval">循环间隔时间</label>
<div class="interval-input">
<input
type="range"
id="interval"
min="1"
max="60"
value="5"
>
<span id="interval-value">5</span>
<span class="unit">秒</span>
</div>
</div>
<div class="button-group">
<button class="btn btn-primary" onclick="speak()">
<span class="btn-icon">▶️</span>
开始广播
</button>
<button class="btn btn-danger" onclick="stopPlay()">
<span class="btn-icon">⏹️</span>
停止播放
</button>
</div>
</div>
<div class="files-container">
<div class="files-header">
<button class="btn btn-secondary btn-small refresh-btn" onclick="loadFiles()" title="刷新列表">
🔄刷新列表
</button>
<h3>语音文件列表</h3>
</div>
<div id="files-list" class="files-list">
<div class="empty-state">
<span class="empty-icon">📭</span>
<p>暂无语音文件</p>
</div>
</div>
</div>
<div class="logs-container">
<div class="logs-header">
<button class="btn btn-secondary btn-small clear-btn" onclick="clearLogs()" title="清空日志">
🗑️清空日志
</button>
<h3>运行日志</h3>
</div>
<div id="logs" class="logs-content">
<p class="log-info">系统已就绪,等待操作...</p>
</div>
</div>
<main class="composer">
<label class="sr-only" for="text">广播内容</label>
<div class="slab">
<textarea id="text" rows="4" maxlength="500"
placeholder="输入今天要吆喝的内容…"></textarea>
<div class="slab-foot">
<span class="meta" id="count">0 / 500</span>
<label class="voice-pick">
<span class="sr-only">选择声音</span>
<select id="voice">
{% for k,v in voices.items() %}<option>{{ k }}</option>{% endfor %}
</select>
</label>
</div>
</div>
<script>
const themeKey = 'broadcast-theme';
const darkIcon = '🌙';
const lightIcon = '☀️';
<div class="loop-row">
<label class="switch">
<input type="checkbox" id="loop">
<span class="switch-track" aria-hidden="true"></span>
<span class="switch-text">循环播报</span>
</label>
<div class="interval" id="interval-wrap" hidden>
<label class="sr-only" for="interval">循环间隔秒数</label>
<input type="range" id="interval" min="1" max="60" value="5">
<output class="meta" id="interval-out" for="interval">每 5 秒</output>
</div>
</div>
function initTheme() {
const savedTheme = localStorage.getItem(themeKey);
if (savedTheme === 'light' || (!savedTheme && window.matchMedia('(prefers-color-scheme: light)').matches)) {
setLightTheme();
} else {
setDarkTheme();
}
}
<button class="broadcast" id="go" type="button">开始广播</button>
</main>
function setDarkTheme() {
document.body.classList.add('dark-theme');
document.body.classList.remove('light-theme');
document.getElementById('theme-icon').textContent = darkIcon;
localStorage.setItem(themeKey, 'dark');
}
<section class="drawer" id="history">
<button class="drawer-head" type="button" aria-expanded="false" aria-controls="history-body">
<span class="drawer-title">之前播过的</span>
<span class="drawer-meta" id="history-count">0 条</span>
<span class="caret" aria-hidden="true"></span>
</button>
<div class="drawer-body" id="history-body">
<div class="rows" id="files-list"></div>
</div>
</section>
function setLightTheme() {
document.body.classList.add('light-theme');
document.body.classList.remove('dark-theme');
document.getElementById('theme-icon').textContent = lightIcon;
localStorage.setItem(themeKey, 'light');
}
<section class="drawer" id="status">
<button class="drawer-head" type="button" aria-expanded="false" aria-controls="status-body">
<span class="drawer-title" id="status-text">待机</span>
<span class="drawer-meta" id="status-meta"></span>
<span class="caret" aria-hidden="true"></span>
</button>
<div class="drawer-body" id="status-body">
<div class="logs" id="logs"></div>
<button class="ghost-btn small" id="clear-logs" type="button">清空日志</button>
</div>
</section>
function toggleTheme() {
if (document.body.classList.contains('dark-theme')) {
setLightTheme();
} else {
setDarkTheme();
}
}
</div>
function toggleInterval() {
const intervalGroup = document.getElementById('interval-group');
const loopChecked = document.getElementById('loop').checked;
intervalGroup.style.display = loopChecked ? 'block' : 'none';
}
<script>
(function () {
'use strict';
function addLog(message, type = 'info') {
const logsContainer = document.getElementById('logs');
const logEntry = document.createElement('p');
logEntry.className = `log-${type}`;
const timestamp = new Date().toLocaleTimeString('zh-CN');
logEntry.innerHTML = `<span class="log-time">[${timestamp}]</span> ${message}`;
logsContainer.appendChild(logEntry);
logsContainer.scrollTop = logsContainer.scrollHeight;
}
var $ = function (id) { return document.getElementById(id); };
function clearLogs() {
const logsContainer = document.getElementById('logs');
logsContainer.innerHTML = '<p class="log-info">系统已就绪,等待操作...</p>';
}
var THEME_KEY = 'broadcast-theme';
var themeMeta = document.querySelector('meta[name="theme-color"]');
var COLORS = { light: '#F0F1EE', dark: '#121614' };
function updateCharCount() {
const textarea = document.getElementById('text');
const count = textarea.value.length;
document.querySelector('.char-count').textContent = `${count}/500`;
}
var elText = $('text'), elVoice = $('voice'), elLoop = $('loop'),
elInterval = $('interval'), elIntervalWrap = $('interval-wrap'),
elIntervalOut = $('interval-out'), elCount = $('count'),
elGo = $('go'), elFiles = $('files-list'), elHistoryCount = $('history-count'),
elStatusText = $('status-text'), elStatusMeta = $('status-meta'),
elLogs = $('logs');
async function speak() {
const text = document.getElementById('text').value.trim();
const voice = document.getElementById('voice').value;
const loop = document.getElementById('loop').checked;
const interval = document.getElementById('interval').value;
/* ---------- 主题 ---------- */
if (!text) {
addLog('请输入广播内容', 'error');
return;
}
function applyTheme(name) {
var light = name === 'light';
document.documentElement.classList.toggle('theme-light', light);
$('theme-btn').textContent = light ? '夜间' : '日间';
if (themeMeta) themeMeta.setAttribute('content', light ? COLORS.light : COLORS.dark);
try { localStorage.setItem(THEME_KEY, name); } catch (e) {}
}
addLog('开始处理广播请求...', 'info');
function initTheme() {
var saved = null;
try { saved = localStorage.getItem(THEME_KEY); } catch (e) {}
if (saved !== 'light' && saved !== 'dark') {
saved = window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
}
applyTheme(saved);
}
const formData = new FormData();
formData.append('text', text);
formData.append('voice', voice);
formData.append('loop', loop);
formData.append('interval', interval);
$('theme-btn').addEventListener('click', function () {
applyTheme(document.documentElement.classList.contains('theme-light') ? 'dark' : 'light');
});
addLog(`正在生成语音,使用声音: ${voice}`, 'info');
if (loop) {
addLog(`循环模式已启用,间隔时间: ${interval}秒`, 'info');
}
/* ---------- 日志 ---------- */
try {
addLog('正在调用语音合成服务...', 'info');
const response = await fetch('/speak', {
method: 'POST',
body: formData
});
const result = await response.json();
if (result.status === 'ok') {
addLog('语音合成成功,开始播放...', 'success');
loadFiles();
} else {
addLog(`播放失败: ${result.message || '未知错误'}`, 'error');
}
} catch (error) {
addLog(`请求失败: ${error.message}`, 'error');
}
}
function stamp() {
return new Date().toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' });
}
async function stopPlay() {
addLog('正在停止播放...', 'info');
try {
await fetch('/stop');
addLog('播放已停止', 'success');
} catch (error) {
addLog(`停止失败: ${error.message}`, 'error');
}
}
function log(message, kind) {
var line = document.createElement('div');
line.className = 'log' + (kind ? ' log-' + kind : '');
var time = document.createElement('span');
time.className = 'log-time';
time.textContent = stamp();
var body = document.createElement('span');
body.textContent = message;
line.appendChild(time);
line.appendChild(body);
elLogs.appendChild(line);
elLogs.scrollTop = elLogs.scrollHeight;
while (elLogs.childElementCount > 80) elLogs.removeChild(elLogs.firstChild);
}
async function loadFiles() {
try {
const response = await fetch('/files');
const result = await response.json();
renderFiles(result.files);
} catch (error) {
addLog(`加载文件列表失败: ${error.message}`, 'error');
}
}
$('clear-logs').addEventListener('click', function () { elLogs.innerHTML = ''; });
function renderFiles(files) {
const container = document.getElementById('files-list');
if (!files || files.length === 0) {
container.innerHTML = `
<div class="empty-state">
<span class="empty-icon">📭</span>
<p>暂无语音文件</p>
</div>
`;
return;
}
/* ---------- 抽屉 ---------- */
container.innerHTML = files.map(file => `
<div class="file-item ${file.is_favorite ? 'file-item-favorite' : ''}">
<div class="file-info">
<div class="file-icon">🎵</div>
<div class="file-details">
<div class="file-name-container">
<input
type="text"
class="file-name-input"
value="${escapeHtml(file.display_name)}"
data-filename="${file.filename}"
onblur="saveFileName(this)"
onkeyup="handleFileNameKeyup(event, this)"
/>
</div>
<div class="file-meta">
<span class="file-voice">${file.voice || '未知声音'}</span>
<span class="file-date">${formatDate(file.created_at)}</span>
</div>
</div>
</div>
<div class="file-actions">
<button class="file-btn file-btn-favorite ${file.is_favorite ? 'active' : ''}" onclick="toggleFavorite('${file.filename}')" title="${file.is_favorite ? '取消收藏' : '收藏'}">
${file.is_favorite ? '⭐' : '☆'}
</button>
<button class="file-btn file-btn-play" onclick="playFile('${file.filename}')" title="播放">
▶️
</button>
<button class="file-btn file-btn-loop" onclick="playFileLoop('${file.filename}')" title="循环播放">
🔄
</button>
<button class="file-btn file-btn-delete" onclick="deleteFile('${file.filename}', '${escapeHtml(file.display_name)}')" title="删除">
🗑️
</button>
</div>
</div>
`).join('');
}
function drawer(id) {
var box = $(id);
var head = box.querySelector('.drawer-head');
var body = box.querySelector('.drawer-body');
head.addEventListener('click', function () {
var open = body.classList.toggle('open');
head.setAttribute('aria-expanded', open ? 'true' : 'false');
});
return {
open: function () { body.classList.add('open'); head.setAttribute('aria-expanded', 'true'); },
body: body
};
}
function escapeHtml(str) {
return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
var history = drawer('history');
var status = drawer('status');
function formatDate(timestamp) {
const date = new Date(timestamp * 1000);
return date.toLocaleString('zh-CN', {
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
});
}
/* ---------- 广播状态 ---------- */
async function playFile(filename) {
const loop = document.getElementById('loop').checked;
const interval = document.getElementById('interval').value;
addLog(`正在播放已保存的语音...`, 'info');
const formData = new FormData();
formData.append('filename', filename);
formData.append('loop', loop);
formData.append('interval', interval);
try {
const response = await fetch('/play-file', {
method: 'POST',
body: formData
});
const result = await response.json();
if (result.status === 'ok') {
addLog(result.message, 'success');
} else {
addLog(`播放失败: ${result.message}`, 'error');
}
} catch (error) {
addLog(`播放失败: ${error.message}`, 'error');
}
}
var live = false; // 喇叭是否正在响
var busy = false; // 是否正在合成
async function playFileLoop(filename) {
const interval = document.getElementById('interval').value;
addLog(`正在循环播放已保存的语音...`, 'info');
const formData = new FormData();
formData.append('filename', filename);
formData.append('loop', true);
formData.append('interval', interval);
try {
const response = await fetch('/play-file', {
method: 'POST',
body: formData
});
const result = await response.json();
if (result.status === 'ok') {
addLog(`${result.message},间隔时间: ${interval}秒`, 'success');
} else {
addLog(`播放失败: ${result.message}`, 'error');
}
} catch (error) {
addLog(`播放失败: ${error.message}`, 'error');
}
}
function setLive(on, info) {
live = on;
document.body.classList.toggle('is-live', on);
elGo.textContent = busy ? '正在合成…' : (on ? '停止广播' : '开始广播');
elGo.disabled = busy;
elGo.classList.toggle('is-stop', on && !busy);
async function saveFileName(input) {
const filename = input.dataset.filename;
const newName = input.value.trim();
if (!newName) {
input.value = input.oldValue || filename;
return;
}
if (newName === input.oldValue) return;
try {
const formData = new FormData();
formData.append('filename', filename);
formData.append('new_name', newName);
const response = await fetch('/rename-file', {
method: 'POST',
body: formData
});
const result = await response.json();
if (result.status === 'ok') {
addLog(`文件已重命名为: ${newName}`, 'success');
} else {
addLog(`重命名失败: ${result.message}`, 'error');
input.value = input.oldValue;
}
} catch (error) {
addLog(`重命名失败: ${error.message}`, 'error');
input.value = input.oldValue;
}
}
if (on) {
elStatusText.textContent = '正在播报';
elStatusMeta.textContent = describe(info);
} else if (!busy) {
elStatusText.textContent = '待机';
elStatusMeta.textContent = '';
}
}
function handleFileNameKeyup(event, input) {
if (!input.oldValue) {
input.oldValue = input.value;
}
if (event.key === 'Enter') {
input.blur();
} else if (event.key === 'Escape') {
input.value = input.oldValue;
input.blur();
}
}
function describe(info) {
if (!info || !info.playing) return '';
var who = info.display_name ? ',' + info.display_name : '';
return info.mode === 'loop'
? '循环播报,每 ' + info.interval + ' 秒一次' + who
: '播一遍' + who;
}
async function toggleFavorite(filename) {
try {
const formData = new FormData();
formData.append('filename', filename);
const response = await fetch('/toggle-favorite', {
method: 'POST',
body: formData
});
const result = await response.json();
if (result.status === 'ok') {
addLog(result.message, 'success');
loadFiles();
} else {
addLog(`操作失败: ${result.message}`, 'error');
}
} catch (error) {
addLog(`操作失败: ${error.message}`, 'error');
}
}
function setBusy(on) {
busy = on;
elGo.disabled = on;
elGo.textContent = on ? '正在合成…' : (live ? '停止广播' : '开始广播');
elGo.classList.toggle('is-stop', !on && live);
elGo.classList.toggle('is-busy', on);
if (on) { elStatusText.textContent = '正在合成'; elStatusMeta.textContent = '等几秒'; }
}
async function deleteFile(filename, displayName) {
if (!confirm(`确定要删除文件 "${displayName}" 吗?`)) {
return;
}
try {
const formData = new FormData();
formData.append('filename', filename);
const response = await fetch('/delete-file', {
method: 'POST',
body: formData
});
const result = await response.json();
if (result.status === 'ok') {
addLog(`文件已删除: ${displayName}`, 'success');
loadFiles();
} else {
addLog(`删除失败: ${result.message}`, 'error');
}
} catch (error) {
addLog(`删除失败: ${error.message}`, 'error');
}
}
async function poll() {
try {
var r = await fetch('/status');
var info = await r.json();
if (info.playing !== live && !busy) setLive(info.playing, info);
else if (info.playing) elStatusMeta.textContent = describe(info);
} catch (e) { /* 网络抖动就跳过这一轮 */ }
}
document.getElementById('text').addEventListener('input', updateCharCount);
document.getElementById('interval').addEventListener('input', function() {
document.getElementById('interval-value').textContent = this.value;
});
/* ---------- 内容与音色 ---------- */
initTheme();
function autoGrow() {
elText.style.height = 'auto';
elText.style.height = Math.min(elText.scrollHeight, 420) + 'px';
elCount.textContent = elText.value.length + ' / 500';
}
elText.addEventListener('input', autoGrow);
elLoop.addEventListener('change', function () {
elIntervalWrap.hidden = !elLoop.checked;
});
elInterval.addEventListener('input', function () {
elIntervalOut.textContent = '每 ' + elInterval.value + ' 秒';
});
/* ---------- 广播 / 停止 ---------- */
elGo.addEventListener('click', function () {
if (live) stop(); else speak();
});
async function speak() {
var text = elText.value.trim();
if (!text) { flash('先写一句要播的话'); elText.focus(); return; }
setBusy(true);
log('开始合成,音色:' + elVoice.value);
if (elLoop.checked) log('循环播报,每 ' + elInterval.value + ' 秒一次');
var form = new FormData();
form.append('text', text);
form.append('voice', elVoice.value);
form.append('loop', elLoop.checked);
form.append('interval', elInterval.value);
try {
var r = await fetch('/speak', { method: 'POST', body: form });
var result = await r.json();
setBusy(false);
if (result.status === 'ok') {
log('已开始广播', 'ok');
loadFiles();
</script>
poll();
} else {
fail(result.message || '合成失败');
}
} catch (e) {
setBusy(false);
fail(e.message);
}
}
async function stop() {
try {
await fetch('/stop');
log('已停止播报');
setLive(false);
} catch (e) { fail(e.message); }
}
function fail(message) {
log('没成功:' + message, 'err');
elStatusText.textContent = '上次没成功';
elStatusMeta.textContent = message;
status.open();
}
var flashTimer = null;
function flash(message) {
log(message, 'err');
elText.classList.add('nudge');
clearTimeout(flashTimer);
flashTimer = setTimeout(function () { elText.classList.remove('nudge'); }, 600);
}
/* ---------- 语音列表 ---------- */
function escapeHtml(str) {
return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;')
.replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
function when(ts) {
var d = new Date(ts * 1000);
var now = new Date();
var sameDay = d.toDateString() === now.toDateString();
var hm = d.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' });
return sameDay ? hm : (d.getMonth() + 1) + '-' + d.getDate() + ' ' + hm;
}
async function loadFiles() {
try {
var r = await fetch('/files');
var result = await r.json();
renderFiles(result.files || []);
} catch (e) {
log('读不到语音列表:' + e.message, 'err');
}
}
function renderFiles(files) {
elHistoryCount.textContent = files.length + ' 条';
elFiles.innerHTML = '';
if (!files.length) {
var empty = document.createElement('p');
empty.className = 'empty';
empty.textContent = '还没有播过的内容。上面写一句,点「开始广播」就有了。';
elFiles.appendChild(empty);
return;
}
files.forEach(function (file) { elFiles.appendChild(fileRow(file)); });
}
function fileRow(file) {
var row = document.createElement('div');
row.className = 'row' + (file.is_favorite ? ' is-fav' : '');
row.dataset.filename = file.filename;
var name = document.createElement('input');
name.className = 'row-name';
name.type = 'text';
name.value = file.display_name;
name.dataset.filename = file.filename;
name.dataset.old = file.display_name;
name.setAttribute('aria-label', '这一条的名字,可以直接改');
var meta = document.createElement('div');
meta.className = 'row-meta';
var voice = document.createElement('span');
voice.textContent = file.voice || '未知声音';
var time = document.createElement('span');
time.textContent = when(file.created_at);
meta.appendChild(voice);
meta.appendChild(time);
var actions = document.createElement('div');
actions.className = 'row-actions';
actions.appendChild(action('播一遍', 'play'));
actions.appendChild(action('循环', 'loop'));
actions.appendChild(action('删除', 'del'));
actions.appendChild(favButton(file.is_favorite));
row.appendChild(name);
row.appendChild(meta);
row.appendChild(actions);
name.addEventListener('blur', function () { rename(name); });
name.addEventListener('keydown', function (e) {
if (e.key === 'Enter') name.blur();
if (e.key === 'Escape') { name.value = name.dataset.old; name.blur(); }
});
actions.querySelector('[data-act="play"]').addEventListener('click', function () { play(file.filename, false); });
actions.querySelector('[data-act="loop"]').addEventListener('click', function () { play(file.filename, true); });
actions.querySelector('.row-fav').addEventListener('click', function () { favorite(file.filename); });
actions.querySelector('[data-act="del"]').addEventListener('click', function () { confirmDelete(row, file); });
return row;
}
function action(label, act) {
var b = document.createElement('button');
b.type = 'button';
b.className = 'act';
b.dataset.act = act;
b.textContent = label;
return b;
}
function favButton(isFav) {
var b = action(isFav ? '已收藏' : '收藏', 'fav');
b.classList.add('row-fav');
if (isFav) b.classList.add('on');
return b;
}
async function play(filename, loop) {
var form = new FormData();
form.append('filename', filename);
form.append('loop', loop);
form.append('interval', elInterval.value);
try {
var r = await fetch('/play-file', { method: 'POST', body: form });
var result = await r.json();
if (result.status === 'ok') {
log(result.message);
poll();
} else {
fail(result.message || '播不了这一条');
}
} catch (e) { fail(e.message); }
}
async function rename(input) {
var next = input.value.trim();
if (!next || next === input.dataset.old) { input.value = input.dataset.old; return; }
var form = new FormData();
form.append('filename', input.dataset.filename);
form.append('new_name', next);
try {
var r = await fetch('/rename-file', { method: 'POST', body: form });
var result = await r.json();
if (result.status === 'ok') { input.dataset.old = next; log('改名成功'); }
else { input.value = input.dataset.old; fail(result.message); }
} catch (e) { input.value = input.dataset.old; fail(e.message); }
}
async function favorite(filename) {
var form = new FormData();
form.append('filename', filename);
try {
var r = await fetch('/toggle-favorite', { method: 'POST', body: form });
var result = await r.json();
if (result.status === 'ok') { log(result.message); loadFiles(); }
else { fail(result.message); }
} catch (e) { fail(e.message); }
}
function confirmDelete(row, file) {
var actions = row.querySelector('.row-actions');
if (row.dataset.confirming) return;
row.dataset.confirming = '1';
var kept = actions.innerHTML;
actions.innerHTML = '';
var ask = document.createElement('span');
ask.className = 'confirm-text';
ask.textContent = '删掉这条?';
var yes = action('删除', 'yes');
yes.classList.add('danger');
var no = action('取消', 'no');
yes.addEventListener('click', function () { remove(file); });
no.addEventListener('click', function () {
delete row.dataset.confirming;
actions.innerHTML = kept;
rebind(actions, row, file);
});
actions.appendChild(ask);
actions.appendChild(yes);
actions.appendChild(no);
yes.focus();
}
function rebind(actions, row, file) {
actions.querySelector('[data-act="play"]').addEventListener('click', function () { play(file.filename, false); });
actions.querySelector('[data-act="loop"]').addEventListener('click', function () { play(file.filename, true); });
actions.querySelector('.row-fav').addEventListener('click', function () { favorite(file.filename); });
actions.querySelector('[data-act="del"]').addEventListener('click', function () { confirmDelete(row, file); });
}
async function remove(file) {
var form = new FormData();
form.append('filename', file.filename);
try {
var r = await fetch('/delete-file', { method: 'POST', body: form });
var result = await r.json();
if (result.status === 'ok') { log('已删除'); loadFiles(); poll(); }
else { fail(result.message); }
} catch (e) { fail(e.message); }
}
/* ---------- 启动 ---------- */
initTheme();
autoGrow();
loadFiles();
poll();
setInterval(poll, 3000);
log('就绪');
})();
</script>
</body>
</html>
</html>