AIDungeon QoL Tool

A QoL script for AID, adding customizable hotkeys, also increases performance by removing the countless span elements from last response

当前为 2024-05-03 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name AIDungeon QoL Tool
  3. // @version 1.0.5
  4. // @description A QoL script for AID, adding customizable hotkeys, also increases performance by removing the countless span elements from last response
  5. // @author AliH2K
  6. // @match https://*.aidungeon.com/*
  7. // @icon https://play-lh.googleusercontent.com/ALmVcUVvR8X3q-hOUbcR7S__iicLgIWDwM9K_9PJy87JnK1XfHSi_tp1sUlJJBVsiSc
  8. // @require https://code.jquery.com/jquery-3.7.1.min.js
  9. // @require https://update.greasyfork.org/scripts/383527/701631/Wait_for_key_elements.js
  10. // @require https://update.greasyfork.org/scripts/439099/1203718/MonkeyConfig%20Modern%20Reloaded.js
  11. // @grant GM_addStyle
  12. // @grant GM_getValue
  13. // @grant GM_setValue
  14. // @grant GM_deleteValue
  15. // @grant GM_registerMenuCommand
  16. // @license MIT
  17. // @namespace https://greasyfork.org/users/1294499
  18. // ==/UserScript==
  19. /* global jQuery, $, waitForKeyElements, MonkeyConfig */
  20. const $ = jQuery.noConflict(true);
  21.  
  22. const getSetTextFunc = (value, parent) => {
  23. const inputElem = $(parent || value).find('input');
  24. if (!parent) {
  25. const booleans = inputElem
  26. .filter(':checkbox')
  27. .map((_, el) => el.checked)
  28. .get();
  29. if (!booleans[0]) return inputElem.val().toUpperCase();
  30. return booleans;
  31. } else {
  32. inputElem.each((i, el) => {
  33. if (el.type === 'checkbox') el.checked = value[i];
  34. else el.value = value.toUpperCase();
  35. });
  36. }
  37. };
  38.  
  39. const cfg = new MonkeyConfig({
  40. title: 'Configure',
  41. menuCommand: true,
  42. params: {
  43. Modifier_Keys: {
  44. type: 'custom',
  45. html: '<input id="ALT" type="checkbox" name="ALT" /> <label for="ALT">ALT</label> <input id="CTRL" type="checkbox" name="CTRL" /> <label for="CTRL">CTRL</label> <input id="SHIFT" type="checkbox" name="SHIFT" /> <label for="SHIFT">SHIFT</label>',
  46. set: getSetTextFunc,
  47. get: getSetTextFunc,
  48. default: [true, true, false]
  49. },
  50. Take_Turn: { type: 'custom', html: '<input type="text" maxlength="1" />', set: getSetTextFunc, get: getSetTextFunc, default: 'C' },
  51. Continue: { type: 'custom', html: '<input type="text" maxlength="1" />', set: getSetTextFunc, get: getSetTextFunc, default: 'A' },
  52. Retry: { type: 'custom', html: '<input type="text" maxlength="1" />', set: getSetTextFunc, get: getSetTextFunc, default: 'S' },
  53. Retry_History: { type: 'custom', html: '<input type="text" maxlength="1" />', set: getSetTextFunc, get: getSetTextFunc, default: 'X' },
  54. Erase: { type: 'custom', html: '<input type="text" maxlength="1" />', set: getSetTextFunc, get: getSetTextFunc, default: 'D' },
  55. Do: { type: 'custom', html: '<input type="text" maxlength="1" />', set: getSetTextFunc, get: getSetTextFunc, default: 'Q' },
  56. Say: { type: 'custom', html: '<input type="text" maxlength="1" />', set: getSetTextFunc, get: getSetTextFunc, default: 'W' },
  57. Story: { type: 'custom', html: '<input type="text" maxlength="1" />', set: getSetTextFunc, get: getSetTextFunc, default: 'E' },
  58. See: { type: 'custom', html: '<input type="text" maxlength="1" />', set: getSetTextFunc, get: getSetTextFunc, default: 'R' },
  59. Response_Underline: { type: 'checkbox', default: true },
  60. Response_Bg_Color: { type: 'checkbox', default: false }
  61. }
  62. });
  63.  
  64. const actionArray = [
  65. { name: 'Take_Turn', type: 'Command', 'aria-Label': 'Command: take a turn' },
  66. { name: 'Continue', type: 'Command', 'aria-Label': 'Command: continue' },
  67. { name: 'Retry', type: 'Command', 'aria-Label': 'Command: retry' },
  68. { name: 'Retry_History', type: 'History', 'aria-Label': 'Retry history' },
  69. { name: 'Erase', type: 'Command', 'aria-Label': 'Command: erase' },
  70. { name: 'Do', type: 'Mode', 'aria-Label': "Set to 'Do' mode" },
  71. { name: 'Say', type: 'Mode', 'aria-Label': "Set to 'Say' mode" },
  72. { name: 'Story', type: 'Mode', 'aria-Label': "Set to 'Story' mode" },
  73. { name: 'See', type: 'Mode', 'aria-Label': "Set to 'See' mode" }
  74. ];
  75.  
  76. const actionKeys = actionArray.map((action) => cfg.get(action.name));
  77.  
  78. const handleKeyPress = (e) => {
  79. if (e.repeat) return;
  80. const key = e.key.toUpperCase();
  81. const modifiers = ['ALT', 'CTRL', 'SHIFT'].map((mod) => e[`${mod.toLowerCase()}Key`]);
  82. const modifsActive = modifiers.every((value, index) => value === cfg.get('Modifier_Keys')[index]);
  83. const index = actionKeys.indexOf(key);
  84. if (modifsActive && index !== -1) {
  85. const action = actionArray[index];
  86. const targetElem = `[aria-label="${action['aria-Label']}"]`;
  87. if ($("[aria-label='Close text input']").length) $("[aria-label='Close text input']").click();
  88. if (action.type === 'Command') setTimeout(() => $(targetElem).click(), 50);
  89. else if (action.type === 'Mode') delayedClicks([() => $('[aria-label="Command: take a turn"]').click(), () => $('[aria-label="Change input mode"]').click(), () => $(targetElem).click()]);
  90. else if (action.type === 'History' && $('[aria-label="Retry history"]').length) setTimeout(() => $(targetElem).click(), 50);
  91. }
  92. const selectKeys = ['ARROWLEFT', 'ENTER', 'ARROWRIGHT'];
  93. if (selectKeys.includes(key) && $('[role="dialog"]').length) setTimeout(() => $("[role='dialog']").find("[role='button']")[selectKeys.indexOf(key)].click(), 50);
  94. };
  95.  
  96. const delayedClicks = (clicks, i = 0) => {
  97. if (i < clicks.length) {
  98. setTimeout(() => {
  99. clicks[i]();
  100. delayedClicks(clicks, i + 1);
  101. }, 50);
  102. }
  103. };
  104.  
  105. function handleChanges() {
  106. const targetNode = $("[role='article']")[0];
  107. let lastResponse = targetNode.lastChild.lastChild;
  108. targetNode.lastChild.children.length % 2 !== 0 && lastResponse.tagName === 'SPAN' ? (lastResponse.style.pointerEvents = 'none') : lastResponse.style.pointerEvents === 'none' ? (lastResponse.style.pointerEvents = '') : '';
  109. if (lastResponse.firstChild.nodeType !== 3 && lastResponse.tagName === 'SPAN') {
  110. const interval = setInterval(() => {
  111. const opacity = lastResponse.lastChild instanceof HTMLElement ? getComputedStyle(lastResponse.lastChild).opacity : '1';
  112. if (opacity === '1') {
  113. clearInterval(interval);
  114. const SPANS = Array.from(lastResponse.children);
  115. let joinedText = '';
  116. SPANS.forEach((span) => (joinedText += span.textContent));
  117. while (lastResponse.firstChild && lastResponse.firstChild.nodeType !== 3) lastResponse.removeChild(lastResponse.firstChild);
  118. if (joinedText.length > 1) lastResponse.textContent = joinedText;
  119. }
  120. }, 500);
  121. }
  122. }
  123.  
  124. const config = { childList: true, subtree: true };
  125. const observer = new MutationObserver(handleChanges);
  126.  
  127. waitForKeyElements("[role='article']", () => {
  128. if (window.location.href.includes('/read')) return storyGetPrep();
  129. if (!window.location.href.includes('/play')) return;
  130. const targetNode = $("[role='article']")[0];
  131. observer.observe(targetNode, config);
  132. handleChanges();
  133. const CSS = `
  134. div>span:last-child>#transition-opacity:last-child, #game-backdrop-saturate {
  135. border-bottom-color: ${cfg.get('Response_Underline') ? 'var(--color-61)' : 'unset'};
  136. border-bottom-width: ${cfg.get('Response_Underline') ? '2px' : 'unset'};
  137. border-bottom-style: ${cfg.get('Response_Underline') ? 'solid' : 'unset'};
  138. background-color: ${cfg.get('Response_Bg_Color') ? 'var(--color-60)' : 'unset'};
  139. backdrop-filter: unset;
  140. }
  141. `;
  142. GM_addStyle(CSS);
  143. document.addEventListener('keydown', handleKeyPress);
  144. });
  145.  
  146. function storyGetPrep() {
  147. const reference = [...$('[role=button]')].find((e) => e.innerText === 'Aa');
  148.  
  149. function inject(label, action) {
  150. const button = reference.cloneNode(true);
  151. button.removeAttribute('aria-disabled');
  152. button.setAttribute('style', 'pointer-events: all !important;');
  153. button.querySelector('p').innerText = label;
  154. button.onclick = (e) => {
  155. e.preventDefault();
  156. e.bubbles = false;
  157. action();
  158. };
  159. reference.parentNode.prepend(button);
  160. }
  161.  
  162. function onSave(type) {
  163. const story = $('[aria-label="Story"]')[0];
  164. const title = $('[role=heading]')[0]?.innerText;
  165.  
  166. if (!story || !title) return alert('Wait for content to load first!');
  167.  
  168. let text = story.innerText.replaceAll(/w_\w+\n+\s+/g, type === 'text' ? '' : '> ');
  169. if (type === 'md') text = '## ' + title + '\n\n' + text;
  170.  
  171. text = text.replaceAll(/\n+/g, '\n\n');
  172. const blob = URL.createObjectURL(new Blob([text], { type: type === 'text' ? 'text/plain' : 'text/x-markdown' }));
  173. const a = document.createElement('a');
  174. a.download = title + (type === 'text' ? '.txt' : '.md');
  175. a.href = blob;
  176. a.click();
  177. URL.revokeObjectURL(blob);
  178. }
  179.  
  180. inject('.txt', () => onSave('text'));
  181. inject('.md', () => onSave('md'));
  182. }