Caveduck Modifier

修改Caveduck網站的樣式。

目前為 2024-11-26 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name			Caveduck Modifier
// @namespace		https://muyi.tw/labs/caveduck_modifier/
// @version			0.23
// @description		修改Caveduck網站的樣式。
// @license			AGPL-3.0-or-later
// @author			慕儀
// @match			*://caveduck.io/talk/*
// @match			*://caveduck.io/created-characters/edit/*
// @match			*://caveduck.io/*-editor/*
// @match			*://caveduck.io/prompt-build-script/*
// @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, sw_fontOverride, sw_replaceText, sw_autoHeight;
	const tarAutoHeight = `prompt-input`;
	const tarAutoScrollHeight = `lorebook-data-input textarea`;
	const textReplaceSelector = `#chatMessages b:not([data-text-replaced]), #chatMessages p:not([data-text-replaced])`;
	const $ = (selector) => document.querySelectorAll(selector);
	const muyiStyles = 'https://muyi.tw/labs/caveduck_modifier/style.css?v=1131127';
	const fontStyles = `
		user-input-form div[ng-repeat] textarea,
		#chatMessages b,
		#chatMessages p,
		form[ng-if~="!!chat.editMode"] textarea {
			font: normal clamp(16px, .8vw, 32px) / 1.75em var(--m_ff1);
		}
		#chatMessages b {
			font-family: var(--m_ff2);
		}
		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,}': '⋯⋯',
		'!': '!',
		'\\?': '?',
		'~': '~',
		';': ';',
		':': ':',
		',': ',',
		'\\.': '。',
		'\\(': '(',
		'\\)': ')'
	};
	// 主要動作
	function mainAction() {
		if (sw_autoHeight) initializeAutoHeight();
		if (['zh-hant', 'zh-hans', 'ja', 'ko'].includes(inLanguage) && (sw_replaceText)) replaceTextContent();
	}
	// 添加自訂樣式
	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() {
		const debouncedAutoHeight = debounce(() => {
			$(tarAutoHeight).forEach(autoHeight);
			$(tarAutoScrollHeight).forEach(autoScrollHeight);
		}, 200, 2);
		debouncedAutoHeight();
		window.addEventListener('keydown', debouncedAutoHeight);
		window.addEventListener('mousedown', 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 = document.querySelector('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() {
		let toggleButtonText, reloadButtonText, fontCheckboxTitle, fontCheckboxDesc, replaceCheckboxTitle, replaceCheckboxDesc, autoHeightCheckboxTitle, autoHeightCheckboxDesc
		const isZH = ['zh-hant', 'zh-hans'].includes(inLanguage);
		toggleButtonText = isZH ? '慕儀\n神器' : 'MuYi\'s\nToolbox';
		reloadButtonText = isZH ? '套用並重載' : 'Apply and reload';
		fontCheckboxTitle = isZH ? '覆蓋字型' : 'Override Font';
		fontCheckboxDesc = isZH ? '作用頁:Talk<br>用慕儀喜歡的自訂字型取代預設字型,僅限聊天頁。' : 'Effective page:Talk<br>Replace default font with MuYi\'s preferred custom font.';
		replaceCheckboxTitle = isZH ? '取代符號' : 'Replace Symbols';
		replaceCheckboxDesc = isZH ? '作作用頁:Talk<br>Claude 3 Haiku會使用錯誤的中文標點符號,這個功能可以修正它,僅限聊天頁。' : 'Effective page:Talk<br>Claude 3 Haiku uses incorrect Chinese punctuation. This feature fixes it.';
		autoHeightCheckboxTitle = isZH ? '編輯頁無卷軸' : 'No Scrollbar for Edit Page';
		autoHeightCheckboxDesc = isZH ? '作用頁:Edit Character、Lorabook、Custom prompt<br>慕儀認為每個項目使用卷軸十分愚蠢,勾選此項可以將其設為自動高度。' : 'Effective page:Edit Character、Lorabook、Custom prompt<br>MuYi thinks using scrollbars for each item is stupid. Enable this to auto-height.';

		// 建立按鈕
		const toggleButton = document.createElement('button');
		toggleButton.className = 'button--red mt_fixed mt_toggleButton'
		toggleButton.textContent = toggleButtonText;

		// 建立設定視窗
		const settingsPanel = document.createElement('div');
		settingsPanel.className = 'mt_fixed mt_settingsPanel';
		settingsPanel.style.display = 'none';

		// 核取方塊:覆蓋字型
		const fontCheckbox = createCheckbox(fontCheckboxTitle, 'enableFontOverride', true, fontCheckboxDesc);
		settingsPanel.appendChild(fontCheckbox);

		// 核取方塊:取代符號
		const replaceCheckbox = createCheckbox(replaceCheckboxTitle, 'enableReplaceText', true, replaceCheckboxDesc);
		settingsPanel.appendChild(replaceCheckbox);

		// 核取方塊:設定框無卷軸
		const autoHeightCheckbox = createCheckbox(autoHeightCheckboxTitle, 'enableautoHeight', true, autoHeightCheckboxDesc);
		settingsPanel.appendChild(autoHeightCheckbox);

		// 重整按鈕
		const reloadButton = document.createElement('button');
		reloadButton.textContent = reloadButtonText;
		reloadButton.style.marginTop = '10px';
		reloadButton.style.padding = '5px 10px';
		reloadButton.style.cursor = 'pointer';
		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(toggleButton);
		document.body.appendChild(settingsPanel);
	}

	// 建立核取方塊元件
	function createCheckbox(labelText, storageKey, defaultValue, tooltipText = '') {
		const wrapper = document.createElement('div');

		const label = document.createElement('label');

		const checkbox = document.createElement('input');
		checkbox.type = 'checkbox';
		checkbox.checked = JSON.parse(localStorage.getItem(storageKey) || defaultValue);

		checkbox.addEventListener('change', () => {
			localStorage.setItem(storageKey, checkbox.checked);
		});

		label.appendChild(checkbox);
		label.appendChild(document.createTextNode(labelText));
		if (tooltipText) {
			const tooltip = document.createElement('div');
			tooltip.innerHTML = `${tooltipText}`;
			label.appendChild(tooltip);
		}
		wrapper.appendChild(label);
		return wrapper;
	}

	function checkSettings() {
		checkInLanguage();
		sw_fontOverride = JSON.parse(localStorage.getItem('enableFontOverride') || 'false');
		sw_replaceText = JSON.parse(localStorage.getItem('enableReplaceText') || 'false');
		sw_autoHeight = JSON.parse(localStorage.getItem('enableautoHeight') || 'false');
		if (sw_fontOverride) addCustomStyles();
		mainAction();
	}

	function setStylesheet() {
		const link = document.createElement("link");
		link.rel = 'stylesheet';
		link.href = muyiStyles;
		document.head.appendChild(link);
	}

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

})();