-
- - 0/500 -
+
-
- - -
+
+

喇叭

+
+ + +
+
-
- -
- - - -
- - -
-
- -
-
- -

语音文件列表

-
-
-
- 📭 -

暂无语音文件

-
-
-
- -
-
- -

运行日志

-
-
-

系统已就绪,等待操作...

-
-
+
+ +
+ +
+ 0 / 500 + +
- + 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, '&').replace(//g, '>').replace(/"/g, '"'); + } + + 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('就绪'); +})(); + - \ No newline at end of file +