Update local plugin changes
This commit is contained in:
@@ -1,8 +1,21 @@
|
||||
// tts-panel.js
|
||||
/**
|
||||
* TTS 播放器面板 - 极简胶囊版 v2
|
||||
* 黑白灰配色,舒缓动画
|
||||
* TTS 播放器面板 - 极简胶囊版 v4
|
||||
* 新增:自动朗读快捷开关,支持双向同步
|
||||
*/
|
||||
|
||||
import { registerToToolbar, removeFromToolbar } from '../../core/message-toolbar.js';
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// 常量
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
const INITIAL_RENDER_LIMIT = 1;
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// 状态
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
let stylesInjected = false;
|
||||
const panelMap = new Map();
|
||||
const pendingCallbacks = new Map();
|
||||
@@ -28,9 +41,9 @@ export function clearPanelConfigHandlers() {
|
||||
clearQueueFn = null;
|
||||
}
|
||||
|
||||
// ============ 工具函数 ============
|
||||
|
||||
// ============ 样式 ============
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// 样式
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
function injectStyles() {
|
||||
if (stylesInjected) return;
|
||||
@@ -40,7 +53,7 @@ function injectStyles() {
|
||||
═══════════════════════════════════════════════════════════════ */
|
||||
|
||||
.xb-tts-panel {
|
||||
--h: 30px;
|
||||
--h: 34px;
|
||||
--bg: rgba(0, 0, 0, 0.55);
|
||||
--bg-hover: rgba(0, 0, 0, 0.7);
|
||||
--border: rgba(255, 255, 255, 0.08);
|
||||
@@ -48,13 +61,13 @@ function injectStyles() {
|
||||
--text: rgba(255, 255, 255, 0.85);
|
||||
--text-sub: rgba(255, 255, 255, 0.45);
|
||||
--text-dim: rgba(255, 255, 255, 0.25);
|
||||
--success: rgba(255, 255, 255, 0.9);
|
||||
--success: rgba(62, 207, 142, 0.9);
|
||||
--success-soft: rgba(62, 207, 142, 0.12);
|
||||
--error: rgba(239, 68, 68, 0.8);
|
||||
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
margin: 8px 0;
|
||||
z-index: 10;
|
||||
user-select: none;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
@@ -70,7 +83,7 @@ function injectStyles() {
|
||||
height: var(--h);
|
||||
background: var(--bg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 15px;
|
||||
border-radius: 17px;
|
||||
padding: 0 3px;
|
||||
backdrop-filter: blur(16px);
|
||||
-webkit-backdrop-filter: blur(16px);
|
||||
@@ -84,6 +97,14 @@ function injectStyles() {
|
||||
border-color: var(--border-active);
|
||||
}
|
||||
|
||||
/* 自动朗读开启时的边框提示 */
|
||||
.xb-tts-panel[data-auto="true"] .xb-tts-capsule {
|
||||
border-color: rgba(62, 207, 142, 0.25);
|
||||
}
|
||||
.xb-tts-panel[data-auto="true"]:hover .xb-tts-capsule {
|
||||
border-color: rgba(62, 207, 142, 0.4);
|
||||
}
|
||||
|
||||
/* 状态边框 */
|
||||
.xb-tts-panel[data-status="playing"] .xb-tts-capsule {
|
||||
border-color: rgba(255, 255, 255, 0.25);
|
||||
@@ -97,8 +118,8 @@ function injectStyles() {
|
||||
═══════════════════════════════════════════════════════════════ */
|
||||
|
||||
.xb-tts-btn {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -107,9 +128,10 @@ function injectStyles() {
|
||||
color: var(--text);
|
||||
cursor: pointer;
|
||||
border-radius: 50%;
|
||||
font-size: 10px;
|
||||
font-size: 11px;
|
||||
transition: all 0.25s ease;
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.xb-tts-btn:hover {
|
||||
@@ -120,12 +142,26 @@ function injectStyles() {
|
||||
transform: scale(0.92);
|
||||
}
|
||||
|
||||
/* 播放按钮 */
|
||||
.xb-tts-btn.play-btn {
|
||||
font-size: 11px;
|
||||
/* 播放按钮的自动朗读指示点 */
|
||||
.xb-tts-auto-dot {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
background: var(--success);
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 6px rgba(62, 207, 142, 0.6);
|
||||
opacity: 0;
|
||||
transform: scale(0);
|
||||
transition: all 0.25s ease;
|
||||
}
|
||||
.xb-tts-panel[data-auto="true"] .xb-tts-auto-dot {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
/* 停止按钮 - 正方形图标 */
|
||||
/* 停止按钮 */
|
||||
.xb-tts-btn.stop-btn {
|
||||
color: var(--text-sub);
|
||||
font-size: 8px;
|
||||
@@ -137,8 +173,8 @@ function injectStyles() {
|
||||
|
||||
/* 展开按钮 */
|
||||
.xb-tts-btn.expand-btn {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
font-size: 8px;
|
||||
color: var(--text-dim);
|
||||
opacity: 0.6;
|
||||
@@ -206,7 +242,7 @@ function injectStyles() {
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════
|
||||
波形动画 - 舒缓版
|
||||
波形动画
|
||||
═══════════════════════════════════════════════════════════════ */
|
||||
|
||||
.xb-tts-wave {
|
||||
@@ -383,19 +419,71 @@ function injectStyles() {
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
/* 工具栏 */
|
||||
/* ═══════════════════════════════════════════════════════════════
|
||||
工具栏(包含自动朗读开关)
|
||||
═══════════════════════════════════════════════════════════════ */
|
||||
|
||||
.xb-tts-tools {
|
||||
margin-top: 8px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid var(--border);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.xb-tts-usage {
|
||||
font-size: 10px;
|
||||
color: var(--text-dim);
|
||||
flex-shrink: 0;
|
||||
min-width: 32px;
|
||||
}
|
||||
|
||||
/* 自动朗读开关 - flex:1 填满剩余空间 */
|
||||
.xb-tts-auto-toggle {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 10px;
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
.xb-tts-auto-toggle:hover {
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
border-color: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
.xb-tts-auto-toggle.on {
|
||||
background: rgba(62, 207, 142, 0.08);
|
||||
border-color: rgba(62, 207, 142, 0.25);
|
||||
}
|
||||
|
||||
.xb-tts-auto-indicator {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
transition: all 0.25s ease;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.xb-tts-auto-toggle.on .xb-tts-auto-indicator {
|
||||
background: var(--success);
|
||||
box-shadow: 0 0 6px rgba(62, 207, 142, 0.5);
|
||||
}
|
||||
|
||||
.xb-tts-auto-text {
|
||||
font-size: 11px;
|
||||
color: var(--text-sub);
|
||||
transition: color 0.2s;
|
||||
}
|
||||
.xb-tts-auto-toggle:hover .xb-tts-auto-text {
|
||||
color: var(--text);
|
||||
}
|
||||
.xb-tts-auto-toggle.on .xb-tts-auto-text {
|
||||
color: rgba(62, 207, 142, 0.9);
|
||||
}
|
||||
|
||||
.xb-tts-icon-btn {
|
||||
@@ -405,6 +493,7 @@ function injectStyles() {
|
||||
padding: 4px 6px;
|
||||
border-radius: 4px;
|
||||
transition: all 0.2s;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.xb-tts-icon-btn:hover {
|
||||
color: var(--text);
|
||||
@@ -448,24 +537,31 @@ function injectStyles() {
|
||||
stylesInjected = true;
|
||||
}
|
||||
|
||||
// ============ 面板创建 ============
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// 面板创建
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
function createPanel(messageId) {
|
||||
const config = getConfigFn?.() || {};
|
||||
const currentSpeed = config?.volc?.speechRate || 1.0;
|
||||
const isAutoSpeak = config?.autoSpeak !== false;
|
||||
|
||||
const div = document.createElement('div');
|
||||
div.className = 'xb-tts-panel';
|
||||
div.dataset.messageId = messageId;
|
||||
div.dataset.status = 'idle';
|
||||
div.dataset.hasQueue = 'false';
|
||||
div.dataset.auto = isAutoSpeak ? 'true' : 'false';
|
||||
|
||||
// Template-only UI markup.
|
||||
// Template-only UI markup built locally.
|
||||
// eslint-disable-next-line no-unsanitized/property
|
||||
div.innerHTML = `
|
||||
<div class="xb-tts-capsule">
|
||||
<div class="xb-tts-loading"></div>
|
||||
<button class="xb-tts-btn play-btn" title="播放">▶</button>
|
||||
<button class="xb-tts-btn play-btn" title="播放">
|
||||
▶
|
||||
<span class="xb-tts-auto-dot"></span>
|
||||
</button>
|
||||
|
||||
<div class="xb-tts-info">
|
||||
<div class="xb-tts-wave">
|
||||
@@ -500,7 +596,11 @@ function createPanel(messageId) {
|
||||
<span class="xb-tts-val speed-val">${currentSpeed.toFixed(1)}x</span>
|
||||
</div>
|
||||
<div class="xb-tts-tools">
|
||||
<span class="xb-tts-usage">--</span>
|
||||
<span class="xb-tts-usage">-字</span>
|
||||
<div class="xb-tts-auto-toggle${isAutoSpeak ? ' on' : ''}" title="AI回复后自动朗读">
|
||||
<span class="xb-tts-auto-indicator"></span>
|
||||
<span class="xb-tts-auto-text">自动朗读</span>
|
||||
</div>
|
||||
<span class="xb-tts-icon-btn settings-btn" title="TTS 设置">⚙</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -514,40 +614,91 @@ function buildVoiceOptions(select, config) {
|
||||
const current = config?.volc?.defaultSpeaker || '';
|
||||
|
||||
if (mySpeakers.length === 0) {
|
||||
// Template-only UI markup.
|
||||
// eslint-disable-next-line no-unsanitized/property
|
||||
select.innerHTML = '<option value="" disabled>暂无音色</option>';
|
||||
select.textContent = '';
|
||||
const opt = document.createElement('option');
|
||||
opt.value = '';
|
||||
opt.disabled = true;
|
||||
opt.textContent = '暂无音色';
|
||||
select.appendChild(opt);
|
||||
select.selectedIndex = -1;
|
||||
return;
|
||||
}
|
||||
|
||||
const isMyVoice = current && mySpeakers.some(s => s.value === current);
|
||||
|
||||
// UI options from config values only.
|
||||
// eslint-disable-next-line no-unsanitized/property
|
||||
select.innerHTML = mySpeakers.map(s => {
|
||||
const selected = isMyVoice && s.value === current ? ' selected' : '';
|
||||
return `<option value="${s.value}"${selected}>${s.name || s.value}</option>`;
|
||||
}).join('');
|
||||
select.textContent = '';
|
||||
mySpeakers.forEach((s) => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = s.value;
|
||||
opt.textContent = s.name || s.value;
|
||||
if (isMyVoice && s.value === current) opt.selected = true;
|
||||
select.appendChild(opt);
|
||||
});
|
||||
|
||||
if (!isMyVoice) {
|
||||
select.selectedIndex = -1;
|
||||
}
|
||||
}
|
||||
|
||||
function mountPanel(messageEl, messageId, onPlay) {
|
||||
if (panelMap.has(messageId)) return panelMap.get(messageId);
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// IntersectionObserver 管理
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
function setupObserver() {
|
||||
if (observer) return;
|
||||
|
||||
const nameBlock = messageEl.querySelector('.mes_block > .ch_name') ||
|
||||
messageEl.querySelector('.name_text')?.parentElement;
|
||||
if (!nameBlock) return null;
|
||||
observer = new IntersectionObserver((entries) => {
|
||||
const toMount = [];
|
||||
|
||||
for (const entry of entries) {
|
||||
if (!entry.isIntersecting) continue;
|
||||
|
||||
const el = entry.target;
|
||||
const mid = Number(el.getAttribute('mesid'));
|
||||
const cb = pendingCallbacks.get(mid);
|
||||
|
||||
if (cb) {
|
||||
toMount.push({ el, mid, cb });
|
||||
pendingCallbacks.delete(mid);
|
||||
observer.unobserve(el);
|
||||
}
|
||||
}
|
||||
|
||||
if (toMount.length > 0) {
|
||||
requestAnimationFrame(() => {
|
||||
for (const { el, mid, cb } of toMount) {
|
||||
mountPanel(el, mid, cb);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, {
|
||||
rootMargin: '300px',
|
||||
threshold: 0
|
||||
});
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// 面板挂载
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
function mountPanel(messageEl, messageId, onPlay) {
|
||||
// 已存在且有效
|
||||
if (panelMap.has(messageId)) {
|
||||
const existing = panelMap.get(messageId);
|
||||
if (existing.root?.isConnected) return existing;
|
||||
existing._cleanup?.();
|
||||
panelMap.delete(messageId);
|
||||
}
|
||||
|
||||
const panel = createPanel(messageId);
|
||||
if (nameBlock.nextSibling) {
|
||||
nameBlock.parentNode.insertBefore(panel, nameBlock.nextSibling);
|
||||
} else {
|
||||
nameBlock.parentNode.appendChild(panel);
|
||||
}
|
||||
|
||||
// 使用工具栏管理器注册
|
||||
const success = registerToToolbar(messageId, panel, {
|
||||
position: 'left',
|
||||
id: `tts-${messageId}`
|
||||
});
|
||||
|
||||
if (!success) return null;
|
||||
|
||||
const ui = {
|
||||
root: panel,
|
||||
@@ -560,8 +711,10 @@ function mountPanel(messageEl, messageId, onPlay) {
|
||||
speedSlider: panel.querySelector('.speed-slider'),
|
||||
speedVal: panel.querySelector('.speed-val'),
|
||||
usageText: panel.querySelector('.xb-tts-usage'),
|
||||
autoToggle: panel.querySelector('.xb-tts-auto-toggle'),
|
||||
};
|
||||
|
||||
// 事件绑定
|
||||
ui.playBtn.onclick = (e) => {
|
||||
e.stopPropagation();
|
||||
onPlay(messageId);
|
||||
@@ -577,6 +730,11 @@ function mountPanel(messageEl, messageId, onPlay) {
|
||||
panel.classList.toggle('expanded');
|
||||
if (panel.classList.contains('expanded')) {
|
||||
buildVoiceOptions(ui.voiceSelect, getConfigFn?.());
|
||||
// 同步当前语速
|
||||
const config = getConfigFn?.();
|
||||
const currentSpeed = config?.volc?.speechRate || 1.0;
|
||||
ui.speedSlider.value = currentSpeed;
|
||||
ui.speedVal.textContent = currentSpeed.toFixed(1) + 'x';
|
||||
}
|
||||
};
|
||||
|
||||
@@ -586,6 +744,22 @@ function mountPanel(messageEl, messageId, onPlay) {
|
||||
openSettingsFn?.();
|
||||
};
|
||||
|
||||
// 自动朗读开关
|
||||
ui.autoToggle.onclick = async (e) => {
|
||||
e.stopPropagation();
|
||||
const config = getConfigFn?.();
|
||||
if (!config) return;
|
||||
|
||||
const newValue = config.autoSpeak === false ? true : false;
|
||||
config.autoSpeak = newValue;
|
||||
|
||||
// 保存配置
|
||||
await saveConfigFn?.({ autoSpeak: newValue });
|
||||
|
||||
// 更新所有面板的自动朗读状态
|
||||
updateAutoSpeakAll();
|
||||
};
|
||||
|
||||
ui.voiceSelect.onchange = async (e) => {
|
||||
const config = getConfigFn?.();
|
||||
if (config?.volc) {
|
||||
@@ -597,11 +771,14 @@ function mountPanel(messageEl, messageId, onPlay) {
|
||||
ui.speedSlider.oninput = (e) => {
|
||||
ui.speedVal.textContent = Number(e.target.value).toFixed(1) + 'x';
|
||||
};
|
||||
|
||||
ui.speedSlider.onchange = async (e) => {
|
||||
const config = getConfigFn?.();
|
||||
if (config?.volc) {
|
||||
config.volc.speechRate = Number(e.target.value);
|
||||
await saveConfigFn?.({ volc: config.volc });
|
||||
// 同步所有面板的语速显示
|
||||
updateSpeedAll();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -614,59 +791,123 @@ function mountPanel(messageEl, messageId, onPlay) {
|
||||
|
||||
ui._cleanup = () => {
|
||||
document.removeEventListener('click', closeMenu);
|
||||
removeFromToolbar(messageId, panel);
|
||||
};
|
||||
|
||||
panelMap.set(messageId, ui);
|
||||
return ui;
|
||||
}
|
||||
|
||||
// ============ 对外接口 ============
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// 全局同步更新
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* 更新所有面板的自动朗读状态
|
||||
*/
|
||||
export function updateAutoSpeakAll() {
|
||||
const config = getConfigFn?.();
|
||||
const isAutoSpeak = config?.autoSpeak !== false;
|
||||
|
||||
panelMap.forEach((ui) => {
|
||||
if (!ui.root) return;
|
||||
|
||||
// 更新 data-auto 属性(控制播放按钮上的绿点)
|
||||
ui.root.dataset.auto = isAutoSpeak ? 'true' : 'false';
|
||||
|
||||
// 更新菜单内的开关状态
|
||||
if (ui.autoToggle) {
|
||||
ui.autoToggle.classList.toggle('on', isAutoSpeak);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新所有面板的语速显示
|
||||
*/
|
||||
export function updateSpeedAll() {
|
||||
const config = getConfigFn?.();
|
||||
const currentSpeed = config?.volc?.speechRate || 1.0;
|
||||
|
||||
panelMap.forEach((ui) => {
|
||||
if (!ui.root) return;
|
||||
if (ui.speedSlider) ui.speedSlider.value = currentSpeed;
|
||||
if (ui.speedVal) ui.speedVal.textContent = currentSpeed.toFixed(1) + 'x';
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新所有面板的音色选择
|
||||
*/
|
||||
export function updateVoiceAll() {
|
||||
const config = getConfigFn?.();
|
||||
panelMap.forEach((ui) => {
|
||||
if (!ui.root || !ui.voiceSelect) return;
|
||||
buildVoiceOptions(ui.voiceSelect, config);
|
||||
});
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// 对外接口
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
export function initTtsPanelStyles() {
|
||||
injectStyles();
|
||||
}
|
||||
|
||||
function observeForLazyMount(messageEl, messageId, onPlay) {
|
||||
if (panelMap.has(messageId) && panelMap.get(messageId).root?.isConnected) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (pendingCallbacks.has(messageId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
setupObserver();
|
||||
pendingCallbacks.set(messageId, onPlay);
|
||||
observer.observe(messageEl);
|
||||
}
|
||||
|
||||
export function ensureTtsPanel(messageEl, messageId, onPlay) {
|
||||
injectStyles();
|
||||
|
||||
if (panelMap.has(messageId)) {
|
||||
const existingUi = panelMap.get(messageId);
|
||||
if (existingUi.root && existingUi.root.isConnected) {
|
||||
|
||||
return existingUi;
|
||||
}
|
||||
|
||||
existingUi._cleanup?.();
|
||||
const existing = panelMap.get(messageId);
|
||||
if (existing.root?.isConnected) return existing;
|
||||
existing._cleanup?.();
|
||||
panelMap.delete(messageId);
|
||||
}
|
||||
|
||||
const rect = messageEl.getBoundingClientRect();
|
||||
if (rect.top < window.innerHeight + 500 && rect.bottom > -500) {
|
||||
return mountPanel(messageEl, messageId, onPlay);
|
||||
}
|
||||
|
||||
if (!observer) {
|
||||
observer = new IntersectionObserver((entries) => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
const el = entry.target;
|
||||
const mid = Number(el.getAttribute('mesid'));
|
||||
const cb = pendingCallbacks.get(mid);
|
||||
if (cb) {
|
||||
mountPanel(el, mid, cb);
|
||||
pendingCallbacks.delete(mid);
|
||||
observer.unobserve(el);
|
||||
}
|
||||
}
|
||||
});
|
||||
}, { rootMargin: '500px' });
|
||||
}
|
||||
|
||||
pendingCallbacks.set(messageId, onPlay);
|
||||
observer.observe(messageEl);
|
||||
observeForLazyMount(messageEl, messageId, onPlay);
|
||||
return null;
|
||||
}
|
||||
|
||||
export function renderPanelsForChat(chat, getMessageElement, onPlay) {
|
||||
injectStyles();
|
||||
|
||||
let immediateCount = 0;
|
||||
|
||||
for (let i = chat.length - 1; i >= 0; i--) {
|
||||
const message = chat[i];
|
||||
if (!message || message.is_user) continue;
|
||||
|
||||
const messageEl = getMessageElement(i);
|
||||
if (!messageEl) continue;
|
||||
|
||||
if (panelMap.has(i) && panelMap.get(i).root?.isConnected) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (immediateCount < INITIAL_RENDER_LIMIT) {
|
||||
mountPanel(messageEl, i, onPlay);
|
||||
immediateCount++;
|
||||
} else {
|
||||
observeForLazyMount(messageEl, i, onPlay);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function updateTtsPanel(messageId, state) {
|
||||
const ui = panelMap.get(messageId);
|
||||
if (!ui || !state) return;
|
||||
@@ -679,7 +920,6 @@ export function updateTtsPanel(messageId, state) {
|
||||
ui.root.dataset.status = status;
|
||||
ui.root.dataset.hasQueue = hasQueue ? 'true' : 'false';
|
||||
|
||||
// 状态文本和图标
|
||||
let statusText = '';
|
||||
let playIcon = '▶';
|
||||
let showStop = false;
|
||||
@@ -726,18 +966,25 @@ export function updateTtsPanel(messageId, state) {
|
||||
playIcon = '▶';
|
||||
}
|
||||
|
||||
// 更新播放按钮(保留自动朗读指示点)
|
||||
const playBtnContent = ui.playBtn.querySelector('.xb-tts-auto-dot');
|
||||
ui.playBtn.textContent = playIcon;
|
||||
if (playBtnContent) {
|
||||
ui.playBtn.appendChild(playBtnContent);
|
||||
} else {
|
||||
const dot = document.createElement('span');
|
||||
dot.className = 'xb-tts-auto-dot';
|
||||
ui.playBtn.appendChild(dot);
|
||||
}
|
||||
|
||||
ui.statusText.textContent = statusText;
|
||||
|
||||
// 队列徽标
|
||||
if (hasQueue && current > 0) {
|
||||
ui.badge.textContent = `${current}/${total}`;
|
||||
}
|
||||
|
||||
// 停止按钮显示
|
||||
ui.stopBtn.style.display = showStop ? '' : 'none';
|
||||
|
||||
// 进度条
|
||||
if (hasQueue && total > 0) {
|
||||
const pct = Math.min(100, (current / total) * 100);
|
||||
ui.progressInner.style.width = `${pct}%`;
|
||||
@@ -748,29 +995,31 @@ export function updateTtsPanel(messageId, state) {
|
||||
ui.progressInner.style.width = '0%';
|
||||
}
|
||||
|
||||
// 用量显示
|
||||
if (state.textLength) {
|
||||
if (state.textLength && ui.usageText) {
|
||||
ui.usageText.textContent = `${state.textLength} 字`;
|
||||
}
|
||||
}
|
||||
|
||||
export function removeAllTtsPanels() {
|
||||
panelMap.forEach(ui => {
|
||||
ui._cleanup?.();
|
||||
ui.root?.remove();
|
||||
});
|
||||
panelMap.clear();
|
||||
pendingCallbacks.clear();
|
||||
observer?.disconnect();
|
||||
observer = null;
|
||||
}
|
||||
|
||||
export function removeTtsPanel(messageId) {
|
||||
const ui = panelMap.get(messageId);
|
||||
if (ui) {
|
||||
ui._cleanup?.();
|
||||
ui.root?.remove();
|
||||
panelMap.delete(messageId);
|
||||
}
|
||||
pendingCallbacks.delete(messageId);
|
||||
}
|
||||
|
||||
export function removeAllTtsPanels() {
|
||||
panelMap.forEach((ui) => {
|
||||
ui._cleanup?.();
|
||||
});
|
||||
panelMap.clear();
|
||||
pendingCallbacks.clear();
|
||||
|
||||
observer?.disconnect();
|
||||
observer = null;
|
||||
}
|
||||
|
||||
export function getPanelMap() {
|
||||
return panelMap;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user