Caveduck Modifier

修改Caveduck網站的樣式。

Ajankohdalta 28.11.2024. Katso uusin versio.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name			Caveduck Modifier
// @namespace		https://labs.muyi.tw/caveduck_modifier/
// @version			0.26.1
// @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_shortButtons, sw_replaceText, sw_mdCorrection, sw_autoHeight;
	let debouncedAutoHeight;
	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 $ = (selector) => document.querySelectorAll(selector);
	const $$ = (selector) => document.querySelector(selector);
	const o_editMyInfoButton = $$('button[ng-click="onEditMyInfoButtonClicked()"]');
	const o_editUserNoteButton = $$('button[ng-click="onEditUserNoteButtonClicked()"]');
	const o_optionButton = $$('#optionButton');
	const o_editButton = $$('#editButton');
	const o_imgButton = $$('.hidden[ng-show~="backgroundImage"]');
	const muyiStyles = 'https://labs.muyi.tw/caveduck_modifier/style2.css?v=11311281032';
	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);
			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,}': '⋯⋯',
		'!': '!',
		'\\?': '?',
		'~': '~',
		';': ';',
		':': ':',
		',': ',',
		'\\.': '。',
		'\\(': '(',
		'\\)': ')'
	};
	// 主要動作
	function mainAction() {
		if (sw_autoHeight && (mURL('*/created-characters/edit/*')||mURL('*/prompt-build-script/*')||mURL('*/lorebook-editor/*'))) initializeAutoHeight();
		if (['zh-hant', 'zh-hans', 'ja', 'ko'].includes(inLanguage) && (sw_replaceText)) replaceTextContent();
		if (sw_mdCorrection && mURL('*/talk/*')) o_imgButton.classList.add('mt_fix');
	}
	// 添加自訂樣式
	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);
			}, 200, 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 = 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() {
		const locale = {
			'zh-hant': {
				fontCheckbox: ['覆蓋字型', '作用頁:Talk<br>用慕儀喜歡的自訂字型取代預設字型。'],
				sbCheckbox: ['快捷按鈕', '作用頁:Talk<br>將「我的資訊」與「使用者筆記」按鈕移到右側。'],
				replaceCheckbox: ['取代符號', '作用頁:Talk<br>Claude 3 Haiku會使用錯誤的中文標點符號,這個功能可以修正它。'],
				mdCorrection: ['行動裝置修正', '作用頁:Talk<br>修正行動裝置的顯示問題。'],
				autoHeightCheckbox: ['編輯頁無卷軸', '作用頁:Edit Character、Lorebook、Custom prompt<br>慕儀認為每個項目使用卷軸十分愚蠢,勾選此項可以將其設為自動高度。'],
				toggleButton: '慕儀\n神器',
				reloadButton: '套用並重載',
			},
			'en': {
				fontCheckbox: ['Override Font', 'Effective page: Talk<br>Replace default font with MuYi\'s preferred custom font.'],
				sbCheckbox: ['Shortcut buttons', 'Effective page: Talk<br>Move the "My Information" and "User Notes" buttons to the right side.'],
				replaceCheckbox: ['Replace Symbols', 'Effective page: Talk<br>Claude 3 Haiku uses incorrect Chinese punctuation. This feature fixes it.'],
				mdCorrection: ['Mobile Device Correction', 'Effective page: Talk<br>Fix the display issues of mobile devices.'],
				autoHeightCheckbox: ['No Scrollbar for Edit Page', 'Effective page: Edit Character、Lorebook、Custom prompt<br>MuYi thinks using scrollbars for each item is stupid. Enable this to auto-height.'],
				toggleButton: 'MuYi\'s\nToolbox',
				reloadButton: 'Apply and reload',
			},
		};

		const lang = ['zh-hant', 'zh-hans'].includes(inLanguage) ? 'zh-hant' : 'en';
		const texts = locale[lang];

		const checkboxes = [
			{ key: 'fontCheckbox', id: 'enableFontOverride' },
			{ key: 'sbCheckbox', id: 'enableSbCheckbox' },
			{ key: 'replaceCheckbox', id: 'enableReplaceText' },
			{ key: 'mdCorrection', id: 'enableMdCorrection' },
			{ key: 'autoHeightCheckbox', id: 'enableAutoHeight' },
		];

		// 創建核取方塊
		const createCheckbox = ({ key, id }) => {
			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';
			checkbox.id = id;

			const isChecked = JSON.parse(localStorage.getItem(id) || 'false');
			checkbox.checked = isChecked;
			label.setAttribute('for', id);
			label.textContent = texts[key][0];
			desc.innerHTML = texts[key][1];
			label.appendChild(desc);
			checkbox.addEventListener('change', () => {
				localStorage.setItem(id, 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 = '1rem';

		const settingsPanel = document.createElement('div');
		settingsPanel.className = 'mt_fixed mt_settingsPanel';
		settingsPanel.style.display = 'none';

		// 添加核取方塊
		checkboxes.forEach((checkbox) => {
			settingsPanel.appendChild(createCheckbox(checkbox));
		});

		// 重整按鈕
		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 (sw_shortButtons && mURL('*/talk/*')) {
			['👤', '📝'].forEach((text, index) => {
				const button = document.createElement('button');
				button.textContent = text;
				button.addEventListener('click', () => {
					o_optionButton.click();
					o_editButton.click();
					(index === 0 ? o_editMyInfoButton : o_editUserNoteButton).click();
				});
				mt.appendChild(button);
			});
		}
	}


	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 checkSettings() {
		checkInLanguage();
		sw_fontOverride = JSON.parse(localStorage.getItem('enableFontOverride') || 'false');
		sw_shortButtons = JSON.parse(localStorage.getItem('enableSbCheckbox') || 'false');
		sw_replaceText = JSON.parse(localStorage.getItem('enableReplaceText') || 'false');
		sw_autoHeight = JSON.parse(localStorage.getItem('enableAutoHeight') || 'false');
		sw_mdCorrection = JSON.parse(localStorage.getItem('enableMdCorrection') || '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);
	});

})();