Caveduck Modifier

修改Caveduck網站的樣式。

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

  1. // ==UserScript==
  2. // @name Caveduck Modifier
  3. // @namespace https://labs.muyi.tw/caveduck_modifier/
  4. // @version 0.29.5
  5. // @description 修改Caveduck網站的樣式。
  6. // @license AGPL-3.0-or-later
  7. // @author 慕儀
  8. // @match *://caveduck.io/*
  9. // @grant GM_addStyle
  10. // @icon data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZlcnNpb249IjEuMSIgdmlld0JveD0iMCAwIDEyODAgMTI4MCI+CiAgPGRlZnM+CiAgICA8c3R5bGU+CiAgICAgIC5jbHMtMSB7CiAgICAgICAgZmlsbDogI2ZmZjsKICAgICAgfQoKICAgICAgLmNscy0yIHsKICAgICAgICBmaWxsOiAjZjJiNDEyOwogICAgICB9CiAgICA8L3N0eWxlPgogIDwvZGVmcz4KICA8IS0tIEdlbmVyYXRvcjogQWRvYmUgSWxsdXN0cmF0b3IgMjguNy4xLCBTVkcgRXhwb3J0IFBsdWctSW4gLiBTVkcgVmVyc2lvbjogMS4yLjAgQnVpbGQgMTQyKSAgLS0+CiAgPGc+CiAgICA8ZyBpZD0iQ2F2ZWR1Y2siPgogICAgICA8ZyBpZD0iQ2F2ZWR1Y2stMiIgZGF0YS1uYW1lPSJDYXZlZHVjayI+CiAgICAgICAgPHBhdGggZD0iTTEwOTYuMSw0NTMuNWMzMDUuMiw0OTcuMywxNTIuNiw3NDYtNDU3LjgsNzQ2Uy0xMjQuNiw5NTAuOCwxODAuNiw0NTMuNWMzMDUuMi00OTcuMyw2MTAuMy00OTcuMyw5MTUuNSwwWiIvPgogICAgICAgIDxwYXRoIGNsYXNzPSJjbHMtMSIgZD0iTTExNDcuNiw0NTMuNWMyMDguOCwzNjEuNywxMDQuNCw1NDIuNS0zMTMuMiw1NDIuNXMtNTIyLTE4MC44LTMxMy4yLTU0Mi41YzIwOC44LTM2MS43LDQxNy42LTM2MS43LDYyNi41LDBaIi8+CiAgICAgICAgPHBhdGggY2xhc3M9ImNscy0yIiBkPSJNNjg0LDcyMS4yYzEwMC4yLDE3My42LDUwLjEsMjYwLjQtMTUwLjMsMjYwLjRzLTI1MC42LTg2LjgtMTUwLjMtMjYwLjRjMTAwLjItMTczLjYsMjAwLjUtMTczLjYsMzAwLjcsMFoiLz4KICAgICAgICA8cGF0aCBkPSJNODcxLjksNjEyLjdjMjUuMSw0My40LDEyLjUsNjUuMS0zNy42LDY1LjFzLTYyLjYtMjEuNy0zNy42LTY1LjFjMjUuMS00My40LDUwLjEtNDMuNCw3NS4yLDBaIi8+CiAgICAgIDwvZz4KICAgIDwvZz4KICA8L2c+Cjwvc3ZnPg==
  11. // ==/UserScript==
  12.  
  13. (function () {
  14. 'use strict';
  15.  
  16. let inLanguage;
  17. let debouncedAutoHeight;
  18. const $ = (selector) => document.querySelectorAll(selector);
  19. const $$ = (selector) => document.querySelector(selector);
  20. const tarAutoHeight = `prompt-input`;
  21. const tarAutoScrollHeight = `lorebook-data-input textarea, #charDesc`;
  22. const textReplaceSelector = `#chatMessages b:not([data-text-replaced]), #chatMessages p:not([data-text-replaced])`;
  23. const cURL = window.location.href;
  24. const muyiStyles = 'https://labs.muyi.tw/caveduck_modifier/style2.css?v=11312091930';
  25. const fontStyles = `
  26. user-input-form div[ng-repeat] textarea,
  27. #chatMessages b,
  28. #chatMessages p,
  29. form[ng-if~="!!chat.editMode"] textarea {
  30. font: normal clamp(16px, .95vw, 32px) / 1.75em var(--m_ff1);
  31. }
  32. #chatMessages b {
  33. font-family: var(--m_ff2);
  34. font-weight: 400;
  35. }
  36. form[ng-if~="!!chat.editMode"] textarea {
  37. font-size: var(--m_font-size);
  38. }
  39. user-input-form div[ng-repeat] textarea {
  40. font-size: var(--m_font-size);
  41. }
  42. `;
  43. const charMap = {
  44. '\\.{2,}': '⋯⋯',
  45. '⋯': '⋯⋯',
  46. '⋯{3,}': '⋯⋯',
  47. '!': '!',
  48. '\\?': '?',
  49. '~': '~',
  50. ';': ';',
  51. ':': ':',
  52. ',': ',',
  53. '\\.': '。',
  54. '\\(': '(',
  55. '\\)': ')'
  56. };
  57. const locale = {
  58. 'zh-hant': {
  59. cb_fontOverride: ['覆蓋字型', '作用頁:Talk<br>用慕儀喜歡的自訂字型取代預設字型。'],
  60. cb_shortButtons: ['快捷按鈕', '作用頁:Talk<br>將「我的資訊」與「使用者筆記」按鈕移到右側。'],
  61. cb_replaceText: ['取代符號', '作用頁:Talk<br>Claude 3 Haiku會使用錯誤的中文標點符號,這個功能可以修正它。'],
  62. cb_deskFix: ['桌面顯示修正', '作用頁:Talk<br>修正高解析度下的顯示體驗,讓對話畫面佔用全版,且圖片顯示區域更大。'],
  63. cb_mdFix: ['行動顯示修正', '作用頁:Talk<br>修正行動裝置的顯示問題。'],
  64. cb_hideLLM: ['隱藏低效LLM', '作用頁:Talk<br>隱藏表現與成本不合比例的LLM,避免誤選取。'],
  65. cb_autoHeight: ['編輯框自動高度', '作用頁:Edit Character、Lorebook、Custom prompt<br>每個項目使用卷軸十分愚蠢,勾選此項可以將其設為自動高度。'],
  66. toggleButton: '慕儀\n神器',
  67. reloadButton: '套用並重載',
  68. },
  69. 'en': {
  70. cb_fontOverride: ['Override Font', 'Active on: Talk<br>Replace default font with MuYi\'s preferred custom font.'],
  71. cb_shortButtons: ['Shortcut buttons', 'Active on: Talk<br>Move the "My Information" and "User Notes" buttons to the right side.'],
  72. cb_replaceText: ['Replace Symbols', 'Active on: Talk<br>Claude 3 Haiku uses incorrect Chinese punctuation. This feature fixes it.'],
  73. 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.'],
  74. cb_mdFix: ['Mobile Display Fix', 'Active on: Talk<br>Fix the display issues of mobile devices.'],
  75. cb_hideLLM: ['Hide unimportant LLMs', 'Active on: Talk<br>Hide LLMs whose performance does not correlate with their costs to avoid misselection.'],
  76. 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.'],
  77. toggleButton: 'MuYi\'s\nToolbox',
  78. reloadButton: 'Apply and reload',
  79. },
  80. };
  81.  
  82. const settings = Object.keys(locale['en'])
  83. .filter(key => key.startsWith('cb_'))
  84. .map(localeName => ({
  85. localeName: localeName,
  86. key: `sw_${localeName.slice(3)}`
  87. }));
  88.  
  89. const switches = {};
  90. settings.forEach(setting => {
  91. switches[setting.key] = JSON.parse(localStorage.getItem(`enable${setting.key.slice(3)}`) || 'false');
  92. });
  93.  
  94. const domElements = {
  95. o_editMyInfoButton: 'button[ng-click="uiState.settingModalMode = \'edit_my_info\'"]',
  96. o_editUserNoteButton: 'button[ng-click="uiState.settingModalMode = \'edit_user_note\'"]',
  97. o_optionButton: '#optionButton',
  98. o_imgButton: '.hidden[ng-show~="backgroundImage"]'
  99. };
  100.  
  101. const [
  102. o_editMyInfoButton,
  103. o_editUserNoteButton,
  104. o_optionButton,
  105. o_imgButton
  106. ] = Object.values(domElements).map(selector => $$(selector));
  107.  
  108.  
  109. function getAncestor(selector, level) {
  110. if (typeof selector !== 'string' || typeof level !== 'number' || level < 0) {
  111. throw new Error('Invalid parameters');
  112. }
  113. const element = document.querySelector(selector);
  114. if (!element) {
  115. return null;
  116. }
  117. if (level === 0) {
  118. return element;
  119. }
  120. let current = element;
  121. for (let i = 0; i < level; i++) {
  122. current = current.parentElement;
  123. if (!current) {
  124. return null;
  125. }
  126. }
  127. return current;
  128. }
  129.  
  130. // 添加自訂樣式
  131. function addCustomStyles() {
  132. GM_addStyle(fontStyles);
  133. console.log("Custom styles added.");
  134. }
  135.  
  136. // 自動調整高度的核心函式
  137. function autoHeight(el) {
  138. el.style.height = 'auto';
  139. el.style.overflow = 'auto';
  140. }
  141.  
  142. function autoScrollHeight(el) {
  143. autoHeight(el);
  144. el.style.height = `${el.scrollHeight}px`;
  145. }
  146.  
  147. // 初始化符合條件的元素
  148. function initializeAutoHeight() {
  149. if (!debouncedAutoHeight) {
  150. debouncedAutoHeight = debounce(() => {
  151. $(tarAutoHeight).forEach(autoHeight);
  152. $(tarAutoScrollHeight).forEach(autoScrollHeight);
  153. }, 666, 2);
  154. debouncedAutoHeight();
  155. window.addEventListener('keydown', debouncedAutoHeight);
  156. window.addEventListener('click', debouncedAutoHeight);
  157. }
  158. }
  159.  
  160.  
  161. // 替換指定選擇符的內容
  162. function replaceTextContent() {
  163. const processedAttribute = "data-text-replaced"; // 標記屬性名稱
  164. const el = $(`${textReplaceSelector}:not([${processedAttribute}])`);
  165. el.forEach((el) => {
  166. let originalText = el.textContent;
  167. for (const [pattern, replacement] of Object.entries(charMap)) {
  168. originalText = originalText.replace(new RegExp(pattern, 'g'), replacement);
  169. }
  170. el.textContent = originalText;
  171. el.setAttribute(processedAttribute, ""); // 添加標記屬性
  172. });
  173. }
  174.  
  175. // 延遲觸發的去抖函式
  176. function debounce(func, delay, repeat) {
  177. let timer = null;
  178. let count = 1;
  179. return () => {
  180. func();
  181. if (timer) clearInterval(timer);
  182. timer = setInterval(() => {
  183. func();
  184. count += 1;
  185. if (count >= repeat) {
  186. clearInterval(timer);
  187. }
  188. }, delay);
  189. };
  190. }
  191.  
  192. // 啟動 MutationObserver
  193. function initializeObserver() {
  194. const observer = new MutationObserver(() => {
  195. mainAction();
  196. });
  197. observer.observe(document.body, { childList: true, subtree: true });
  198. console.log("MutationObserver initialized.");
  199. }
  200.  
  201. // 檢查 inLanguage 並啟動必要功能
  202. function checkInLanguage() {
  203. const script = $$('script[type="application/ld+json"]');
  204. if (script) {
  205. try {
  206. const jsonData = JSON.parse(script.textContent);
  207. inLanguage = jsonData[0]?.inLanguage || '';
  208. } catch (error) {
  209. console.error("Failed to parse JSON:", error);
  210. }
  211. }
  212. }
  213.  
  214. function createSettingsUI() {
  215. const lang = ['zh-hant', 'zh-hans'].includes(inLanguage) ? 'zh-hant' : 'en';
  216. const texts = locale[lang];
  217.  
  218. // 創建核取方塊
  219. const createCheckbox = (setting) => {
  220. const container = document.createElement('div');
  221. const checkbox = document.createElement('input');
  222. const label = document.createElement('label');
  223. const desc = document.createElement('div');
  224. desc.className = 'desc';
  225.  
  226. checkbox.type = 'checkbox';
  227. const storageKey = `enable${setting.key.slice(3)}`;
  228. checkbox.id = storageKey;
  229.  
  230. const isChecked = JSON.parse(localStorage.getItem(storageKey) || 'false');
  231. checkbox.checked = isChecked;
  232. label.setAttribute('for', storageKey);
  233. label.textContent = texts[setting.localeName][0];
  234. desc.innerHTML = texts[setting.localeName][1];
  235. label.appendChild(desc);
  236. checkbox.addEventListener('change', () => {
  237. localStorage.setItem(storageKey, checkbox.checked);
  238. });
  239. container.appendChild(checkbox);
  240. container.appendChild(label);
  241. return container;
  242. };
  243.  
  244. // 創建按鈕和設定視窗
  245. const mt = document.createElement('div');
  246. mt.id = 'mt';
  247.  
  248. const toggleButton = document.createElement('button');
  249. toggleButton.className = 'button--red mt_toggleButton';
  250. toggleButton.textContent = texts.toggleButton;
  251. if (lang === 'zh-hant') toggleButton.style.fontSize = '.8rem';
  252.  
  253. const settingsPanel = document.createElement('div');
  254. settingsPanel.className = 'mt_fixed mt_settingsPanel';
  255. settingsPanel.style.display = 'none';
  256.  
  257. // 添加核取方塊
  258. settings.forEach((setting) => {
  259. settingsPanel.appendChild(createCheckbox(setting));
  260. });
  261.  
  262. // 重整按鈕
  263. const reloadButton = document.createElement('button');
  264. reloadButton.textContent = texts.reloadButton;
  265. reloadButton.className = 'button--red';
  266. reloadButton.addEventListener('click', () => location.reload());
  267. settingsPanel.appendChild(reloadButton);
  268.  
  269. // 切換視窗顯示
  270. toggleButton.addEventListener('click', () => {
  271. settingsPanel.style.display = settingsPanel.style.display === 'none' ? 'block' : 'none';
  272. });
  273.  
  274. // 添加到頁面
  275. document.body.appendChild(mt);
  276. mt.appendChild(toggleButton);
  277. document.body.appendChild(settingsPanel);
  278.  
  279. // 快捷按鈕
  280. if (switches.sw_shortButtons && mURL('*/talk/*')) {
  281. ['👤', '📝'].forEach((text, index) => {
  282. const button = document.createElement('button');
  283. button.textContent = text;
  284. button.addEventListener('click', () => {
  285. o_optionButton.click();
  286. setTimeout(() => {
  287. (index === 0 ? o_editMyInfoButton : o_editUserNoteButton).click();
  288. }, 100);
  289. });
  290. mt.appendChild(button);
  291. });
  292. }
  293.  
  294. }
  295.  
  296. function checkSettings() {
  297. checkInLanguage();
  298. settings.forEach(setting => {
  299. switches[setting.switchVar] = JSON.parse(localStorage.getItem(setting.storageKey) || 'false');
  300. });
  301.  
  302. if (switches.sw_fontOverride) addCustomStyles();
  303. mainAction();
  304. }
  305.  
  306. function mURL(pattern) {
  307. const patternParts = pattern.split('*');
  308. let lastIndex = 0;
  309. for (let part of patternParts) {
  310. if (part === "") continue;
  311. const index = cURL.indexOf(part, lastIndex);
  312. if (index === -1) return false;
  313. lastIndex = index + part.length;
  314. }
  315. return true;
  316. }
  317.  
  318. function setStylesheet() {
  319. const link = document.createElement("link");
  320. link.rel = 'stylesheet';
  321. link.href = muyiStyles;
  322. document.head.appendChild(link);
  323. }
  324.  
  325. function mainAction() {
  326. if (switches.sw_autoHeight && (mURL('*/created-characters/*') || mURL('*/prompt-build-script/*') || mURL('*/lorebook-editor/*'))) initializeAutoHeight();
  327. if (['zh-hant', 'zh-hans', 'ja', 'ko'].includes(inLanguage) && (switches.sw_replaceText)) replaceTextContent();
  328. if (mURL('*/public')) {
  329. if (switches.sw_mdFix) {
  330. $$('section.flex-col.py-60 > h3.text-2xl').classList.remove('px-16');
  331. }
  332. }
  333. if (mURL('*/talk/*')) {
  334. if (switches.sw_mdFix) o_imgButton.classList.add('mt_fix');
  335. if (switches.sw_deskFix) {
  336. $('.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 => {
  337. el.classList.add('mt_fix');
  338. });
  339. }
  340. if (switches.sw_hideLLM) {
  341. const selectors = [
  342. '[id="claude-v3.5-sonnet"]',
  343. '[id="claude-v3-sonnet"]',
  344. '[id="claude-v3-opus"]',
  345. '[id="claude-v2"]',
  346. '[id="gpt-4-turbo"]',
  347. '[id="mixtral-8x7b-instruct"]',
  348. '[id="toppy-m-7b"]'
  349. ];
  350. selectors.forEach(selector => {
  351. const parentElement = getAncestor(selector, 4);
  352. if (parentElement) {
  353. parentElement.style.display = 'none';
  354. }
  355. });
  356. }
  357. }
  358. }
  359.  
  360. window.addEventListener('load', () => {
  361. setStylesheet();
  362. checkSettings();
  363. createSettingsUI();
  364. setTimeout(initializeObserver, 100);
  365. });
  366. })();