// ==UserScript==
// @name Caveduck Modifier
// @namespace https://labs.muyi.tw/caveduck_modifier/
// @version 0.29.8
// @description 修改Caveduck網站的樣式。
// @license AGPL-3.0-or-later
// @author 慕儀
// @match *://caveduck.io/*
// @grant GM_addStyle
// @icon data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZlcnNpb249IjEuMSIgdmlld0JveD0iMCAwIDEyODAgMTI4MCI+CiAgPGRlZnM+CiAgICA8c3R5bGU+CiAgICAgIC5jbHMtMSB7CiAgICAgICAgZmlsbDogI2ZmZjsKICAgICAgfQoKICAgICAgLmNscy0yIHsKICAgICAgICBmaWxsOiAjZjJiNDEyOwogICAgICB9CiAgICA8L3N0eWxlPgogIDwvZGVmcz4KICA8IS0tIEdlbmVyYXRvcjogQWRvYmUgSWxsdXN0cmF0b3IgMjguNy4xLCBTVkcgRXhwb3J0IFBsdWctSW4gLiBTVkcgVmVyc2lvbjogMS4yLjAgQnVpbGQgMTQyKSAgLS0+CiAgPGc+CiAgICA8ZyBpZD0iQ2F2ZWR1Y2siPgogICAgICA8ZyBpZD0iQ2F2ZWR1Y2stMiIgZGF0YS1uYW1lPSJDYXZlZHVjayI+CiAgICAgICAgPHBhdGggZD0iTTEwOTYuMSw0NTMuNWMzMDUuMiw0OTcuMywxNTIuNiw3NDYtNDU3LjgsNzQ2Uy0xMjQuNiw5NTAuOCwxODAuNiw0NTMuNWMzMDUuMi00OTcuMyw2MTAuMy00OTcuMyw5MTUuNSwwWiIvPgogICAgICAgIDxwYXRoIGNsYXNzPSJjbHMtMSIgZD0iTTExNDcuNiw0NTMuNWMyMDguOCwzNjEuNywxMDQuNCw1NDIuNS0zMTMuMiw1NDIuNXMtNTIyLTE4MC44LTMxMy4yLTU0Mi41YzIwOC44LTM2MS43LDQxNy42LTM2MS43LDYyNi41LDBaIi8+CiAgICAgICAgPHBhdGggY2xhc3M9ImNscy0yIiBkPSJNNjg0LDcyMS4yYzEwMC4yLDE3My42LDUwLjEsMjYwLjQtMTUwLjMsMjYwLjRzLTI1MC42LTg2LjgtMTUwLjMtMjYwLjRjMTAwLjItMTczLjYsMjAwLjUtMTczLjYsMzAwLjcsMFoiLz4KICAgICAgICA8cGF0aCBkPSJNODcxLjksNjEyLjdjMjUuMSw0My40LDEyLjUsNjUuMS0zNy42LDY1LjFzLTYyLjYtMjEuNy0zNy42LTY1LjFjMjUuMS00My40LDUwLjEtNDMuNCw3NS4yLDBaIi8+CiAgICAgIDwvZz4KICAgIDwvZz4KICA8L2c+Cjwvc3ZnPg==
// ==/UserScript==
(function () {
'use strict';
let inLanguage;
let debouncedAutoHeight;
const $ = (selector) => document.querySelectorAll(selector);
const $$ = (selector) => document.querySelector(selector);
const tarAutoHeight = `prompt-input`;
const tarAutoScrollHeight = `lorebook-data-input textarea, #charDesc`;
const textReplaceSelector = `#chatMessages b:not([data-text-replaced]), #chatMessages p:not([data-text-replaced])`;
const cURL = window.location.href;
const muyiStyles = 'https://labs.muyi.tw/caveduck_modifier/style2.css?v=11403290552';
const fontStyles = `
user-input-form div[ng-repeat] textarea,
#chatMessages b,
#chatMessages p,
form[ng-if~="!!chat.editMode"] textarea {
font: normal clamp(16px, .95vw, 32px) / 1.75em var(--m_ff1);
}
#chatMessages b {
font-family: var(--m_ff2);
font-weight: 400;
}
form[ng-if~="!!chat.editMode"] textarea {
font-size: var(--m_font-size);
}
user-input-form div[ng-repeat] textarea {
font-size: var(--m_font-size);
}
`;
const charMap = {
'\\.{2,}': '⋯⋯',
'⋯': '⋯⋯',
'⋯{3,}': '⋯⋯',
'!': '!',
'\\?': '?',
'~': '~',
';': ';',
':': ':',
',': ',',
'\\.': '。',
'\\(': '(',
'\\)': ')'
};
const locale = {
'zh-hant': {
cb_fontOverride: ['覆蓋字型', '作用頁:Talk<br>用慕儀喜歡的自訂字型取代預設字型。'],
cb_shortButtons: ['快捷按鈕', '作用頁:Talk<br>將「我的資訊」與「使用者筆記」按鈕移到右側。'],
cb_replaceText: ['取代符號', '作用頁:Talk<br>Claude 3 Haiku會使用錯誤的中文標點符號,這個功能可以修正它。'],
cb_deskFix: ['桌面顯示修正', '作用頁:Talk<br>修正高解析度下的顯示體驗,讓對話畫面佔用全版,且圖片顯示區域更大。'],
cb_mdFix: ['行動顯示修正', '作用頁:Talk<br>修正行動裝置的顯示問題。'],
cb_autoHeight: ['編輯框自動高度', '作用頁:Edit Character、Lorebook、Custom prompt<br>每個項目使用卷軸十分愚蠢,勾選此項可以將其設為自動高度。'],
toggleButton: '慕儀\n神器',
reloadButton: '套用並重載',
},
'en': {
cb_fontOverride: ['Override Font', 'Active on: Talk<br>Replace default font with MuYi\'s preferred custom font.'],
cb_shortButtons: ['Shortcut buttons', 'Active on: Talk<br>Move the "My Information" and "User Notes" buttons to the right side.'],
cb_replaceText: ['Replace Symbols', 'Active on: Talk<br>Claude 3 Haiku uses incorrect Chinese punctuation. This feature fixes it.'],
cb_deskFix: ['Desktop Display Fix', 'Active on: Talk<br>Fix display experience on high resolution, making the chat screen occupy the full screen and enlarging the image display area.'],
cb_mdFix: ['Mobile Display Fix', 'Active on: Talk<br>Fix the display issues of mobile devices.'],
cb_autoHeight: ['Auto Height for Edit Box', 'Active on: Edit Character、Lorebook、Custom prompt<br>Using scrollbars for each item is stupid. Enable this to auto-height.'],
toggleButton: 'MuYi\'s\nToolbox',
reloadButton: 'Apply and reload',
},
};
const settings = Object.keys(locale['en'])
.filter(key => key.startsWith('cb_'))
.map(localeName => ({
localeName: localeName,
key: `sw_${localeName.slice(3)}`
}));
const switches = {};
settings.forEach(setting => {
switches[setting.key] = JSON.parse(localStorage.getItem(`enable${setting.key.slice(3)}`) || 'false');
});
const domElements = {
o_editMyInfoButton: 'button[ng-click="uiState.settingModalMode = \'edit_my_info\'"]',
o_editUserNoteButton: 'button[ng-click="uiState.settingModalMode = \'edit_user_note\'"]',
o_optionButton: 'button#optionButton',
o_imgButton: '.hidden[ng-show~="backgroundImage"]'
};
function getAncestor(selector, level) {
if (typeof selector !== 'string' || typeof level !== 'number' || level < 0) {
throw new Error('Invalid parameters');
}
const element = document.querySelector(selector);
if (!element) {
return null;
}
if (level === 0) {
return element;
}
let current = element;
for (let i = 0; i < level; i++) {
current = current.parentElement;
if (!current) {
return null;
}
}
return current;
}
// 添加自訂樣式
function addCustomStyles() {
GM_addStyle(fontStyles);
console.log("Custom styles added.");
}
// 自動調整高度的核心函式
function autoHeight(el) {
el.style.height = 'auto';
el.style.overflow = 'auto';
}
function autoScrollHeight(el) {
autoHeight(el);
el.style.height = `${el.scrollHeight}px`;
}
// 初始化符合條件的元素
function initializeAutoHeight() {
if (!debouncedAutoHeight) {
debouncedAutoHeight = debounce(() => {
$(tarAutoHeight).forEach(autoHeight);
$(tarAutoScrollHeight).forEach(autoScrollHeight);
}, 666, 2);
debouncedAutoHeight();
window.addEventListener('keydown', debouncedAutoHeight);
window.addEventListener('click', debouncedAutoHeight);
}
}
// 替換指定選擇符的內容
function replaceTextContent() {
const processedAttribute = "data-text-replaced"; // 標記屬性名稱
const el = $(`${textReplaceSelector}:not([${processedAttribute}])`);
el.forEach((el) => {
let originalText = el.textContent;
for (const [pattern, replacement] of Object.entries(charMap)) {
originalText = originalText.replace(new RegExp(pattern, 'g'), replacement);
}
el.textContent = originalText;
el.setAttribute(processedAttribute, ""); // 添加標記屬性
});
}
// 延遲觸發的去抖函式
function debounce(func, delay, repeat) {
let timer = null;
let count = 1;
return () => {
func();
if (timer) clearInterval(timer);
timer = setInterval(() => {
func();
count += 1;
if (count >= repeat) {
clearInterval(timer);
}
}, delay);
};
}
// 啟動 MutationObserver
function initializeObserver() {
const observer = new MutationObserver(() => {
mainAction();
});
observer.observe(document.body, { childList: true, subtree: true });
console.log("MutationObserver initialized.");
}
// 檢查 inLanguage 並啟動必要功能
function checkInLanguage() {
const script = $$('script[type="application/ld+json"]');
if (script) {
try {
const jsonData = JSON.parse(script.textContent);
inLanguage = jsonData[0]?.inLanguage || '';
} catch (error) {
console.error("Failed to parse JSON:", error);
}
}
}
function createSettingsUI() {
const lang = ['zh-hant', 'zh-hans'].includes(inLanguage) ? 'zh-hant' : 'en';
const texts = locale[lang];
// 創建核取方塊
const createCheckbox = (setting) => {
const container = document.createElement('div');
const checkbox = document.createElement('input');
const label = document.createElement('label');
const desc = document.createElement('div');
desc.className = 'desc';
checkbox.type = 'checkbox';
const storageKey = `enable${setting.key.slice(3)}`;
checkbox.id = storageKey;
const isChecked = JSON.parse(localStorage.getItem(storageKey) || 'false');
checkbox.checked = isChecked;
label.setAttribute('for', storageKey);
label.textContent = texts[setting.localeName][0];
desc.innerHTML = texts[setting.localeName][1];
label.appendChild(desc);
checkbox.addEventListener('change', () => {
localStorage.setItem(storageKey, checkbox.checked);
});
container.appendChild(checkbox);
container.appendChild(label);
return container;
};
// 創建按鈕和設定視窗
const mt = document.createElement('div');
mt.id = 'mt';
const toggleButton = document.createElement('button');
toggleButton.className = 'button--red mt_toggleButton';
toggleButton.textContent = texts.toggleButton;
if (lang === 'zh-hant') toggleButton.style.fontSize = '.8rem';
const settingsPanel = document.createElement('div');
settingsPanel.className = 'mt_fixed mt_settingsPanel';
settingsPanel.style.display = 'none';
// 添加核取方塊
settings.forEach((setting) => {
settingsPanel.appendChild(createCheckbox(setting));
});
// 重整按鈕
const reloadButton = document.createElement('button');
reloadButton.textContent = texts.reloadButton;
reloadButton.className = 'button--red';
reloadButton.addEventListener('click', () => location.reload());
settingsPanel.appendChild(reloadButton);
// 切換視窗顯示
toggleButton.addEventListener('click', (event) => {
event.stopPropagation(); // 避免點擊 toggleButton 時也觸發關閉
const isVisible = settingsPanel.style.display === 'block';
if (!isVisible) {
settingsPanel.style.display = 'block';
// 加入全頁點擊監聽器,只會執行一次
const outsideClickListener = (e) => {
if (!settingsPanel.contains(e.target) && e.target !== toggleButton) {
settingsPanel.style.display = 'none';
document.removeEventListener('click', outsideClickListener);
}
};
document.addEventListener('click', outsideClickListener);
} else {
settingsPanel.style.display = 'none';
}
});
// 添加到頁面
document.body.appendChild(mt);
mt.appendChild(toggleButton);
document.body.appendChild(settingsPanel);
// 快捷按鈕
if (switches.sw_shortButtons && mURL('*/talk/*')) {
['👤', '📝'].forEach((text, index) => {
const button = document.createElement('button');
button.textContent = text;
button.addEventListener('click', () => {
const optionButton = $$(domElements.o_optionButton);
if (!optionButton) return console.warn('找不到 option 按鈕');
optionButton.click();
setTimeout(() => {
const targetSelector = index === 0
? domElements.o_editMyInfoButton
: domElements.o_editUserNoteButton;
const targetButton = $$(targetSelector);
if (targetButton) {
targetButton.click();
} else {
console.warn('找不到指定按鈕');
}
}, 100);
});
mt.appendChild(button);
});
}
}
function checkSettings() {
checkInLanguage();
settings.forEach(setting => {
switches[setting.switchVar] = JSON.parse(localStorage.getItem(setting.storageKey) || 'false');
});
if (switches.sw_fontOverride) addCustomStyles();
mainAction();
}
function mURL(pattern) {
const patternParts = pattern.split('*');
let lastIndex = 0;
for (let part of patternParts) {
if (part === "") continue;
const index = cURL.indexOf(part, lastIndex);
if (index === -1) return false;
lastIndex = index + part.length;
}
return true;
}
function setStylesheet() {
const link = document.createElement("link");
link.rel = 'stylesheet';
link.href = muyiStyles;
document.head.appendChild(link);
}
function mainAction() {
if (switches.sw_autoHeight && (mURL('*/created-characters/*') || mURL('*/prompt-build-script/*') || mURL('*/lorebook-editor/*'))) initializeAutoHeight();
if (['zh-hant', 'zh-hans', 'ja', 'ko'].includes(inLanguage) && (switches.sw_replaceText)) replaceTextContent();
if (mURL('*/public')) {
if (switches.sw_mdFix) {
$$('section.flex-col.py-60 > h3.text-2xl').classList.remove('px-16');
}
}
if (mURL('*/talk/*')) {
if (switches.sw_mdFix) {
const imgButton = $$(domElements.o_imgButton);
if (imgButton) {
imgButton.classList.add('mt_fix');
} else {
console.warn('找不到圖片按鈕元素(o_imgButton)');
}
}
if (switches.sw_deskFix) {
$('.container, .items-center.text-lg.relative, div.flex.overflow-hidden.flex-grow>div.relative[class*="md:w-[40%]"], div.flex.overflow-hidden.flex-grow>div.flex[class*="md:w-[60%]"]').forEach(el => {
el.classList.add('mt_fix');
});
}
}
}
window.addEventListener('load', () => {
setStylesheet();
checkSettings();
createSettingsUI();
setTimeout(initializeObserver, 100);
});
})();