Caveduck Modifier

修改Caveduck網站的樣式。

当前为 2025-02-05 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name			Caveduck Modifier
// @namespace		https://labs.muyi.tw/caveduck_modifier/
// @version			0.29.5
// @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=11312091930';
	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_hideLLM: ['隱藏低效LLM', '作用頁:Talk<br>隱藏表現與成本不合比例的LLM,避免誤選取。'],
			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_hideLLM: ['Hide unimportant LLMs', 'Active on: Talk<br>Hide LLMs whose performance does not correlate with their costs to avoid misselection.'],
			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: '#optionButton',
		o_imgButton: '.hidden[ng-show~="backgroundImage"]'
	};

	const [
		o_editMyInfoButton,
		o_editUserNoteButton,
		o_optionButton,
		o_imgButton
	] = Object.values(domElements).map(selector => $$(selector));


	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', () => {
			settingsPanel.style.display = settingsPanel.style.display === 'none' ? 'block' : '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', () => {
					o_optionButton.click();
					setTimeout(() => {
						(index === 0 ? o_editMyInfoButton : o_editUserNoteButton).click();
					}, 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) o_imgButton.classList.add('mt_fix');
			if (switches.sw_deskFix) {
				$('.container, .items-center.text-lg.relative, div.flex.overflow-hidden.flex-grow>div.flex[class*="md:w-[40%]"], div.flex.overflow-hidden.flex-grow>div.flex[class*="md:w-[60%]"]').forEach(el => {
					el.classList.add('mt_fix');
				});
			}
			if (switches.sw_hideLLM) {
				const selectors = [
					'[id="claude-v3.5-sonnet"]',
					'[id="claude-v3-sonnet"]',
					'[id="claude-v3-opus"]',
					'[id="claude-v2"]',
					'[id="gpt-4-turbo"]',
					'[id="mixtral-8x7b-instruct"]',
					'[id="toppy-m-7b"]'
				];
				selectors.forEach(selector => {
					const parentElement = getAncestor(selector, 4);
					if (parentElement) {
						parentElement.style.display = 'none';
					}
				});
			}
		}
	}

	window.addEventListener('load', () => {
		setStylesheet();
		checkSettings();
		createSettingsUI();
		setTimeout(initializeObserver, 100);
	});
})();