Apply local structure and editor config
This commit is contained in:
259
widgets/button-collapse.js
Normal file
259
widgets/button-collapse.js
Normal file
@@ -0,0 +1,259 @@
|
||||
let stylesInjected = false;
|
||||
|
||||
const SELECTORS = {
|
||||
chat: '#chat',
|
||||
messages: '.mes',
|
||||
mesButtons: '.mes_block .mes_buttons',
|
||||
buttons: '.memory-button, .dynamic-prompt-analysis-btn, .mes_history_preview',
|
||||
collapse: '.xiaobaix-collapse-btn',
|
||||
};
|
||||
|
||||
const XPOS_KEY = 'xiaobaix_x_btn_position';
|
||||
const getXBtnPosition = () => {
|
||||
try {
|
||||
return (
|
||||
window?.extension_settings?.LittleWhiteBox?.xBtnPosition ||
|
||||
localStorage.getItem(XPOS_KEY) ||
|
||||
'name-left'
|
||||
);
|
||||
} catch {
|
||||
return 'name-left';
|
||||
}
|
||||
};
|
||||
|
||||
const injectStyles = () => {
|
||||
if (stylesInjected) return;
|
||||
const css = `
|
||||
.mes_block .mes_buttons{align-items:center}
|
||||
.xiaobaix-collapse-btn{
|
||||
position:relative;display:inline-flex;width:32px;height:32px;justify-content:center;align-items:center;
|
||||
border-radius:50%;background:var(--SmartThemeBlurTintColor);cursor:pointer;
|
||||
box-shadow:inset 0 0 15px rgba(0,0,0,.6),0 2px 8px rgba(0,0,0,.2);
|
||||
transition:opacity .15s ease,transform .15s ease}
|
||||
.xiaobaix-xstack{position:relative;display:inline-flex;align-items:center;justify-content:center;pointer-events:none}
|
||||
.xiaobaix-xstack span{
|
||||
position:absolute;font:italic 900 20px 'Arial Black',sans-serif;letter-spacing:-2px;transform:scaleX(.8);
|
||||
text-shadow:0 0 10px rgba(255,255,255,.5),0 0 20px rgba(100,200,255,.3);color:#fff}
|
||||
.xiaobaix-xstack span:nth-child(1){color:rgba(255,255,255,.1);transform:scaleX(.8) translateX(-8px);text-shadow:none}
|
||||
.xiaobaix-xstack span:nth-child(2){color:rgba(255,255,255,.2);transform:scaleX(.8) translateX(-4px);text-shadow:none}
|
||||
.xiaobaix-xstack span:nth-child(3){color:rgba(255,255,255,.4);transform:scaleX(.8) translateX(-2px);text-shadow:none}
|
||||
.xiaobaix-sub-container{display:none;position:absolute;right:38px;border-radius:8px;padding:4px;gap:8px;pointer-events:auto}
|
||||
.xiaobaix-collapse-btn.open .xiaobaix-sub-container{display:flex;background:var(--SmartThemeBlurTintColor)}
|
||||
.xiaobaix-collapse-btn.open,.xiaobaix-collapse-btn.open ~ *{pointer-events:auto!important}
|
||||
.mes_block .mes_buttons.xiaobaix-expanded{width:150px}
|
||||
.xiaobaix-sub-container,.xiaobaix-sub-container *{pointer-events:auto!important}
|
||||
.xiaobaix-sub-container .memory-button,.xiaobaix-sub-container .dynamic-prompt-analysis-btn,.xiaobaix-sub-container .mes_history_preview{opacity:1!important;filter:none!important}
|
||||
.xiaobaix-sub-container.dir-right{left:38px;right:auto;z-index:1000;margin-top:2px}
|
||||
`;
|
||||
const style = document.createElement('style');
|
||||
style.textContent = css;
|
||||
document.head.appendChild(style);
|
||||
stylesInjected = true;
|
||||
};
|
||||
|
||||
const createCollapseButton = (dirRight) => {
|
||||
injectStyles();
|
||||
const btn = document.createElement('div');
|
||||
btn.className = 'mes_btn xiaobaix-collapse-btn';
|
||||
// Template-only UI markup.
|
||||
// eslint-disable-next-line no-unsanitized/property
|
||||
btn.innerHTML = `
|
||||
<div class="xiaobaix-xstack"><span>X</span><span>X</span><span>X</span><span>X</span></div>
|
||||
<div class="xiaobaix-sub-container${dirRight ? ' dir-right' : ''}"></div>
|
||||
`;
|
||||
const sub = btn.lastElementChild;
|
||||
|
||||
['click','pointerdown','pointerup'].forEach(t => {
|
||||
sub.addEventListener(t, e => e.stopPropagation(), { passive: true });
|
||||
});
|
||||
|
||||
btn.addEventListener('click', (e) => {
|
||||
e.preventDefault(); e.stopPropagation();
|
||||
const open = btn.classList.toggle('open');
|
||||
const mesButtons = btn.closest(SELECTORS.mesButtons);
|
||||
if (mesButtons) mesButtons.classList.toggle('xiaobaix-expanded', open);
|
||||
});
|
||||
|
||||
return btn;
|
||||
};
|
||||
|
||||
const findInsertPoint = (messageEl) => {
|
||||
return messageEl.querySelector(
|
||||
'.ch_name.flex-container.justifySpaceBetween .flex-container.flex1.alignitemscenter,' +
|
||||
'.ch_name.flex-container.justifySpaceBetween .flex-container.flex1.alignItemsCenter'
|
||||
);
|
||||
};
|
||||
|
||||
const ensureCollapseForMessage = (messageEl, pos) => {
|
||||
const mesButtons = messageEl.querySelector(SELECTORS.mesButtons);
|
||||
if (!mesButtons) return null;
|
||||
|
||||
let collapseBtn = messageEl.querySelector(SELECTORS.collapse);
|
||||
const dirRight = pos === 'edit-right';
|
||||
|
||||
if (!collapseBtn) collapseBtn = createCollapseButton(dirRight);
|
||||
else collapseBtn.querySelector('.xiaobaix-sub-container')?.classList.toggle('dir-right', dirRight);
|
||||
|
||||
if (dirRight) {
|
||||
const container = findInsertPoint(messageEl);
|
||||
if (!container) return null;
|
||||
if (collapseBtn.parentNode !== container) container.appendChild(collapseBtn);
|
||||
} else {
|
||||
if (mesButtons.lastElementChild !== collapseBtn) mesButtons.appendChild(collapseBtn);
|
||||
}
|
||||
return collapseBtn;
|
||||
};
|
||||
|
||||
let processed = new WeakSet();
|
||||
let io = null;
|
||||
let mo = null;
|
||||
let queue = [];
|
||||
let rafScheduled = false;
|
||||
|
||||
const processOneMessage = (message) => {
|
||||
if (!message || processed.has(message)) return;
|
||||
|
||||
const mesButtons = message.querySelector(SELECTORS.mesButtons);
|
||||
if (!mesButtons) { processed.add(message); return; }
|
||||
|
||||
const pos = getXBtnPosition();
|
||||
if (pos === 'edit-right' && !findInsertPoint(message)) { processed.add(message); return; }
|
||||
|
||||
const targetBtns = mesButtons.querySelectorAll(SELECTORS.buttons);
|
||||
if (!targetBtns.length) { processed.add(message); return; }
|
||||
|
||||
const collapseBtn = ensureCollapseForMessage(message, pos);
|
||||
if (!collapseBtn) { processed.add(message); return; }
|
||||
|
||||
const sub = collapseBtn.querySelector('.xiaobaix-sub-container');
|
||||
const frag = document.createDocumentFragment();
|
||||
targetBtns.forEach(b => frag.appendChild(b));
|
||||
sub.appendChild(frag);
|
||||
|
||||
processed.add(message);
|
||||
};
|
||||
|
||||
const ensureIO = () => {
|
||||
if (io) return io;
|
||||
io = new IntersectionObserver((entries) => {
|
||||
for (const e of entries) {
|
||||
if (!e.isIntersecting) continue;
|
||||
processOneMessage(e.target);
|
||||
io.unobserve(e.target);
|
||||
}
|
||||
}, {
|
||||
root: document.querySelector(SELECTORS.chat) || null,
|
||||
rootMargin: '200px 0px',
|
||||
threshold: 0
|
||||
});
|
||||
return io;
|
||||
};
|
||||
|
||||
const observeVisibility = (nodes) => {
|
||||
const obs = ensureIO();
|
||||
nodes.forEach(n => { if (n && !processed.has(n)) obs.observe(n); });
|
||||
};
|
||||
|
||||
const hookMutations = () => {
|
||||
const chat = document.querySelector(SELECTORS.chat);
|
||||
if (!chat) return;
|
||||
|
||||
if (!mo) {
|
||||
mo = new MutationObserver((muts) => {
|
||||
for (const m of muts) {
|
||||
m.addedNodes && m.addedNodes.forEach(n => {
|
||||
if (n.nodeType !== 1) return;
|
||||
const el = n;
|
||||
if (el.matches?.(SELECTORS.messages)) queue.push(el);
|
||||
else el.querySelectorAll?.(SELECTORS.messages)?.forEach(mes => queue.push(mes));
|
||||
});
|
||||
}
|
||||
if (!rafScheduled && queue.length) {
|
||||
rafScheduled = true;
|
||||
requestAnimationFrame(() => {
|
||||
observeVisibility(queue);
|
||||
queue = [];
|
||||
rafScheduled = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
mo.observe(chat, { childList: true, subtree: true });
|
||||
};
|
||||
|
||||
const processExistingVisible = () => {
|
||||
const all = document.querySelectorAll(`${SELECTORS.chat} ${SELECTORS.messages}`);
|
||||
if (!all.length) return;
|
||||
const unprocessed = [];
|
||||
all.forEach(n => { if (!processed.has(n)) unprocessed.push(n); });
|
||||
if (unprocessed.length) observeVisibility(unprocessed);
|
||||
};
|
||||
|
||||
const initButtonCollapse = () => {
|
||||
injectStyles();
|
||||
hookMutations();
|
||||
processExistingVisible();
|
||||
if (window && window['registerModuleCleanup']) {
|
||||
try { window['registerModuleCleanup']('buttonCollapse', cleanup); } catch {}
|
||||
}
|
||||
};
|
||||
|
||||
const processButtonCollapse = () => {
|
||||
processExistingVisible();
|
||||
};
|
||||
|
||||
const registerButtonToSubContainer = (messageId, buttonEl) => {
|
||||
if (!buttonEl) return false;
|
||||
const message = document.querySelector(`${SELECTORS.chat} ${SELECTORS.messages}[mesid="${messageId}"]`);
|
||||
if (!message) return false;
|
||||
|
||||
processOneMessage(message);
|
||||
|
||||
const pos = getXBtnPosition();
|
||||
const collapseBtn = message.querySelector(SELECTORS.collapse) || ensureCollapseForMessage(message, pos);
|
||||
if (!collapseBtn) return false;
|
||||
|
||||
const sub = collapseBtn.querySelector('.xiaobaix-sub-container');
|
||||
sub.appendChild(buttonEl);
|
||||
buttonEl.style.pointerEvents = 'auto';
|
||||
buttonEl.style.opacity = '1';
|
||||
return true;
|
||||
};
|
||||
|
||||
const cleanup = () => {
|
||||
io?.disconnect(); io = null;
|
||||
mo?.disconnect(); mo = null;
|
||||
queue = [];
|
||||
rafScheduled = false;
|
||||
|
||||
document.querySelectorAll(SELECTORS.collapse).forEach(btn => {
|
||||
const sub = btn.querySelector('.xiaobaix-sub-container');
|
||||
const message = btn.closest(SELECTORS.messages) || btn.closest('.mes');
|
||||
const mesButtons = message?.querySelector(SELECTORS.mesButtons) || message?.querySelector('.mes_buttons');
|
||||
if (sub && mesButtons) {
|
||||
mesButtons.classList.remove('xiaobaix-expanded');
|
||||
const frag = document.createDocumentFragment();
|
||||
while (sub.firstChild) frag.appendChild(sub.firstChild);
|
||||
mesButtons.appendChild(frag);
|
||||
}
|
||||
btn.remove();
|
||||
});
|
||||
|
||||
processed = new WeakSet();
|
||||
};
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
Object.assign(window, {
|
||||
initButtonCollapse,
|
||||
cleanupButtonCollapse: cleanup,
|
||||
registerButtonToSubContainer,
|
||||
processButtonCollapse,
|
||||
});
|
||||
|
||||
document.addEventListener('xiaobaixEnabledChanged', (e) => {
|
||||
const en = e && e.detail && e.detail.enabled;
|
||||
if (!en) cleanup();
|
||||
});
|
||||
}
|
||||
|
||||
export { initButtonCollapse, cleanup, registerButtonToSubContainer, processButtonCollapse };
|
||||
265
widgets/message-toolbar.js
Normal file
265
widgets/message-toolbar.js
Normal file
@@ -0,0 +1,265 @@
|
||||
// widgets/message-toolbar.js
|
||||
/**
|
||||
* 消息工具栏管理器
|
||||
* 统一管理消息级别的功能按钮(TTS、画图等)
|
||||
*/
|
||||
|
||||
let toolbarMap = new WeakMap();
|
||||
const registeredComponents = new Map(); // messageId -> Map<componentId, element>
|
||||
|
||||
let stylesInjected = false;
|
||||
|
||||
function injectStyles() {
|
||||
if (stylesInjected) return;
|
||||
stylesInjected = true;
|
||||
|
||||
const style = document.createElement('style');
|
||||
style.id = 'xb-msg-toolbar-styles';
|
||||
style.textContent = `
|
||||
.xb-msg-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin: 8px 0;
|
||||
min-height: 34px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.xb-msg-toolbar:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.xb-msg-toolbar-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.xb-msg-toolbar-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.xb-msg-toolbar-left:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.xb-msg-toolbar-right:empty {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
function getMessageElement(messageId) {
|
||||
return document.querySelector(`.mes[mesid="${messageId}"]`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取或创建消息的工具栏
|
||||
*/
|
||||
export function getOrCreateToolbar(messageEl) {
|
||||
if (!messageEl) return null;
|
||||
|
||||
// 已有工具栏且有效
|
||||
if (toolbarMap.has(messageEl)) {
|
||||
const existing = toolbarMap.get(messageEl);
|
||||
if (existing.isConnected) return existing;
|
||||
toolbarMap.delete(messageEl);
|
||||
}
|
||||
|
||||
injectStyles();
|
||||
|
||||
// 找锚点
|
||||
const nameBlock = messageEl.querySelector('.mes_block > .ch_name') ||
|
||||
messageEl.querySelector('.name_text')?.parentElement;
|
||||
if (!nameBlock) return null;
|
||||
|
||||
// 检查是否已有工具栏
|
||||
let toolbar = nameBlock.parentNode.querySelector(':scope > .xb-msg-toolbar');
|
||||
if (toolbar) {
|
||||
toolbarMap.set(messageEl, toolbar);
|
||||
ensureSections(toolbar);
|
||||
return toolbar;
|
||||
}
|
||||
|
||||
// 创建工具栏
|
||||
toolbar = document.createElement('div');
|
||||
toolbar.className = 'xb-msg-toolbar';
|
||||
|
||||
const leftSection = document.createElement('div');
|
||||
leftSection.className = 'xb-msg-toolbar-left';
|
||||
|
||||
const rightSection = document.createElement('div');
|
||||
rightSection.className = 'xb-msg-toolbar-right';
|
||||
|
||||
toolbar.appendChild(leftSection);
|
||||
toolbar.appendChild(rightSection);
|
||||
|
||||
nameBlock.parentNode.insertBefore(toolbar, nameBlock.nextSibling);
|
||||
toolbarMap.set(messageEl, toolbar);
|
||||
|
||||
return toolbar;
|
||||
}
|
||||
|
||||
function ensureSections(toolbar) {
|
||||
if (!toolbar.querySelector('.xb-msg-toolbar-left')) {
|
||||
const left = document.createElement('div');
|
||||
left.className = 'xb-msg-toolbar-left';
|
||||
toolbar.insertBefore(left, toolbar.firstChild);
|
||||
}
|
||||
if (!toolbar.querySelector('.xb-msg-toolbar-right')) {
|
||||
const right = document.createElement('div');
|
||||
right.className = 'xb-msg-toolbar-right';
|
||||
toolbar.appendChild(right);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册组件到工具栏
|
||||
*/
|
||||
export function registerToToolbar(messageId, element, options = {}) {
|
||||
const { position = 'left', id } = options;
|
||||
|
||||
const messageEl = getMessageElement(messageId);
|
||||
if (!messageEl) return false;
|
||||
|
||||
const toolbar = getOrCreateToolbar(messageEl);
|
||||
if (!toolbar) return false;
|
||||
|
||||
// 设置组件 ID
|
||||
if (id) {
|
||||
element.dataset.toolbarId = id;
|
||||
|
||||
// 去重:移除已存在的同 ID 组件
|
||||
const existing = toolbar.querySelector(`[data-toolbar-id="${id}"]`);
|
||||
if (existing && existing !== element) {
|
||||
existing.remove();
|
||||
}
|
||||
}
|
||||
|
||||
// 插入到对应区域
|
||||
const section = position === 'right'
|
||||
? toolbar.querySelector('.xb-msg-toolbar-right')
|
||||
: toolbar.querySelector('.xb-msg-toolbar-left');
|
||||
|
||||
if (section && !section.contains(element)) {
|
||||
section.appendChild(element);
|
||||
}
|
||||
|
||||
// 记录
|
||||
if (!registeredComponents.has(messageId)) {
|
||||
registeredComponents.set(messageId, new Map());
|
||||
}
|
||||
if (id) {
|
||||
registeredComponents.get(messageId).set(id, element);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从工具栏移除组件
|
||||
*/
|
||||
export function removeFromToolbar(messageId, element) {
|
||||
if (!element) return;
|
||||
|
||||
const componentId = element.dataset?.toolbarId;
|
||||
element.remove();
|
||||
|
||||
// 清理记录
|
||||
const components = registeredComponents.get(messageId);
|
||||
if (components && componentId) {
|
||||
components.delete(componentId);
|
||||
if (components.size === 0) {
|
||||
registeredComponents.delete(messageId);
|
||||
}
|
||||
}
|
||||
|
||||
cleanupEmptyToolbar(messageId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 ID 移除组件
|
||||
*/
|
||||
export function removeFromToolbarById(messageId, componentId) {
|
||||
const messageEl = getMessageElement(messageId);
|
||||
if (!messageEl) return;
|
||||
|
||||
const toolbar = toolbarMap.get(messageEl);
|
||||
if (!toolbar) return;
|
||||
|
||||
const element = toolbar.querySelector(`[data-toolbar-id="${componentId}"]`);
|
||||
if (element) {
|
||||
removeFromToolbar(messageId, element);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查组件是否已注册
|
||||
*/
|
||||
export function hasComponent(messageId, componentId) {
|
||||
const messageEl = getMessageElement(messageId);
|
||||
if (!messageEl) return false;
|
||||
|
||||
const toolbar = toolbarMap.get(messageEl);
|
||||
if (!toolbar) return false;
|
||||
|
||||
return !!toolbar.querySelector(`[data-toolbar-id="${componentId}"]`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理空工具栏
|
||||
*/
|
||||
function cleanupEmptyToolbar(messageId) {
|
||||
const messageEl = getMessageElement(messageId);
|
||||
if (!messageEl) return;
|
||||
|
||||
const toolbar = toolbarMap.get(messageEl);
|
||||
if (!toolbar) return;
|
||||
|
||||
const leftSection = toolbar.querySelector('.xb-msg-toolbar-left');
|
||||
const rightSection = toolbar.querySelector('.xb-msg-toolbar-right');
|
||||
|
||||
const isEmpty = (!leftSection || leftSection.children.length === 0) &&
|
||||
(!rightSection || rightSection.children.length === 0);
|
||||
|
||||
if (isEmpty) {
|
||||
toolbar.remove();
|
||||
toolbarMap.delete(messageEl);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除消息的整个工具栏
|
||||
*/
|
||||
export function removeToolbar(messageId) {
|
||||
const messageEl = getMessageElement(messageId);
|
||||
if (!messageEl) return;
|
||||
|
||||
const toolbar = toolbarMap.get(messageEl);
|
||||
if (toolbar) {
|
||||
toolbar.remove();
|
||||
toolbarMap.delete(messageEl);
|
||||
}
|
||||
registeredComponents.delete(messageId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理所有工具栏
|
||||
*/
|
||||
export function removeAllToolbars() {
|
||||
document.querySelectorAll('.xb-msg-toolbar').forEach(t => t.remove());
|
||||
toolbarMap = new WeakMap();
|
||||
registeredComponents.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取工具栏(如果存在)
|
||||
*/
|
||||
export function getToolbar(messageId) {
|
||||
const messageEl = getMessageElement(messageId);
|
||||
return messageEl ? toolbarMap.get(messageEl) : null;
|
||||
}
|
||||
Reference in New Issue
Block a user